///|
/// Read one JSONL line from an engine's stdout stream back into an `Event`.
///
/// The inverse of `Event::to_json`, pinned per variant by `parse_test`.
///
/// `None` means "not an event this version reads" — an unknown `event` name, a
/// missing `event` key, or a payload whose required fields are absent or
/// mistyped. A client stays tolerant of a newer engine by skipping such a line
/// rather than failing on it, which matters because the TUI and the desktop
/// host both launch whichever `loci` is on PATH.
///
/// Only fields that a renderer treats as genuinely optional are defaulted:
/// `picked_up_from` and `recommendation` (a cold entry and a session with no
/// advice are ordinary), and the two string lists (empty and absent mean the
/// same to every renderer). Everything else is required, because its absence
/// means the line is not what it claims to be.
pub fn parse_event(line : Json) -> Event? {
  guard line is Object(fields) else { return None }
  guard text(fields, "event") is Some(name) else { return None }
  match name {
    "ready" =>
      match (int(fields, "version"), text(fields, "engine")) {
        (Some(version), Some(engine)) => Some(Ready(version~, engine~))
        _ => None
      }
    "entered" =>
      match
        (
          object(fields, "locus").bind(parse_locus_fields),
          object(fields, "trail").bind(parse_trail_fields),
        ) {
        (Some(locus), Some(trail)) => Some(Entered(locus~, trail~))
        _ => None
      }
    "trail_listed" =>
      object(fields, "trail")
      .bind(parse_trail_fields)
      .map(trail => TrailListed(trail~))
    "located" =>
      match fields.get("loci") {
        Some(Array(items)) => {
          let loci : Array[Locus] = []
          for item in items {
            guard item is Object(nested) else { return None }
            guard parse_locus_fields(nested) is Some(locus) else { return None }
            loci.push(locus)
          }
          Some(Located(loci~))
        }
        _ => None
      }
    "residue_filed" =>
      object(fields, "residue")
      .bind(parse_residue_fields)
      .map(residue => ResidueFiled(residue~))
    "sealed" =>
      match (int(fields, "epoch"), text(fields, "root")) {
        (Some(epoch), Some(root)) => Some(Sealed(epoch~, root~))
        _ => None
      }
    "stored" =>
      match (text(fields, "digest"), text(fields, "disposition")) {
        (Some(digest), Some(disposition)) => Some(Stored(digest~, disposition~))
        _ => None
      }
    "diffed" =>
      match
        (
          int(fields, "added"),
          int(fields, "removed"),
          int(fields, "changed"),
          int(fields, "unchanged"),
        ) {
        (Some(added), Some(removed), Some(changed), Some(unchanged)) =>
          Some(Diffed(added~, removed~, changed~, unchanged~))
        _ => None
      }
    "diagnostic" =>
      match
        (
          text(fields, "severity").bind(Severity::parse),
          text(fields, "message"),
        ) {
        (Some(severity), Some(message)) => Some(Diagnostic(severity~, message~))
        _ => None
      }
    "finished" => int(fields, "code").map(code => Finished(code~))
    _ => None
  }
}

///|
/// Read one JSONL line from a surface's stdin stream back into a `Command`.
///
/// `None` carries the same meaning as in `parse_event`: a line this version
/// does not read, to be skipped rather than treated as a failure.
pub fn parse_command(line : Json) -> Command? {
  guard line is Object(fields) else { return None }
  guard text(fields, "command") is Some(name) else { return None }
  match name {
    "enter" => text(fields, "locus").map(locus => Enter(locus~))
    "trail" => text(fields, "locus").map(locus => Trail(locus~))
    "locate" => Some(Locate)
    "sign" =>
      object(fields, "residue")
      .bind(parse_residue_fields)
      .map(residue => Sign(residue~))
    "interrupt" => Some(Interrupt)
    "shutdown" => Some(Shutdown)
    _ => None
  }
}

///|
/// Read a residue payload from its already-destructured object fields.
pub fn parse_residue(value : Json) -> Residue? {
  guard value is Object(fields) else { return None }
  parse_residue_fields(fields)
}

///|
/// Read a locus payload from its already-destructured object fields.
pub fn parse_locus(value : Json) -> Locus? {
  guard value is Object(fields) else { return None }
  parse_locus_fields(fields)
}

///|
/// Read a trail payload from its already-destructured object fields.
pub fn parse_trail(value : Json) -> Trail? {
  guard value is Object(fields) else { return None }
  parse_trail_fields(fields)
}

///|
fn parse_residue_fields(fields : Map[String, Json]) -> Residue? {
  guard text(fields, "locus") is Some(locus) else { return None }
  guard text(fields, "tier") is Some(tier) else { return None }
  guard text(fields, "session_short_id") is Some(session_short_id) else {
    return None
  }
  guard text(fields, "timestamp") is Some(timestamp) else { return None }
  Some({
    locus,
    tier,
    session_short_id,
    timestamp,
    picked_up_from: opt_text(fields, "picked_up_from"),
    what_i_did: strings(fields, "what_i_did"),
    what_i_left_open: strings(fields, "what_i_left_open"),
    recommendation: opt_text(fields, "recommendation"),
  })
}

///|
fn parse_locus_fields(fields : Map[String, Json]) -> Locus? {
  guard text(fields, "name") is Some(name) else { return None }
  guard text(fields, "spirit") is Some(spirit) else { return None }
  guard int(fields, "trail_depth") is Some(trail_depth) else { return None }
  let last_visit = match fields.get("last_visit") {
    Some(Object(nested)) =>
      match parse_residue_fields(nested) {
        Some(residue) => Some(residue)
        // A present-but-malformed residue is a broken line, not an unvisited
        // locus; reporting it as "never entered" would hide the break.
        None => return None
      }
    _ => None
  }
  Some({ name, spirit, tags: strings(fields, "tags"), trail_depth, last_visit, })
}

///|
fn parse_trail_fields(fields : Map[String, Json]) -> Trail? {
  guard text(fields, "locus") is Some(locus) else { return None }
  let sessions : Array[Residue] = []
  match fields.get("sessions") {
    Some(Array(items)) =>
      for item in items {
        guard item is Object(nested) else { return None }
        guard parse_residue_fields(nested) is Some(residue) else { return None }
        sessions.push(residue)
      }
    _ => return None
  }
  Some({ locus, sessions, })
}