///|
/// Push source for workspace diagnostics: runs `moon check --output-json` and
/// publishes the parsed per-file diagnostics through `publish`. Runs are
/// single-flight — document syncs arriving while a check is in flight
/// collapse into one follow-up run.
struct MoonCheckDiagnostics {
  /// Shared with the host's workspace switching; each run captures one value.
  root : Ref[String]
  publish : (@base_common.Uri, String, Array[@language.Diagnostic]) -> Unit
  collect_check : async () -> String?
  running : Ref[Bool]
  dirty : Ref[Bool]
  sync_generation : Ref[Int]
  /// Exact synced model state per workspace URI.
  documents : Map[String, SyncedDocumentStamp]
  /// Last accepted set per workspace URI, including its producing state.
  published : Ref[Map[String, PublishedDiagnosticSet]]
}

///|
priv struct SyncedDocumentStamp {
  revision : String
  normalized_text : String
}

///|
priv struct PublishedDiagnosticSet {
  revision : String
  normalized_text : String
  diagnostics : Array[@language.Diagnostic]
}

///|
/// Creates a diagnostics runner over a shared workspace root.
fn new_native_moon_check_diagnostics(
  root : Ref[String],
  publish : (@base_common.Uri, String, Array[@language.Diagnostic]) -> Unit,
  moon_command? : String = "moon",
) -> MoonCheckDiagnostics {
  new_moon_check_diagnostics(root, publish, () => {
    // `moon check` exits nonzero whenever errors exist, so the exit code is
    // ignored and stdout is parsed unconditionally.
    Some(
      @process.collect_stdout(
        moon_command,
        ["check", "--output-json"],
        cwd=normalize_host_path(root.val),
      ).1.text(),
    ) catch {
      _ => None
    }
  })
}

///|
fn new_moon_check_diagnostics(
  root : Ref[String],
  publish : (@base_common.Uri, String, Array[@language.Diagnostic]) -> Unit,
  collect_check : async () -> String?,
) -> MoonCheckDiagnostics {
  {
    root,
    publish,
    collect_check,
    running: Ref(false),
    dirty: Ref(false),
    sync_generation: Ref(0),
    documents: {},
    published: Ref({}),
  }
}

///|
/// Forgets every synced document and published diagnostic set. The host calls
/// this when the served workspace root changes: the generation bump makes any
/// in-flight `moon check` run discard its results, and no stale-root
/// diagnostics can replay onto same-named files of the new workspace.
fn MoonCheckDiagnostics::reset_impl(self : MoonCheckDiagnostics) -> Unit {
  self.sync_generation.val += 1
  self.documents.clear()
  self.published.val = {}
}

///|
/// Records the exact synced state. A freshly opened document replays only a
/// diagnostic set produced for the same revision and normalized text.
fn MoonCheckDiagnostics::note_document_impl(
  self : MoonCheckDiagnostics,
  document : @workspace.DocumentSnapshot,
) -> Unit {
  let key = document.uri.to_string()
  let stamp : SyncedDocumentStamp = {
    revision: document.revision,
    normalized_text: @model.TextSnapshot(document.text).get_value(),
  }
  self.sync_generation.val += 1
  self.documents.set(key, stamp)
  if self.running.val {
    self.dirty.val = true
  }
  if self.published.val.get(key) is Some(published) &&
    published.revision == stamp.revision &&
    published.normalized_text == stamp.normalized_text {
    self.emit(key, published.revision, published.diagnostics)
  }
}

///|
/// Schedules a `moon check` run on `spawn_task`; a run already in flight
/// absorbs this request and re-runs once after it finishes.
fn MoonCheckDiagnostics::schedule_impl(
  self : MoonCheckDiagnostics,
  spawn_task : (async () -> Unit) -> Unit,
) -> Unit {
  if self.running.val {
    self.dirty.val = true
    return
  }
  self.running.val = true
  spawn_task(() => {
    // The scheduler may be cancelled or the injected/process collector may
    // raise. A later sync must still be able to start a new run.
    defer {
      self.running.val = false
    }
    for ;; {
      self.dirty.val = false
      self.run_once()
      if !self.dirty.val {
        break
      }
    }
  })
}

///|
async fn MoonCheckDiagnostics::run_once(self : MoonCheckDiagnostics) -> Unit {
  // One root per run; a workspace switch bumps the generation, so a run that
  // raced the switch discards its results below.
  let root = normalize_host_path(self.root.val)
  let generation = self.sync_generation.val
  let documents = copy_synced_documents(self.documents)
  let before = capture_native_disk_snapshots(root, documents)
  let raw = (self.collect_check)()
  let after = capture_native_disk_snapshots(root, documents)
  if self.sync_generation.val != generation ||
    !same_native_disk_snapshot_map(before, after) {
    self.dirty.val = true
    return
  }
  guard raw is Some(raw) else { return }
  let next = parse_moon_check_diagnostics(root, raw)
  let accepted = copy_published_diagnostic_sets(self.published.val)
  for key, document in documents {
    // A stable disk/model mismatch is not a clear for the last accepted model.
    // Preserve that key and continue so historical entries cannot suppress
    // diagnostics for compatible active documents.
    if !native_disk_matches_document(before, key, document) {
      continue
    }
    let diagnostics = next.get(key).unwrap_or([])
    if !diagnostics.is_empty() || self.published.val.contains(key) {
      self.emit(key, document.revision, diagnostics)
    }
    if !diagnostics.is_empty() {
      accepted.set(key, {
        revision: document.revision,
        normalized_text: document.normalized_text,
        diagnostics,
      })
    } else {
      accepted.remove(key)
    }
  }
  self.published.val = accepted
}

