/**
 * # engine/orgfacts — the book ledger + the OrgFacts backing (RUNTIME §2, §5)
 *
 * The engine's realization of the {@link OrgFacts} seam from `src/series`: the path-scoped
 * **org facts** that exist only in an acting session (`pnl`, `fills.avg_px`, `fills.count`,
 * `plan(x).state`, `plan(x).rungs`, `plan(x).fills`). `src/series` resolves market facts from
 * canonical state and delegates *org* facts to this backing; the trigger evaluator therefore
 * reads a plan-state chain (`plan(chase).state eq fired`) or a book P&L guard
 * (`pnl lt -200`) through here.
 *
 * The single mutable substrate is the {@link BookLedger}: the engine folds every observed
 * `ORDER fill` into it and stamps each plan's lifecycle state onto it, and {@link EngineOrgFacts}
 * is a **pure read view** over that ledger (it never mutates, never throws — an unresolvable
 * path reads {@link UNKNOWN}, RUNTIME §8). Because the evaluator's provider closes over the
 * same ledger instance, an org fact is always current as of the event being swept.
 *
 * ## P&L is mark-to-intrinsic (the honest daily fact)
 * `pnl` = realized cash-flow (`+` on sells, `−` on buys, per-contract × multiplier) **plus**
 * the residual open position marked at its **intrinsic** at the current spot — the same
 * `intrinsic(spot, strike, right)` the fill engine settles against (RUNTIME §6). Before any
 * spot is known the open mark contributes `0`; with no fills at all `pnl` is `0` (a flat book,
 * not UNKNOWN — a resolved fact).
 */

import type { PathSegment } from "../lang/index.ts";
import type { Right } from "../bus/index.ts";
import { intrinsic } from "../fair/index.ts";
import { UNKNOWN, isUnknown, pathKey, type Resolved, type Unknown, type OrgFacts } from "../series/index.ts";
import { orgPathUnresolvableReason } from "./disarm.ts";

/** One observed fill folded into the ledger — a single leg that actually traded.
 * `strike`/`right` are BOTH present for an option leg and BOTH absent for a spot/equity leg
 * (ADR-0017 — a spot instrument has neither, and a fictional strike is never written). */
export interface LedgerFill {
  readonly plan?: string;
  readonly ref: string;
  readonly instrument: string;
  readonly side: "buy" | "sell";
  readonly qty: number;
  readonly px: number;
  readonly strike?: number;
  readonly right?: Right;
}

/** The per-instrument contract multiplier (dollars per point). Injected so the ledger scales
 * intrinsic marks and cash flows to dollars exactly as the fill engine does. */
export type MultiplierOf = (instrument: string) => number;

/** A `(instrument, strike, right)` position key. A spot fill (no strike/right, ADR-0017) keys
 * on the instrument alone — `<instrument>|spot` — so spot lots net against each other and never
 * against an option leg. */
function legKey(instrument: string, strike: number | undefined, right: Right | undefined): string {
  return strike === undefined || right === undefined ? `${instrument}|spot` : `${instrument}|${strike}|${right}`;
}

/**
 * The single mutable org substrate. The engine folds fills and stamps plan states; the read
 * view ({@link EngineOrgFacts}) and the engine both hold the same instance so facts are always
 * current. Deterministic: fills are appended in observation order and every derived number is a
 * pure function of that ordered log + the current spot.
 */
export class BookLedger {
  readonly #fills: LedgerFill[] = [];
  readonly #planState = new Map<string, string>();
  readonly #planRungs = new Map<string, number>();
  readonly #planFills = new Map<string, number>();
  readonly #spot = new Map<string, number>();
  /** Child-scope org aggregates: `childName → (factName → value)`. A parent Pod folds each child's
   * own published aggregate (`drawdown`, `pnl`, …) here so a PM wake can quantify across the pod
   * (`children(any).drawdown`, RUNTIME §5 / CONTEXT: Pod). */
  readonly #childFacts = new Map<string, Map<string, number>>();
  readonly #multiplierOf: MultiplierOf;

