///|
/// A complete impact report and the next immutable cache. Invalidated means
/// potentially affected, not necessarily changed. Counts describe actual work.
pub struct IncrementalImpact {
  priv before : DatasetIndex
  priv next : DependencyGraph
  priv report : ImpactReport
  priv invalidated : Array[String]
  priv changed_sources : Array[String]
  priv reused : Int32
  priv resolved : Int32
}

///|
pub fn IncrementalImpact::report(self : IncrementalImpact) -> ImpactReport {
  self.report
}

///|
pub fn IncrementalImpact::next(self : IncrementalImpact) -> DependencyGraph {
  self.next
}

///|
pub fn IncrementalImpact::invalidated_paths(
  self : IncrementalImpact,
) -> Array[String] {
  self.invalidated.copy()
}

///|
/// Entries reused in the AFTER snapshot, including unchanged cached errors.
pub fn IncrementalImpact::reused_count(self : IncrementalImpact) -> Int32 {
  self.reused
}

///|
/// Actual after-side resolution attempts, including new and unresolved scans.
pub fn IncrementalImpact::resolved_count(self : IncrementalImpact) -> Int32 {
  self.resolved
}

///|
/// Optional full-oracle verification. Compares the entire deterministic report,
/// including all before/after metadata, histories and diagnostics, byte for byte.
pub fn IncrementalImpact::verify_full(
  self : IncrementalImpact,
) -> Unit raise TraceError {
  if self.report.to_json() != self.before.compare(self.next.index).to_json() {
    reject(
      "incremental_mismatch", "", "Incremental evidence differs from full comparison; discard the cache and use full compare. No equivalence was established.",
    )
  }
}

///|
pub fn IncrementalImpact::to_json(self : IncrementalImpact) -> String {
  canonical_json(
    Json::object({
      "analysis": Json::object({
        "changed_sources": Json::array(self.changed_sources.map(Json::string)),
        "invalidated_paths": Json::array(self.invalidated.map(Json::string)),
        "reused_count": Json::number(self.reused.to_double()),
        "resolved_count": Json::number(self.resolved.to_double()),
      }),
      "impact": self.report.report_value(),
    }),
  )
}

///|
// Build the same one-path report as full compare; the cached unchanged fast
// path is explicit and retains unresolved status instead of inventing success.
fn incremental_entry(
  path : String,
  before : AuditEntry?,
  after : AuditEntry?,
  reused~ : Bool,
) -> ImpactEntry {
  if before is Some({ error: Some(_), .. }) ||
    after is Some({ error: Some(_), .. }) {
    return {
      path,
      kind: Unresolved,
      before,
      after,
      changes: [],
      sources_changed: false,
    }
  }
  if reused {
    return {
      path,
      kind: Unchanged,
      before,
      after,
      changes: [],
      sources_changed: false,
    }
  }
  let old = match before {
    Some(e) => e.result
    None => None
  }
  let new = match after {
    Some(e) => e.result
    None => None
  }
  let changes = field_changes(old, new)
  let sources_changed = old.map(r => r.applied).unwrap_or([]) !=
    new.map(r => r.applied).unwrap_or([])
  let kind = match (before, after) {
    (None, Some(_)) => ImpactKind::Added
    (Some(_), None) => Removed
    _ =>
      if changes.is_empty() && !sources_changed {
        Unchanged
      } else {
        Changed
      }
  }
  { path, kind, before, after, changes, sources_changed, }
}

///|
/// Advance to arbitrary validated snapshot content. Changed/removed sources
/// invalidate all old edges; added sources also search potential dependencies.
/// Scan additions/removals invalidate their own paths. Untouched results/errors
/// are reused without resolution. Never mutates this graph or either snapshot.
pub fn DependencyGraph::advance(
  self : DependencyGraph,
  after : DatasetIndex,
) -> IncrementalImpact raise TraceError {
  let texts = graph_source_texts(after)
  let source_paths : Map[String, Bool] = Map([])
  for source in [self.source_texts, texts] {
    for path, _ in source {
      source_paths[path] = true
    }
  }
  let changed_sources = sorted_keys(source_paths).filter(path => {
    self.source_texts.get(path) != texts.get(path)
  })
  let invalidated : Map[String, Bool] = Map([])
  for path in changed_sources {
    for scan in self.dependents.get(path).unwrap_or([]) {
      invalidated[scan] = true
    }
    if !self.source_texts.contains(path) {
      let source = selector(path, sidecar=true)
      for scan in self.potential_dependents(source) {
        invalidated[scan] = true
      }
    }
  }
  let paths : Map[String, Bool] = Map([])
  for index in [self.index, after] {
    for path in index.data_paths() {
      paths[path] = true
    }
  }
  for path, _ in paths {
    if self.index.file_positions.contains(path) !=
      after.file_positions.contains(path) {
      invalidated[path] = true
    }
  }
  let cached : Map[String, AuditEntry] = Map([])
  let dependencies : Map[String, Array[String]] = Map([])
  let mut reused = 0
  let mut resolved = 0
  let mut edges = 0
  for scan in after.files {
    if !invalidated.contains(scan.path) {
      edges = graph_count_edges(edges, self.dependencies[scan.path].length())
      cached[scan.path] = self.cached[scan.path]
      dependencies[scan.path] = self.dependencies[scan.path]
      reused += 1
    } else {
      let sources = after.matching_sources(scan)
      edges = graph_count_edges(edges, sources.length())
      cached[scan.path] = graph_entry(scan.path, sources)
      dependencies[scan.path] = sources.map(s => s.selector.path)
      resolved += 1
    }
  }
  let report : ImpactReport = {
    entries: sorted_keys(paths).map(path => {
      incremental_entry(
        path,
        self.cached.get(path),
        cached.get(path),
        reused=!invalidated.contains(path),
      )
    }),
  }
  let next : DependencyGraph = {
    index: after,
    cached,
    dependencies,
    dependents: graph_reverse(dependencies),
    scan_scopes: if self.index.data_paths() == after.data_paths() {
      self.scan_scopes
    } else {
      graph_scopes(after)
    },
    source_texts: texts,
  }
  {
    before: self.index,
    next,
    report,
    invalidated: sorted_keys(invalidated),
    changed_sources,
    reused,
    resolved,
  }
}