///|
fn MoonCheckDiagnostics::emit(
  self : MoonCheckDiagnostics,
  uri_key : String,
  revision : String,
  diagnostics : Array[@language.Diagnostic],
) -> Unit {
  let uri = Some(@base_common.Uri::parse(uri_key)) catch { _ => None }
  if uri is Some(uri) {
    (self.publish)(uri, revision, diagnostics)
  }
}

///|
fn copy_synced_documents(
  documents : Map[String, SyncedDocumentStamp],
) -> Map[String, SyncedDocumentStamp] {
  let copy : Map[String, SyncedDocumentStamp] = Map([])
  for key, document in documents {
    copy.set(key, document)
  }
  copy
}

///|
fn copy_published_diagnostic_sets(
  published : Map[String, PublishedDiagnosticSet],
) -> Map[String, PublishedDiagnosticSet] {
  let copy : Map[String, PublishedDiagnosticSet] = Map([])
  for key, diagnostic_set in published {
    copy.set(key, diagnostic_set)
  }
  copy
}

///|
async fn capture_native_disk_snapshots(
  root : String,
  documents : Map[String, SyncedDocumentStamp],
) -> Map[String, NativeDiskSnapshot] {
  let snapshots : Map[String, NativeDiskSnapshot] = Map([])
  for key, _document in documents {
    let uri = Some(@base_common.Uri::parse(key)) catch { _ => None }
    if uri is Some(uri) {
      match @server.resolve_remote_document_uri(uri) {
        ParsedRemotePath(path) =>
          if read_native_disk_snapshot(root, path) is Some(snapshot) {
            snapshots.set(key, snapshot)
          }
        RemotePathError(_) => ()
      }
    }
  }
  snapshots
}

///|
fn same_native_disk_snapshot_map(
  left : Map[String, NativeDiskSnapshot],
  right : Map[String, NativeDiskSnapshot],
) -> Bool {
  if left.length() != right.length() {
    return false
  }
  for key, snapshot in left {
    guard right.get(key) is Some(other) && other == snapshot else {
      return false
    }
  }
  true
}

///|
fn native_disk_matches_document(
  snapshots : Map[String, NativeDiskSnapshot],
  key : String,
  document : SyncedDocumentStamp,
) -> Bool {
  snapshots.get(key) is Some(disk) &&
  disk.signature == document.revision &&
  disk.normalized_text == document.normalized_text
}

///|
/// Parses `moon check --output-json` output — one JSON diagnostic per line —
/// into diagnostics grouped by remote workspace URI. Diagnostic paths outside
/// `root` and non-diagnostic lines are dropped. `loc` is 1-based
/// `line:col-line:col` with an exclusive end column, matching the viewer's
/// range convention directly.
fn parse_moon_check_diagnostics_impl(
  root : String,
  raw : String,
) -> Map[String, Array[@language.Diagnostic]] {
  let prefix = "\{normalize_host_path(root)}/"
  let grouped : Map[String, Array[@language.Diagnostic]] = Map([])
  for line in raw.split("\n") {
    let trimmed = line.trim(char_set="\r\n ").to_owned()
    if trimmed == "" {
      continue
    }
    match (Some(@json.parse(trimmed)) catch { _ => None }) {
      Some(
        {
          "$message_type": "diagnostic",
          "level": String(level),
          "path": String(path),
          "loc": String(loc),
          "message": String(message),
          ..
        }
      ) => {
        let normalized = normalize_host_path(path)
        if !normalized.has_prefix(prefix) {
          continue
        }
        let relative = normalized[prefix.length():].to_owned()
        match (parse_moon_loc(loc), @server.remote_document_uri(relative)) {
          (Some(range), Some(uri)) => {
            let diagnostic : @language.Diagnostic = {
              range,
              severity: check_level_severity(level),
              message,
              tags: None,
            }
            grouped.get_or_init(uri.to_string(), () => []).push(diagnostic)
          }
          _ => ()
        }
      }
      _ => ()
    }
  }
  grouped
}

///|
fn check_level_severity(level : String) -> @language.DiagnosticSeverity {
  match level {
    "error" => Error
    "warning" => Warning
    _ => Info
  }
}

///|
/// Parses a `moon check` location: `line:col-line:col`, or a bare `line:col`
/// which degrades to an empty range at that position.
fn parse_moon_loc(loc : String) -> @base_common.Range? {
  match loc.find("-") {
    Some(dash) =>
      match
        (
          parse_check_position(loc[0:dash]),
          parse_check_position(loc[dash + 1:]),
        ) {
        (Some(start), Some(end)) =>
          Some(@base_common.Range::from_positions(start, end))
        _ => None
      }
    None =>
      match parse_check_position(loc[:]) {
        Some(position) =>
          Some(@base_common.Range::from_positions(position, position))
        None => None
      }
  }
}

///|
fn parse_check_position(view : StringView) -> @base_common.Position? {
  match view.find(":") {
    Some(colon) => {
      let parsed = Some(
        (
          @string.parse_int(view[0:colon], base=10),
          @string.parse_int(view[colon + 1:], base=10),
        ),
      ) catch {
        _ => None
      }
      match parsed {
        Some((line, column)) => Some(Position(line, column))
        None => None
      }
    }
    None => None
  }
}