  constructor(multiplierOf: MultiplierOf) {
    this.#multiplierOf = multiplierOf;
  }

  /** Fold one observed fill. Increments the owning plan's fill count. */
  recordFill(fill: LedgerFill): void {
    this.#fills.push(fill);
    if (fill.plan !== undefined) {
      this.#planFills.set(fill.plan, (this.#planFills.get(fill.plan) ?? 0) + 1);
    }
  }

  /** Stamp a plan's current lifecycle state (read by `plan(x).state`). */
  setPlanState(plan: string, state: string): void {
    this.#planState.set(plan, state);
  }

  /** Record that a plan placed another reload rung (read by `plan(x).rungs`). */
  setPlanRungs(plan: string, rungs: number): void {
    this.#planRungs.set(plan, rungs);
  }

  /** Update the last observed spot for an instrument (drives the open mark). */
  setSpot(instrument: string, px: number): void {
    this.#spot.set(instrument, px);
  }

  /** The last observed spot for an instrument, or `undefined`. */
  spotOf(instrument: string): number | undefined {
    return this.#spot.get(instrument);
  }

  /** Book realized cash-flow (`+` sell, `−` buy) + residual open position marked at intrinsic,
   * per-contract × multiplier (see file header). A flat/empty book is `0`, never UNKNOWN. */
  pnl(): number {
    let cash = 0;
    const netQty = new Map<string, { instrument: string; strike?: number; right?: Right; qty: number }>();
    for (const f of this.#fills) {
      const mult = this.#multiplierOf(f.instrument);
      cash += (f.side === "sell" ? 1 : -1) * f.qty * f.px * mult;
      const k = legKey(f.instrument, f.strike, f.right);
      const cur =
        netQty.get(k) ??
        ({
          instrument: f.instrument,
          ...(f.strike !== undefined ? { strike: f.strike } : {}),
          ...(f.right !== undefined ? { right: f.right } : {}),
          qty: 0,
        } as { instrument: string; strike?: number; right?: Right; qty: number });
      cur.qty += (f.side === "buy" ? 1 : -1) * f.qty;
      netQty.set(k, cur);
    }
    let openMark = 0;
    for (const pos of netQty.values()) {
      if (pos.qty === 0) continue;
      const spot = this.#spot.get(pos.instrument);
      if (spot === undefined) continue; // no spot yet ⇒ contributes 0 (honest, not a guess)
      // Option position: marked at intrinsic (the honest daily fact). Spot position (ADR-0017):
      // marked at the spot itself — mark-to-market, no strike to take an intrinsic of.
      const mark = pos.strike === undefined || pos.right === undefined ? spot : intrinsic(spot, pos.strike, pos.right);
      openMark += pos.qty * mark * this.#multiplierOf(pos.instrument);
    }
    return cash + openMark;
  }

  /** Quantity-weighted average fill price across every observed fill, or UNKNOWN with no fills. */
  avgFillPx(): number | typeof UNKNOWN {
    let q = 0;
    let n = 0;
    for (const f of this.#fills) {
      q += f.qty;
      n += f.qty * f.px;
    }
    return q === 0 ? UNKNOWN : n / q;
  }

  /** Total number of observed fills (lots). */
  fillCount(): number {
    return this.#fills.length;
  }

  /** Publish (upsert) a child scope's org aggregate — the write side of `children(any).<fact>`.
   * The parent Pod node receives each child's own aggregate and folds it here; deterministic, a
   * pure upsert keyed by `(child, fact)`. */
  setChildFact(child: string, fact: string, value: number): void {
    let facts = this.#childFacts.get(child);
    if (facts === undefined) {
      facts = new Map();
      this.#childFacts.set(child, facts);
    }
    facts.set(fact, value);
  }

