///|
/// Language provider backed by the `moon` command line. Hover shells out to
/// `moon ide hover --output-json` and definitions to `moon ide peek-def
/// --json` against the workspace on disk; diagnostics flow through the `moon
/// check` push path (`MoonCheckDiagnostics`) instead of a pull provider.
/// Remaining semantic features without a machine-readable `moon` query return
/// empty results.
struct MoonWorkspaceLanguageProvider {
  /// Shared with the host's workspace switching, so every request runs
  /// against the currently served root.
  root : Ref[String]
  moon_command : String
}

///|
/// Creates a moon-command language provider over a shared workspace root.
fn new_moon_workspace_language_provider(
  root : Ref[String],
  moon_command? : String = "moon",
) -> MoonWorkspaceLanguageProvider {
  { root, moon_command }
}

///|
/// The root a request runs against, captured once per request so a
/// concurrent switch cannot mix two roots inside one query.
fn MoonWorkspaceLanguageProvider::current_root(
  self : MoonWorkspaceLanguageProvider,
) -> String {
  normalize_host_path(self.root.val)
}

///|
async fn MoonWorkspaceLanguageProvider::provide_hover_with_runner(
  self : MoonWorkspaceLanguageProvider,
  model : @model.TextModel,
  position : @base_common.Position,
  run : async (String, Array[String]) -> (Int, String)?,
) -> @language.Hover? {
  match @server.resolve_remote_document_uri(model.uri) {
    ParsedRemotePath(path) => {
      let root = self.current_root()
      let model_identity = model.identity()
      let model_version = model.version
      let content_version = model.get_version_id()
      let model_uri = model.uri.to_string()
      let model_revision = model.revision
      let model_text = model.snapshot.get_value()
      guard read_native_disk_snapshot(root, path) is Some(before) else {
        return None
      }
      guard before.signature == model_revision &&
        before.normalized_text == model_text else {
        return None
      }
      guard !model.is_disposed() &&
        model.identity() == model_identity &&
        model.version == model_version &&
        model.get_version_id() == content_version &&
        model.uri.to_string() == model_uri &&
        model.revision == model_revision &&
        model.snapshot.get_value() == model_text else {
        return None
      }
      let loc = "\{path.to_string()}:\{position.line_number}:\{position.column}"
      let output = run(root, ["ide", "hover", "--loc", loc, "--output-json"])
      guard read_native_disk_snapshot(root, path) is Some(after) &&
        after == before &&
        !model.is_disposed() &&
        model.identity() == model_identity &&
        model.version == model_version &&
        model.get_version_id() == content_version &&
        model.uri.to_string() == model_uri &&
        model.revision == model_revision &&
        model.snapshot.get_value() == model_text else {
        return None
      }
      match output {
        // A missing hover exits nonzero with a plain-text notice, so only an
        // exit-0 payload is parsed.
        Some((0, raw)) => parse_moon_hover_output(raw, position)
        _ => None
      }
    }
    RemotePathError(_) => None
  }
}

///|
/// Parses `moon ide hover --output-json` output: a 1-based
/// `line:col-line:col` range plus markdown content strings. A malformed or
/// empty payload yields no hover; a missing or malformed range degrades to a
/// one-column range at the requested position.
fn parse_moon_hover_output_impl(
  raw : String,
  position : @base_common.Position,
) -> @language.Hover? {
  guard (Some(@json.parse(raw)) catch { _ => None }) is Some(json) else {
    return None
  }
  let contents : Array[@language.HoverContent] = match json {
    { "contents": Array(items), .. } =>
      [
        for item in items if item is String(text) && text != "" => {
          Markdown(text)
        }
      ]
    _ => []
  }
  if contents.is_empty() {
    return None
  }
  let range = match json {
    { "range": String(loc), .. } if parse_moon_loc(loc) is Some(range) => range
    _ =>
      Range(
        position.line_number,
        position.column,
        position.line_number,
        position.column + 1,
      )
  }
  Some({ range, contents })
}

///|
/// Parses the ordered location array emitted by `moon ide peek-def --json` and
/// `moon ide find-references --json`. Absolute paths must be contained by
/// `root`; safe root-relative paths are also accepted. Invalid entries are
/// ignored independently.
fn parse_moon_definition_output_impl(
  root : String,
  raw : String,
) -> Array[@language.Location] {
  guard (Some(@json.parse(raw)) catch { _ => None }) is Some(Array(items)) else {
    return []
  }
  let locations : Array[@language.Location] = []
  for item in items {
    if item is { "path": String(path), "range": String(raw_range), .. } {
      match
        (
          definition_uri_under_root(root, path),
          parse_moon_definition_range(raw_range),
        ) {
        (Some(uri), Some(range)) => locations.push({ uri, range })
        _ => ()
      }
    }
  }
  locations
}

///|
fn definition_uri_under_root(
  root : String,
  raw_path : String,
) -> @base_common.Uri? {
  let root = normalize_host_path(root)
  let path = normalize_host_path(raw_path)
  let root_prefix = if root == "/" { "/" } else { "\{root}/" }
  let relative = if path.has_prefix(root_prefix) {
    path[root_prefix.length():].to_owned()
  } else if path.has_prefix("/") {
    return None
  } else {
    path
  }
  match @workspace.SourcePath::normalize_root_relative_path(relative) {
    ParsedSourcePath(path) => @server.remote_document_uri(path.to_string())
    SourcePathError(_) => None
  }
}

///|
fn parse_moon_definition_range(raw : String) -> @base_common.Range? {
  guard raw.find("-") is Some(dash) else { return None }
  match
    (
      parse_positive_position(raw[0:dash]),
      parse_positive_position(raw[dash + 1:]),
    ) {
    (Some(start), Some(end)) =>
      if start.is_before_or_equal(end) {
        Some(@base_common.Range::from_positions(start, end))
      } else {
        None
      }
    _ => None
  }
}

///|
fn parse_positive_position(raw : StringView) -> @base_common.Position? {
  guard raw.find(":") is Some(colon) else { return None }
  let parsed = Some(
    (
      @string.parse_int(raw[0:colon], base=10),
      @string.parse_int(raw[colon + 1:], base=10),
    ),
  ) catch {
    _ => None
  }
  match parsed {
    Some((line, column)) =>
      if line >= 1 && column >= 1 {
        Some(Position(line, column))
      } else {
        None
      }
    None => None
  }
}