/**
 * # series/provider — resolve a `SeriesRef` to a value (RUNTIME §2)
 *
 * The seam the trigger evaluator (and, later, panes and grade columns) reads through. A
 * {@link SeriesProvider} answers three questions about the *market* and delegates *org*
 * facts to an injected {@link OrgFacts}:
 *
 * - {@link SeriesProvider.resolve} — a `SeriesRef` (from `src/lang`) → a {@link Resolved}
 *   value (`number | string | UNKNOWN`). **Market facts** (`spot`, `vwap`, `hod`, `lod`,
 *   `prior_close`, `or_high`, `or_low`, and the windowed `velocity/move/range`) come from
 *   {@link CanonicalState} + its window engine; everything else is a path-scoped **org
 *   fact** resolved by the injected {@link OrgFacts} (the engine supplies the real one in a
 *   later milestone; a test fake ships here).
 * - {@link SeriesProvider.baseline} — a windowed series' trailing `p*`/`sigma` (the portable
 *   "big", CONTEXT: Window). UNKNOWN unless the series is windowed, tracked, and warm.
 * - {@link SeriesProvider.phase} — the session phase, for `phase open` triggers.
 *
 * Detector-driven predicates (`failed-break`), fill telemetry, and the session calendar are
 * inputs from *other* modules; they are OPTIONAL hooks on the interface, and a provider that
 * does not supply them makes those triggers read UNKNOWN with a logged reason (fail-closed,
 * absent-with-reason — RUNTIME §8). Nothing here ever throws or reads silently false.
 *
 * All time is injected: `resolve` and the detector/calendar hooks take `now` explicitly
 * (RUNTIME §0) — the provider holds no clock.
 */

import type { AtWindow, Baseline, FillEvent, PathSegment, SeriesRef, StructuralEvent, UntilWindow } from "../lang/index.ts";
import type { SessionPhase } from "../bus/index.ts";
import { UNKNOWN, type Resolved, type TriState, type Unknown } from "./types.ts";

/** The injected non-market hooks a canonical provider serves through (RUNTIME §2/§3): the session
 * calendar (`time HH:MM` / `ttl-at`) and own-fill telemetry. Absent hooks read UNKNOWN
 * (fail-closed, absent-with-reason) exactly as the OPTIONAL {@link SeriesProvider} methods
 * specify — the session (which owns the ET calendar and the fill book) supplies the real ones. */
export interface CanonicalProviderHooks {
  /** Minutes-since-ET-midnight for `now` (drives `time HH:MM` windows + absolute `ttl 16:00`). */
  timeOfDayMinutes?(now: number): number | Unknown;
  /** A fill-lifecycle event (`filled`/`rejected`/`cancelled`) as a trigger, from the session book. */
  fillEvent?(ev: FillEvent): TriState;
  /** The `{until,at}` thesis temporal-envelope firing decision (`inner until 15:30` / `inner at 15:30`,
   * kestrel-rtf), given the envelope node, the evaluator-computed tri-state of its `inner`, and `now`.
   * Absent ⇒ UNKNOWN (fail-closed, absent-with-reason) — the concrete firing semantics are specified by
   * a future runtime ADR and wired through this seam. */
  temporalEnvelope?(env: UntilWindow | AtWindow, inner: TriState, now: number): TriState;
}
import type { BaselineStat } from "./windows.ts";
import type { CanonicalState } from "./state.ts";
import { defaultSeriesRegistry, type MarketScalar, type SeriesRegistry } from "./registry.ts";

// ─────────────────────────────────────────────────────────────────────────────
// The seams
// ─────────────────────────────────────────────────────────────────────────────

/** The injected resolver for **org facts** — path-scoped series that exist only in an
 * acting session (`pnl`, `alpha.pnl`, `fills.avg_px`, `plan(chase-urgent).state`,
 * `children(any).drawdown`). The engine supplies the real, book-backed implementation in a
 * later milestone; it must return {@link UNKNOWN} for an unresolvable path (absent book,
 * unknown name) and **never throw** — an unresolvable series de-arms its statement, it does
 * not crash (RUNTIME §8). */
export interface OrgFacts {
  resolve(segments: readonly PathSegment[]): Resolved;
  /** OPTIONAL: resolve a **quantified** org path (`children(any|all).<fact>`) to the *vector* of
   * per-member numeric values, so the trigger can fold the authored comparator per member (∃/∀)
   * instead of collapsing to a comparator-blind scalar. UNKNOWN when the path is genuinely
   * unresolvable (no member has published the fact) — and, like {@link resolve}, this is the point
   * that logs the fail-closed de-arm reason (RUNTIME §8). Absent ⇒ the caller reads UNKNOWN. */
  quantify?(segments: readonly PathSegment[]): readonly number[] | Unknown;
}