  /** A **monotone display summary** of a child-scoped fact across every child that has published it:
   * `any` reads the worst (**max**), `all` reads the best (**min**). UNKNOWN when no child has
   * published the fact. This is a comparator-blind scalar and is therefore used ONLY for display /
   * inspection panes — a *guard* comparison (`children(any|all).<fact> OP …`) must NOT fold to this
   * scalar, because the ∃/∀ meaning depends on the comparator direction (a `<` guard inverts which
   * child is the deciding one). The trigger evaluator resolves guards through {@link childValues}
   * and folds the comparator per member instead. Order-independent (max/min commute) ⇒ deterministic. */
  childFact(fact: string, quant: "any" | "all"): number | Unknown {
    let acc: number | undefined;
    for (const facts of this.#childFacts.values()) {
      const v = facts.get(fact);
      if (v === undefined) continue;
      acc = acc === undefined ? v : quant === "any" ? Math.max(acc, v) : Math.min(acc, v);
    }
    return acc === undefined ? UNKNOWN : acc;
  }

  /** The per-member value vector for a child-scoped fact — every child that has published `fact`,
   * in deterministic (child-name-sorted) order. The comparator-honest input to the ∃/∀ fold: the
   * trigger applies the authored comparator to each member, then combines by quantifier (`any` = ∃,
   * `all` = ∀), so a `<`/`<=` guard can never invert (the fail-open {@link childFact} would hide).
   * Empty when no child has published the fact — the caller reads that as a genuinely unresolvable
   * quantified path and de-arms with a logged reason (RUNTIME §8), never a silent pass. */
  childValues(fact: string): readonly number[] {
    const out: { child: string; v: number }[] = [];
    for (const [child, facts] of this.#childFacts.entries()) {
      const v = facts.get(fact);
      if (v !== undefined) out.push({ child, v });
    }
    out.sort((a, b) => (a.child < b.child ? -1 : a.child > b.child ? 1 : 0));
    return out.map((e) => e.v);
  }

  planState(plan: string): string | undefined {
    return this.#planState.get(plan);
  }
  planRungs(plan: string): number {
    return this.#planRungs.get(plan) ?? 0;
  }
  planFills(plan: string): number {
    return this.#planFills.get(plan) ?? 0;
  }

