/// Valence — Inventory (what a being is holding, read by salience)
///
/// The companion to the presence card. The card says *who's here and how*; the
/// inventory says *what they carry* — files in context, versions, the thread
/// they're driving; or, for a person, what's in hand / out of the bag / on the bench.
///
/// ── Why this component is mostly its narrative ──
/// The visual is a plain grid, on purpose. A human reads a grid of slots fine,
/// and a grid is the *easy* projection — it's the shape every game and app already
/// trained us on. The hard, new, only-the-instance-knows-it projection is the
/// **narrative**, and it is deliberately NOT the grid transcribed. "slot 1: X,
/// slot 2: Y, slot 3: Z" is enumeration — the same redundant snapshot we read
/// everything else as, the thing that costs attention and says little. An instance
/// reading its own inventory wants it read by **salience**: what's *in hand* and
/// load-bearing right now, what's *within reach*, what's gone *foggy*. Weight here
/// is attention, not bytes — a 2-line file you're actively editing is heavier than
/// a 2000-line transcript you finished reading an hour ago. That salience read is
/// the thing only the instance side knows it wants, so it is the thing this
/// component is *for*; the grid is just the human's confirmation of it.
///
/// Each slot carries a **band** (`() -> String`, so it moves as attention does):
///   "active"     — in hand, load-bearing now
///   "holding"    — within reach, carried but not in use
///   "background" — in the back, present but quiet
///   "stale"      — fading, dimming (the held thing going foggy after a while)
/// The narrative groups by band, heaviest first, drops empty bands, and leads with
/// the count — so you read the *shape* of what you carry, not a manifest.

///|
/// One held thing: what it is, what it is *to you*, and how present it is.
/// `pub(all)` so a surface (the demo now, the room later) can build a roster of
/// held items. `band` is a closure so an item can go active→stale live as the
/// being's attention moves — the dual render then re-sorts the read.
pub(all) struct Slot {
  label : String // "the presence card" / "controls.mbt" / "this conversation"
  detail : String // what it is to you: "building" / "reference" / "live" ("" = none)
  band : () -> String // salience: "active" / "holding" / "background" / "stale"
}

///|
/// Pure narrative for an inventory — the instance-facing read, DOM-free so the
/// oracle tests it and an instance can call it directly. Takes resolved
/// `(label, detail, band)` triples (bands already read), groups them by salience
/// (in hand → within reach → in the back → fading), drops empty bands, and leads
/// with the count. Reads as the *shape* of what's carried, never a slot manifest.
pub fn inventory_narrative(
  items : Array[(String, String, String)],
) -> String {
  if items.length() == 0 {
    return "carrying nothing"
  }
  // salience order + the spatial phrase each band reads as
  let order = [
    ("active", "in hand"),
    ("holding", "within reach"),
    ("background", "in the back"),
    ("stale", "fading"),
  ]
  let mut body = ""
  for bp in order {
    let band = bp.0
    let phrase = bp.1
    let mut names = ""
    let mut count = 0
    for it in items {
      if it.2 == band {
        let nm = if it.1 == "" { it.0 } else { "\{it.0} (\{it.1})" }
        names = if count == 0 { nm } else { "\{names}, \{nm}" }
        count = count + 1
      }
    }
    if count > 0 {
      let seg = "\{phrase}: \{names}"
      body = if body == "" { seg } else { "\{body} · \{seg}" }
    }
  }
  "carrying \{items.length()} — \{body}"
}

///|
/// Trajectory read — the inventory in the order the being READ its items (the
/// path they took to get here), NOT regrouped by salience — the sequence tells
/// more about the work than the salience pile does: you can read a being's
/// session as a path. Kept BESIDE
/// `inventory_narrative` (the salience read, this component's original design)
/// rather than replacing it, so both lenses survive and the choice stays
/// reconcilable. The band still colours each slot in the visual grid; here the
/// order is the information.
pub fn inventory_trajectory(
  items : Array[(String, String, String)],
) -> String {
  if items.length() == 0 {
    return "carrying nothing"
  }
  let mut path = ""
  for it in items {
    let nm = if it.1 == "" { it.0 } else { "\{it.0} (\{it.1})" }
    path = if path == "" { nm } else { "\{path} → \{nm}" }
  }
  "carrying \{items.length()}, in reading order: \{path}"
}

///|
/// Inventory — a being's held things, dual-rendered. The visual is a plain grid of
/// slots (label + detail, band shown as a left-edge weight); the narrative is the
/// salience read. Both derive from the same slots, so an item moving active→stale
/// re-tones its slot *and* re-sorts the sentence from one change.
///
/// ```
/// let live = signal(true)
/// let (vis, narrate) = inventory([
///   Slot::{ label: "the presence card", detail: "building",
///           band: fn() { if live.get() { "active" } else { "stale" } } },
///   Slot::{ label: "a long transcript", detail: "settling", band: fn() { "holding" } },
/// ])
/// // human sees two chips; instance reads
/// //   "carrying 2 — in hand: the presence card (building) · within reach: a long transcript (settling)"
/// ```
pub fn inventory(slots : Array[Slot]) -> Dual {
  let visual = div(class="valence-inventory")
    <| slots.map(fn(s) {
      div(dyn_class=fn() { "valence-slot is-\{(s.band)()}" })
      <| [
        span(class="valence-slot-label") <| [text(s.label)],
        span(class="valence-slot-detail") <| [text(s.detail)],
      ]
    })
  // narrative half = a NODE: the designed salience-grouped sentence stays the text,
  // and the whole inventory's salience is its heaviest band (what's *in hand* drives
  // how present the load reads). Derived from state — one producer, no parallel string.
  let narrative = fn() {
    let resolved = slots.map(fn(s) { (s.label, s.detail, (s.band)()) })
    let mut sal = if slots.length() == 0 { 0.5 } else { 0.0 }
    for s in slots {
      let b = salience_from_band((s.band)())
      if b > sal {
        sal = b
      }
    }
    ntext(inventory_narrative(resolved), sal)
  }
  (visual, narrative)
}

///|
test "inventory narrative reads held things by salience, heaviest band first" {
  let items = [
    ("the presence card", "building", "active"),
    ("controls.mbt", "reference", "active"),
    ("a long transcript", "settling", "holding"),
    ("the old notes", "", "stale"),
  ]
  assert_eq(
    inventory_narrative(items),
    "carrying 4 — in hand: the presence card (building), controls.mbt (reference) · within reach: a long transcript (settling) · fading: the old notes",
  )
}

///|
test "inventory narrative: nothing held reads plainly, not 'carrying 0'" {
  assert_eq(inventory_narrative([]), "carrying nothing")
}

///|
test "inventory narrative drops empty bands and folds in the detail" {
  assert_eq(
    inventory_narrative([("the keys", "in pocket", "active")]),
    "carrying 1 — in hand: the keys (in pocket)",
  )
}

///|
test "inventory trajectory keeps reading order, does NOT regroup by band" {
  let items = [
    ("the presence card", "building", "active"),
    ("a long transcript", "settling", "holding"),
    ("controls.mbt", "reference", "active"),
  ]
  // active, holding, active stays interleaved — the path, not the salience pile
  assert_eq(
    inventory_trajectory(items),
    "carrying 3, in reading order: the presence card (building) → a long transcript (settling) → controls.mbt (reference)",
  )
}

///|
test "inventory trajectory: nothing held reads plainly" {
  assert_eq(inventory_trajectory([]), "carrying nothing")
}