/** The market side of series resolution + the optional detector/fill/calendar hooks. */
export interface SeriesProvider {
  /** A `SeriesRef` → its current value at `now`, or UNKNOWN. */
  resolve(ref: SeriesRef, now: number): Resolved;
  /** The trailing baseline (`p*`/`sigma`) of a windowed series, or UNKNOWN if it is not a
   * windowed metric, is untracked, or is not yet warm. */
  baseline(ref: SeriesRef, stat: Baseline): number | Unknown;
  /** The current session phase, or UNKNOWN before the first phase-bearing heartbeat. */
  phase(): SessionPhase | Unknown;
  /** OPTIONAL: a detector-driven structural event (`failed-break of HOD`). Absent ⇒ the
   * trigger reads UNKNOWN. */
  structural?(ev: StructuralEvent, now: number): TriState;
  /** OPTIONAL: a fill-lifecycle event (`filled`, `rejected`). Absent ⇒ UNKNOWN. */
  fillEvent?(ev: FillEvent): TriState;
  /** OPTIONAL: minutes-since-session-midnight for `time HH:MM..HH:MM` triggers. Absent ⇒
   * UNKNOWN. */
  timeOfDayMinutes?(now: number): number | Unknown;
  /** OPTIONAL: the per-member value vector for a **quantified** org operand
   * (`children(any|all).<fact>`), so a comparison folds the authored comparator per member (∃/∀)
   * rather than against a comparator-blind scalar. UNKNOWN when unresolvable. Absent ⇒ UNKNOWN. */
  quantify?(ref: SeriesRef, now: number): readonly number[] | Unknown;
  /** OPTIONAL: the `{until,at}` thesis temporal-envelope firing decision (`inner until 15:30`,
   * kestrel-rtf) — given the envelope node, the evaluator-computed tri-state of its `inner` (whose
   * per-node causal memory the evaluator owns, so the hook cannot re-derive it), and `now`. Absent ⇒
   * the envelope reads UNKNOWN (fail-closed, absent-with-reason), exactly like an absent
   * `structural`/`fillEvent` hook — the capability is DISCLOSED as absent rather than a silent
   * hardcoded never-fire. The concrete firing semantics are specified by a future runtime ADR and
   * wired through this seam. */
  temporalEnvelope?(env: UntilWindow | AtWindow, inner: TriState, now: number): TriState;
}

// ─────────────────────────────────────────────────────────────────────────────
// Classification
// ─────────────────────────────────────────────────────────────────────────────

/** The market resolution binding for a single-segment name, from the registry phonebook — or
 * undefined when the name is not a registered market fact (so it routes to org). This is the
 * one place market-vs-org routing is decided, and it reads through the shared Registry
 * (CONTEXT: Registry) rather than a provider-local copy of the market vocabulary. */
function marketBinding(registry: SeriesRegistry, name: string) {
  const reg = registry.lookup(name);
  return reg?.kind === "market" ? reg.market : undefined;
}

/** True when a ref is a plain single-segment reference with no org selector. */
function isBareSegment(ref: SeriesRef): boolean {
  return ref.segments.length === 1 && ref.segments[0]?.selector === undefined;
}

// ─────────────────────────────────────────────────────────────────────────────
// CanonicalSeriesProvider
// ─────────────────────────────────────────────────────────────────────────────

export class CanonicalSeriesProvider implements SeriesProvider {
  constructor(
    private readonly state: CanonicalState,
    private readonly org: OrgFacts,
    private readonly hooks: CanonicalProviderHooks = {},
    /** The series phonebook the market-vs-org routing dials (CONTEXT: Registry). Defaults to the
     * process-wide platform phonebook; the market side is static, so the default is deterministic. */
    private readonly registry: SeriesRegistry = defaultSeriesRegistry,
  ) {}

  /** The session-calendar hook (`time HH:MM` / `ttl-at`); UNKNOWN when the session injects none. */
  timeOfDayMinutes(now: number): number | Unknown {
    return this.hooks.timeOfDayMinutes ? this.hooks.timeOfDayMinutes(now) : UNKNOWN;
  }

  /** The own-fill telemetry hook; UNKNOWN when the session injects none (absent-with-reason). */
  fillEvent(ev: FillEvent): TriState {
    return this.hooks.fillEvent ? this.hooks.fillEvent(ev) : UNKNOWN;
  }