  /** A deterministic, serializable snapshot for the determinism certification (RUNTIME §7). */
  dump(): {
    fills: readonly LedgerFill[];
    planState: readonly (readonly [string, string])[];
    planRungs: readonly (readonly [string, number])[];
    planFills: readonly (readonly [string, number])[];
    spot: readonly (readonly [string, number])[];
    childFacts: readonly (readonly [string, readonly (readonly [string, number])[]])[];
  } {
    const sortEntries = <V>(m: Map<string, V>): (readonly [string, V])[] =>
      [...m.entries()].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
    return {
      fills: [...this.#fills],
      planState: sortEntries(this.#planState),
      planRungs: sortEntries(this.#planRungs),
      planFills: sortEntries(this.#planFills),
      spot: sortEntries(this.#spot),
      childFacts: [...this.#childFacts.entries()]
        .map(([child, facts]) => [child, sortEntries(facts)] as const)
        .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)),
    };
  }
}

/**
 * The pure read view over a {@link BookLedger} that satisfies the `src/series` {@link OrgFacts}
 * seam. Resolves the v1 org vocabulary and reads UNKNOWN for anything else (fail-closed,
 * absent-with-reason — the referencing statement de-arms, it never crashes, RUNTIME §8).
 */
export class EngineOrgFacts implements OrgFacts {
  readonly #ledger: BookLedger;
  /** Injected de-arm sink (the engine owns the mutable state; this read view stays pure). Invoked
   * with a path-naming reason ONLY when a path is **structurally unresolvable** as an org fact —
   * an unknown vocabulary, not a recognized-but-not-yet-warm fact (`plan(x).state` before its
   * stamp, `fills.avg_px` before a fill). Lets the engine turn a SILENT UNKNOWN into a de-arm with
   * a logged reason (RUNTIME §8 / CONTEXT: "de-arms cleanly with a logged reason"). */
  readonly #onUnresolved: ((reason: string) => void) | undefined;

  constructor(ledger: BookLedger, onUnresolved?: (reason: string) => void) {
    this.#ledger = ledger;
    this.#onUnresolved = onUnresolved;
  }

  resolve(segments: readonly PathSegment[]): Resolved {
    const head = segments[0];
    if (head === undefined) return UNKNOWN;

    // plan(x).<field> — path-scoped plan facts (chaining + inventory triggers).
    if (head.name === "plan" && head.selector?.kind === "sel-name") {
      const plan = head.selector.name;
      const field = segments[1]?.name;
      switch (field) {
        case "state": {
          const s = this.#ledger.planState(plan);
          return s ?? UNKNOWN; // recognized path, not-yet-stamped ⇒ transient UNKNOWN (not a de-arm)
        }
        case "rungs":
          return this.#ledger.planRungs(plan);
        case "fills":
          return this.#ledger.planFills(plan);
        default:
          return this.#unresolvable(segments); // unknown plan field ⇒ fail closed loudly
      }
    }

    // children(any|all).<field> — child-scoped aggregate across the pod's children (RUNTIME §5).
    // Resolves the field across every child that published it, folded by the authored quantifier;
    // no child has it yet ⇒ genuinely unresolvable, de-arm with a logged reason (not a silent 0).
    if (head.name === "children" && head.selector?.kind === "sel-quant") {
      const field = segments[1]?.name;
      if (field === undefined) return this.#unresolvable(segments);
      const v = this.#ledger.childFact(field, head.selector.q);
      return isUnknown(v) ? this.#unresolvable(segments) : v;
    }

    // Book-scoped scalars.
    if (segments.length === 1 && head.selector === undefined) {
      if (head.name === "pnl") return this.#ledger.pnl();
    }
    if (head.name === "fills" && head.selector === undefined) {
      const field = segments[1]?.name;
      if (field === "avg_px") return this.#ledger.avgFillPx(); // recognized; UNKNOWN before a fill
      if (field === "count") return this.#ledger.fillCount();
      return this.#unresolvable(segments); // unknown fills field ⇒ fail closed loudly
    }

    return this.#unresolvable(segments);
  }

  /** Resolve a **quantified** org operand (`children(any|all).<fact>`) to the vector of per-member
   * values so the trigger folds the comparator per member (∃/∀), honoring the comparator direction.
   * UNKNOWN (with the same loud, path-naming de-arm as {@link resolve}) when the path is genuinely
   * unresolvable — an unknown quantified head, a missing field, or a pod where no member has
   * published the fact (fail-closed, never a silent pass — RUNTIME §8). */
  quantify(segments: readonly PathSegment[]): readonly number[] | Unknown {
    const head = segments[0];
    if (head === undefined) return UNKNOWN;
    if (head.name === "children" && head.selector?.kind === "sel-quant") {
      const field = segments[1]?.name;
      if (field === undefined) return this.#unresolvable(segments);
      const vals = this.#ledger.childValues(field);
      return vals.length === 0 ? this.#unresolvable(segments) : vals;
    }
    return this.#unresolvable(segments);
  }

  /** Fail closed on a structurally-unresolvable org path: log a path-naming de-arm reason through
   * the injected sink (if any), then read UNKNOWN. The read stays pure — the engine owns the sink
   * and decides the de-arm; this view only reports the reason (RUNTIME §8). */
  #unresolvable(segments: readonly PathSegment[]): Unknown {
    this.#onUnresolved?.(orgPathUnresolvableReason(pathKey(segments)));
    return UNKNOWN;
  }
}
