///|
// Herdr presence: map Agent turn events onto herdr agent states and shell the
// `herdr` CLI so a cetas pane appears in the herdr agent rollup with live
// state. herdr injects HERDR_ENV / HERDR_PANE_ID into managed panes; without
// them the constructor returns None and nothing registers — zero cost when
// not under herdr.
//
// The state vocabulary is fixed by the installed herdr CLI
// (`idle|working|blocked|unknown`): there is NO `done` state, so both turn
// terminals report `idle` ("agent available again"). A rejected report is
// swallowed by design (presence must never fail a turn) — verify reports
// manually by running the CLI from README when debugging.
//
// While a `cetas` source holds the pane's lifecycle authority, herdr skips
// its own process probes, so the entry survives the process exiting. The
// Lifecycle impl releases that authority on Agent shutdown — the documented
// exit side of the custom-integration contract (`pane release-agent`).

///|
priv enum HerdrAgentState {
  Working
  Blocked
  Idle
}

///|
fn HerdrAgentState::cli_value(self : HerdrAgentState) -> String {
  match self {
    Working => "working"
    Blocked => "blocked"
    Idle => "idle"
  }
}

///|
priv struct HerdrConfig {
  pane : String
  bin : String
}

///|
pub struct HerdrReporter {
  pane : String
  bin : String
  spawn : (String, Array[String]) -> Unit
}

///|
#warnings("-unused_value")
fn HerdrReporter::from_config(cfg : HerdrConfig) -> HerdrReporter {
  { pane: cfg.pane, bin: cfg.bin, spawn: herdr_spawn_report, }
}

///|
/// Shared presence gate for the targets with environment access: HERDR_ENV
/// and HERDR_PANE_ID must be present and non-empty; HERDR_BIN_PATH defaults
/// to "herdr" when absent or blank. Pure, so native and js share one
/// detection decision.
#cfg(any(target="native", target="wasm", target="js"))
fn herdr_gate(marker : String?, pane : String?, bin : String?) -> HerdrConfig? {
  match (marker, pane) {
    (Some(marker), Some(pane)) if marker != "" && pane != "" => {
      let bin = match bin {
        Some(bin) if bin != "" => bin
        _ => "herdr"
      }
      Some({ pane, bin, })
    }
    _ => None
  }
}

///|
fn HerdrReporter::emit(self : HerdrReporter, state : HerdrAgentState) -> Unit {
  (self.spawn)(self.bin, [
    "pane",
    "report-agent",
    self.pane,
    "--source",
    "cetas",
    "--agent",
    "cetas",
    "--state",
    state.cli_value(),
  ])
}

///|
/// Bounded single-line report message: herdr renders it next to the agent
/// state in the rollup, so long multi-line prompts collapse to a hint.
fn report_message(text : String) -> String {
  let chars : Array[Char] = []
  let mut truncated = false
  for char in text {
    if chars.length() >= 120 {
      truncated = true
      break
    }
    match char {
      '\n' | '\r' => chars.push(' ')
      other => chars.push(other)
    }
  }
  let line = String::from_array(chars)
  if truncated {
    line + "…"
  } else {
    line
  }
}

///|
fn HerdrReporter::emit_message(
  self : HerdrReporter,
  state : HerdrAgentState,
  message : String,
) -> Unit {
  (self.spawn)(self.bin, [
    "pane",
    "report-agent",
    self.pane,
    "--source",
    "cetas",
    "--agent",
    "cetas",
    "--state",
    state.cli_value(),
    "--message",
    report_message(message),
  ])
}

///|
/// Drop this source's lifecycle authority for the pane. herdr keeps showing
/// the agent after the process exits while an authority is registered, so
/// every exit path that can reach Agent shutdown must fire this. Public for
/// host-side fatal-exit fallbacks that never own an Agent.
pub fn HerdrReporter::release(self : HerdrReporter) -> Unit {
  (self.spawn)(self.bin, [
    "pane",
    "release-agent",
    self.pane,
    "--source",
    "cetas",
    "--agent",
    "cetas",
  ])
}

///|
pub impl @posoco.Observer for HerdrReporter with fn on_event(
  self,
  event : @posoco.TurnEvent,
) -> Unit {
  match event {
    @posoco.TurnEvent::TurnStarted => self.emit(Working)
    // A permission confirm, plan review, or ask_question entered the
    // composed UiPort: the agent is blocked waiting for the user, which is
    // the state herdr's rollup surfaces (and pings) for "needs you".
    @posoco.TurnEvent::UserRequestStarted(prompt~, ..) =>
      self.emit_message(Blocked, prompt)
    // The request left the port; the turn keeps running until a terminal
    // event reports idle.
    @posoco.TurnEvent::UserRequestFinished(..) => self.emit(Working)
    @posoco.TurnEvent::TurnCompleted => self.emit(Idle)
    @posoco.TurnEvent::TurnFailed(_) => self.emit(Idle)
    _ => ()
  }
}

///|
pub extend HerdrReporter with @posoco.Observer::{on_event_at}

///|
/// The spawn seam never raises, so shutdown stays failure-free for the
/// Agent's lifecycle walk.
pub impl @posoco.Lifecycle for HerdrReporter with fn on_shutdown(self) -> Unit {
  self.release()
}

///|
pub extend HerdrReporter with @posoco.Lifecycle::{on_compose, on_start}