  /** The `{until,at}` temporal-envelope hook (kestrel-rtf); UNKNOWN when the session injects none
   * (absent-with-reason) — the firing semantics are wired by a future runtime ADR. */
  temporalEnvelope(env: UntilWindow | AtWindow, inner: TriState, now: number): TriState {
    return this.hooks.temporalEnvelope ? this.hooks.temporalEnvelope(env, inner, now) : UNKNOWN;
  }

  resolve(ref: SeriesRef, now: number): Resolved {
    if (ref.segments.length === 0) return UNKNOWN;
    const head = ref.segments[0];
    if (head === undefined) return UNKNOWN;

    if (ref.window !== undefined) {
      // A windowed series is a market metric (windowed org facts are not v1).
      if (!isBareSegment(ref)) return UNKNOWN;
      const binding = marketBinding(this.registry, head.name);
      if (binding?.via !== "window") return UNKNOWN;
      const wv = this.state.windows.value(binding.metric, ref.window.value, ref.window.unit, now);
      if (wv === UNKNOWN) return UNKNOWN;
      return binding.pct ? wv.pct : wv.dollars;
    }

    if (isBareSegment(ref)) {
      const binding = marketBinding(this.registry, head.name);
      if (binding?.via === "scalar") return this.marketScalar(binding.scalar);
    }

    // Not a market fact ⇒ a path-scoped org fact.
    return this.org.resolve(ref.segments);
  }

  /** A quantified org operand folds per member (∃/∀) — delegate to the org backing's vector
   * resolution; absent (or a market provider without org quantification) ⇒ UNKNOWN (fail-closed). */
  quantify(ref: SeriesRef, _now: number): readonly number[] | Unknown {
    return this.org.quantify?.(ref.segments) ?? UNKNOWN;
  }

  baseline(ref: SeriesRef, stat: Baseline): number | Unknown {
    if (ref.window === undefined || !isBareSegment(ref)) return UNKNOWN;
    const head = ref.segments[0];
    if (head === undefined) return UNKNOWN;
    const binding = marketBinding(this.registry, head.name);
    if (binding?.via !== "window") return UNKNOWN;
    const bstat: BaselineStat = stat.stat === "p" ? { kind: "p", n: stat.n } : { kind: "sigma", n: stat.n };
    return this.state.windows.baseline(binding.metric, ref.window.value, ref.window.unit, bstat);
  }

  phase(): SessionPhase | Unknown {
    return this.state.phase;
  }

  private marketScalar(name: MarketScalar): Resolved {
    switch (name) {
      case "spot":
        return this.state.spot;
      case "vwap":
        return this.state.vwap;
      // hod/lod/or_high/or_low resolve to their EXCLUSIVE (pre-current-sample) flavour for
      // trigger evaluation, so `spot crosses above hod` fires on a fresh high (RUNTIME §2 /
      // state.ts header). Panes read the inclusive `state.hod`/`state.lod` directly.
      case "hod":
        return this.state.hodTrigger;
      case "lod":
        return this.state.lodTrigger;
      case "prior_close":
        return this.state.priorClose;
      case "or_high":
        return this.state.orHighTrigger;
      case "or_low":
        return this.state.orLowTrigger;
      default:
        return UNKNOWN;
    }
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// FakeOrgFacts — a deterministic test double for the injected org seam
// ─────────────────────────────────────────────────────────────────────────────

/** A path key for a segment list: `plan(chase).state`, `children(any).drawdown`, `pnl`.
 * The canonical string form used by {@link FakeOrgFacts} and handy for logging. */
export function pathKey(segments: readonly PathSegment[]): string {
  return segments
    .map((s) => {
      if (s.selector === undefined) return s.name;
      const inner = s.selector.kind === "sel-quant" ? s.selector.q : s.selector.name;
      return `${s.name}(${inner})`;
    })
    .join(".");
}

/** A map-backed {@link OrgFacts} for tests (the engine ships the real one). Unknown paths
 * resolve to UNKNOWN — exactly the contract the real seam must honor. */
export class FakeOrgFacts implements OrgFacts {
  constructor(private readonly facts: ReadonlyMap<string, Resolved> = new Map()) {}

  static of(entries: Readonly<Record<string, Resolved>>): FakeOrgFacts {
    return new FakeOrgFacts(new Map(Object.entries(entries)));
  }

  resolve(segments: readonly PathSegment[]): Resolved {
    return this.facts.get(pathKey(segments)) ?? UNKNOWN;
  }
}
