///|
/// `status_op` — the wire protocol shared by every status-bar publisher and
/// the statusbar bridge.
///
/// Extensions publish on topic "status" through the three helpers; the
/// bridge subscribes to the bus and decodes each event with
/// `decode_status_op`. Payloads are Json objects, so the protocol survives
/// transport mapping and third-party publishers. Malformed events decode to
/// `None` and are silently dropped — decoding never raises, so one bad
/// publisher cannot crash the host.
///
/// Ownership of a segment (who may write it) is enforced by the bridge, not
/// here: the `source` of the first register/update for a segment owns it.

///|
/// The bus topic every status-bridge event travels on.
pub const STATUS_TOPIC : String = "status"

///|
/// The closed vocabulary of semantic color roles a register may declare.
/// Roles are semantic markers aligned with host theme vocabulary (e.g. the
/// cetas-js theme role names); raw color values (ANSI/hex) never travel on
/// the protocol, and hosts may ignore any declaration. Returns a fresh
/// array per call (`default_ignore_patterns` style): uppercase globals
/// must be `const`, and `const` rejects arrays.
pub fn status_color_roles() -> Array[String] {
  ["accent", "warning", "error", "success", "info", "muted"]
}

///|
/// Whether `role` belongs to `status_color_roles()`. The protocol carries
/// any declared string; vocabulary enforcement is the consumer's (bridge's)
/// job.
pub fn is_status_color_role(role : String) -> Bool {
  status_color_roles().contains(role)
}

///|
/// One decoded status-bar operation.
pub(all) enum StatusOp {
  Register(
    segment~ : String,
    label~ : String?,
    priority~ : Int,
    value~ : String?,
    color~ : String?
  )
  Update(segment~ : String, value~ : String)
  Unregister(segment~ : String)
} derive(Eq, Debug)

///|
pub extend StatusOp with Eq::{not_equal, equal}

///|
pub extend StatusOp with @moonbitlang/core/debug.Debug::{to_repr}

///|
/// Publish a register op: claim `segment` with display metadata. `priority`
/// orders segments on the bar (ascending, left to right). `label`, `value`
/// and `color` keys are emitted only when present; `priority` is always
/// emitted. `color` is a semantic role from `status_color_roles()`.
pub fn publish_status_register(
  bus : EventBus,
  source~ : String,
  segment~ : String,
  label? : String,
  priority? : Int = 0,
  value? : String,
  color? : String,
) -> Unit {
  let pairs : Array[(String, Json)] = [
    ("op", Json::string("register")),
    ("segment", Json::string(segment)),
    ("priority", Json::number(priority.to_double())),
  ]
  match label {
    Some(l) => pairs.push(("label", Json::string(l)))
    None => ()
  }
  match value {
    Some(v) => pairs.push(("value", Json::string(v)))
    None => ()
  }
  match color {
    Some(c) => pairs.push(("color", Json::string(c)))
    None => ()
  }
  bus.publish({
    source,
    topic: STATUS_TOPIC,
    data: Json::object(Map::from_array(pairs)),
  })
}

///|
/// Publish an update op: set the segment's value. For a segment the bridge
/// has not seen, this acts as an implicit register with default priority.
pub fn publish_status_update(
  bus : EventBus,
  source~ : String,
  segment~ : String,
  value~ : String,
) -> Unit {
  bus.publish({
    source,
    topic: STATUS_TOPIC,
    data: Json::object({
      "op": Json::string("update"),
      "segment": Json::string(segment),
      "value": Json::string(value),
    }),
  })
}

///|
/// Publish an unregister op: release the segment.
pub fn publish_status_unregister(
  bus : EventBus,
  source~ : String,
  segment~ : String,
) -> Unit {
  bus.publish({
    source,
    topic: STATUS_TOPIC,
    data: Json::object({
      "op": Json::string("unregister"),
      "segment": Json::string(segment),
    }),
  })
}

///|
/// Decode a bus event into a `StatusOp`. Returns `None` when the event is
/// not on the status topic, the payload is not an object, `op` is missing
/// or unknown, or a required field is missing or wrongly typed — callers
/// drop the event and move on. `priority` is the one forgiving field:
/// absent or non-numeric decodes to 0. Never raises.
pub fn decode_status_op(event : BusEvent) -> StatusOp? {
  if event.topic != STATUS_TOPIC {
    return None
  }
  match event.data {
    Object(fields) =>
      match fields.get("op") {
        Some(String("register")) => register_from(fields)
        Some(String("update")) => update_from(fields)
        Some(String("unregister")) => unregister_from(fields)
        _ => None
      }
    _ => None
  }
}

///|
/// Strict decode for register payloads: `label`, `value` and `color` are
/// optional but must be strings when present — a wrongly typed one rejects
/// the whole event. `color` is NOT checked against `status_color_roles()`
/// here: a present-but-unknown role still decodes, and vocabulary
/// enforcement is the consumer's (bridge's) job via `is_status_color_role`.
fn register_from(fields : Map[String, Json]) -> StatusOp? {
  let segment = match fields.get("segment") {
    Some(String(s)) => s
    _ => return None
  }
  let priority = match fields.get("priority") {
    Some(Number(n, ..)) => n.to_int()
    _ => 0
  }
  let label = match fields.get("label") {
    Some(String(l)) => Some(l)
    Some(_) => return None
    None => None
  }
  let value = match fields.get("value") {
    Some(String(v)) => Some(v)
    Some(_) => return None
    None => None
  }
  let color = match fields.get("color") {
    Some(String(c)) => Some(c)
    Some(_) => return None
    None => None
  }
  Some(Register(segment~, label~, priority~, value~, color~))
}

///|
fn update_from(fields : Map[String, Json]) -> StatusOp? {
  let segment = match fields.get("segment") {
    Some(String(s)) => s
    _ => return None
  }
  let value = match fields.get("value") {
    Some(String(v)) => v
    _ => return None
  }
  Some(Update(segment~, value~))
}

///|
fn unregister_from(fields : Map[String, Json]) -> StatusOp? {
  let segment = match fields.get("segment") {
    Some(String(s)) => s
    _ => return None
  }
  Some(Unregister(segment~))
}