///|
priv struct SourceUse {
  path : String
  winning : Array[String]
  shadowed : Array[String]
  error : Diagnostic?
}

///|
fn copy_diagnostic(d : Diagnostic) -> Diagnostic {
  {
    code: d.code,
    path: d.path,
    message: d.message,
    offset: d.offset,
    related: d.related.copy(),
  }
}

///|
/// Applicability and field contribution for one source within indexed scans.
pub struct SourceCoverageEntry {
  priv path : String
  priv keys : Array[String]
  priv scans : Array[SourceUse]
}

///|
pub fn SourceCoverageEntry::path(self : SourceCoverageEntry) -> String {
  self.path
}

///|
pub fn SourceCoverageEntry::metadata_keys(
  self : SourceCoverageEntry,
) -> Array[String] {
  self.keys.copy()
}

///|
pub fn SourceCoverageEntry::applicable_paths(
  self : SourceCoverageEntry,
) -> Array[String] {
  self.scans.map(s => s.path)
}

///|
pub fn SourceCoverageEntry::winning_paths(
  self : SourceCoverageEntry,
) -> Array[String] {
  self.scans.filter(s => !s.winning.is_empty()).map(s => s.path)
}

///|
pub fn SourceCoverageEntry::fully_shadowed_paths(
  self : SourceCoverageEntry,
) -> Array[String] {
  self.scans
  .filter(s => s.error is None && !self.keys.is_empty() && s.winning.is_empty())
  .map(s => s.path)
}

///|
pub fn SourceCoverageEntry::unresolved_paths(
  self : SourceCoverageEntry,
) -> Array[String] {
  self.scans.filter(s => s.error is Some(_)).map(s => s.path)
}

///|
pub struct SourceCoverageReport {
  priv entries : Array[SourceCoverageEntry]
  priv scan_count : Int32
  priv errors : Array[Diagnostic]
}

///|
pub fn SourceCoverageReport::entries(
  self : SourceCoverageReport,
) -> Array[SourceCoverageEntry] {
  self.entries.copy()
}

///|
pub fn SourceCoverageReport::error_count(self : SourceCoverageReport) -> Int32 {
  self.errors.length()
}

///|
pub fn SourceCoverageReport::diagnostics(
  self : SourceCoverageReport,
) -> Array[Diagnostic] {
  self.errors.map(copy_diagnostic)
}

///|
/// Sources without any applicable indexed scan; this is not a deletion verdict.
pub fn SourceCoverageReport::unmatched_paths(
  self : SourceCoverageReport,
) -> Array[String] {
  self.entries.filter(e => e.scans.is_empty()).map(e => e.path)
}

///|
/// Account for every supplied sidecar, including empty and ambiguous sources.
/// Failed scans never produce speculative winning/shadowed assignments.
pub fn DatasetIndex::source_coverage(
  self : DatasetIndex,
) -> SourceCoverageReport {
  let audit = self.audit_all()
  let sources = self.sidecars.copy()
  sources.sort_by((a, b) => lexical_compare(a.selector.path, b.selector.path))
  let entries = sources.map(source => {
    let keys = sorted_keys(source.metadata)
    let scans : Array[SourceUse] = []
    for i, data in self.files {
      if applies(source.selector, data) {
        let outcome = audit.entries[i]
        match outcome.result {
          Some(result) => {
            let winning : Array[String] = []
            let shadowed : Array[String] = []
            for key in keys {
              let history = result.origins[key]
              if history[history.length() - 1].path == source.selector.path {
                winning.push(key)
              } else {
                shadowed.push(key)
              }
            }
            scans.push({ path: data.path, winning, shadowed, error: None, })
          }
          None =>
            scans.push({
              path: data.path,
              winning: [],
              shadowed: [],
              error: outcome.error,
            })
        }
      }
    }
    { path: source.selector.path, keys, scans, }
  })
  let errors : Array[Diagnostic] = []
  for outcome in audit.entries {
    if outcome.error is Some(error) {
      errors.push(error)
    }
  }
  { entries, scan_count: self.files.length(), errors, }
}

///|
pub fn SourceCoverageReport::to_json(self : SourceCoverageReport) -> String {
  canonical_json(
    Json::object({
      "scope": Json::string("indexed_scans"),
      "scan_count": Json::number(self.scan_count.to_double()),
      "error_count": Json::number(self.error_count().to_double()),
      "unmatched_paths": Json::array(self.unmatched_paths().map(Json::string)),
      "diagnostics": Json::array(self.errors.map(d => d.to_json())),
      "entries": Json::array(
        self.entries.map(entry => {
          Json::object({
            "path": Json::string(entry.path),
            "metadata_keys": Json::array(entry.keys.map(Json::string)),
            "applicable_paths": Json::array(
              entry.applicable_paths().map(Json::string),
            ),
            "winning_paths": Json::array(
              entry.winning_paths().map(Json::string),
            ),
            "fully_shadowed_paths": Json::array(
              entry.fully_shadowed_paths().map(Json::string),
            ),
            "unresolved_paths": Json::array(
              entry.unresolved_paths().map(Json::string),
            ),
            "scans": Json::array(
              entry.scans.map(scan => {
                let fields : Map[String, Json] = {
                  "data_path": Json::string(scan.path),
                  "status": Json::string(
                    if scan.error is Some(_) {
                      "error"
                    } else {
                      "resolved"
                    },
                  ),
                  "winning_fields": Json::array(scan.winning.map(Json::string)),
                  "shadowed_fields": Json::array(
                    scan.shadowed.map(Json::string),
                  ),
                }
                if scan.error is Some(error) {
                  fields["diagnostic"] = error.to_json()
                }
                Json::object(fields)
              }),
            ),
          })
        }),
      ),
    }),
  )
}