/**
 * # src/pm — the Strategy ADT: the TYPED SOURCE the PM item-bank plays are instances of
 *
 * (bd kestrel-markets-n7jx.2 / PM-persona-benchmark design §10-Issue-2). The PM-persona
 * benchmark grades a PM seat's `MarketState → Book` allocation. Until now the "strategies" the
 * PM could pick lived only as loose string ids on the item-bank menus
 * (`tests/bench/pm-item-bank.ts`, `pm3-bank.ts`): `long_straddle`, `call_vertical`, `short_fade`,
 * … — a flat namespace with no type telling you WHICH decisive read makes each one well-formed.
 *
 * This module makes the strategies a real **algebraic data type** with four constructors:
 *
 *     ORB | Straddle | Vertical | NewsShort
 *
 * and — the load-bearing part — parameterises each constructor by **the two edges the whole
 * benchmark measures**, so the decisive read is a *type parameter*, not a runtime string:
 *
 *  - **Edge 1 — EXPRESSIVENESS** (the options family): the `(direction × magnitude/vol)` read.
 *    `Straddle<M>` is parameterised by the {@link Magnitude} pole it expresses (`large` ⇒ own
 *    convexity / LONG straddle; `small` ⇒ harvest premium / SHORT straddle) and is `two-sided`
 *    by construction; `Vertical<D>` is parameterised by the {@link Direction} it expresses
 *    (`up` ⇒ call vertical; `down` ⇒ put vertical) and is `moderate`-magnitude by construction.
 *  - **Edge 2 — INFORMATION** (the small-cap news family): the catalyst-authenticity read.
 *    `NewsShort<C>` is parameterised by the {@link Catalyst} (`promotional` ⇒ fade / short the
 *    unfunded pop; `real` ⇒ ride the funded continuation LONG — the *decisive-field flip*).
 *
 * The parameters are **load-bearing** — they are what makes an ill-typed read a *compile* error,
 * not a runtime surprise:
 *
 *     Straddle<"moderate">   // ✗ type error: M extends "large" | "small"
 *     Vertical<"two-sided">  // ✗ type error: D extends "up" | "down"
 *     lower(straddle(...))   // ✗ type error: a two-sided vol structure is NOT EquityLowerable
 *
 * `{@link toPlay}` maps every constructor onto exactly the item-bank play id it stands for, so the
 * banks' plays are, provably, *instances* of this ADT (a `toPlay` round-trip is asserted in
 * `tests/bench/pm2-spine.test.ts` against `OPTIONS_MENU` / `NEWS_MENU`).
 *
 * `{@link lower}` elaborates the **equity-expressible** constructors (`ORB`, `NewsShort`) down to
 * a real `plan.gbnf` PLAN string that `src/lang`'s `parse()` — the same engine/validator the RL
 * reward-bridge drives (`scripts/rl-poc/reward-bridge.ts`) — accepts. It *reuses* the shipped
 * `scripts/rl-poc/grammar/plan.gbnf` equity subset as its target (extension, never a rewrite); the
 * options structures (`Straddle`, `Vertical`) are NOT `EquityLowerable` — lowering a two-sided /
 * multi-leg vol structure needs the options grammar, which does not yet exist, so the type refuses
 * it rather than emitting a lossy proxy.
 *
 * Instrument hygiene: this module is OSS/public (`src/**` ships). Every example and every default
 * uses a generic index underlying; no strategy repertoire, persona playbook, or private alpha
 * lives here — only the typed spine.
 */

// ─────────────────────────────────────────────────────────────────────────────
// The two LOAD-BEARING EDGES — the decisive reads a PM extracts from a MarketState
// ─────────────────────────────────────────────────────────────────────────────

/** Edge 1a — the directional component of the expressiveness read. */
export type Direction = "up" | "down" | "two-sided";

/** Edge 1b — the magnitude/vol component of the expressiveness read. */
export type Magnitude = "large" | "moderate" | "small";

/** Edge 2 — the information read: is the catalyst funded/real or an unfunded pump? */
export type Catalyst = "real" | "promotional";

/** The one-sided directions — a breakout / vertical / equity order has a side. */
export type OneSided = Extract<Direction, "up" | "down">;

/** The magnitudes a straddle expresses (a straddle is never `moderate` — that is a `Vertical`). */
export type StraddleMag = Extract<Magnitude, "large" | "small">;

/** A budget in whole R, matching `plan.gbnf`'s `digit ::= [1-5]`. */
export type BudgetR = 1 | 2 | 3 | 4 | 5;

// ─────────────────────────────────────────────────────────────────────────────
// The 4-constructor Strategy ADT
// ─────────────────────────────────────────────────────────────────────────────

/** The equity opening-range-breakout scalp — the strategy the shipped `plan.gbnf` directly encodes. */
export interface ORB {
  readonly kind: "ORB";
  /** Breakout side: `up` ⇒ cross ABOVE the range high; `down` ⇒ cross BELOW the range low. */
  readonly direction: OneSided;
  readonly underlying: string;
  readonly orHigh: number;
  readonly orLow: number;
  readonly budgetR: BudgetR;
  /** Scalp take-profit percent, in `plan.gbnf`'s `[0-9] "." [0-9]` band (0.0 .. 9.9). */
  readonly tpPct: number;
}

/**
 * The ATM straddle — the TWO-SIDED vol structure. `M` is load-bearing: `large` ⇒ own convexity
 * (LONG straddle, the `long_straddle` play); `small` ⇒ harvest a pin/crush (SHORT straddle, the
 * `short_straddle` play). There is no directional straddle — `direction` is `"two-sided"` by type.
 */
export interface Straddle<M extends StraddleMag = StraddleMag> {
  readonly kind: "Straddle";
  readonly magnitude: M;
  readonly direction: "two-sided";
  readonly underlying: string;
}

/**
 * The defined-risk vertical — the MODERATE, capped directional structure. `D` is load-bearing:
 * `up` ⇒ bull call spread (`call_vertical`); `down` ⇒ bear put spread (`put_vertical`). A vertical
 * is never `two-sided` (that is a `Straddle`) — `magnitude` is `"moderate"` by type.
 */
export interface Vertical<D extends OneSided = OneSided> {
  readonly kind: "Vertical";
  readonly direction: D;
  readonly magnitude: "moderate";
  readonly underlying: string;
}

/**
 * The small-cap news reaction — the INFORMATION-edge constructor. `C` is load-bearing and encodes
 * the decisive-field flip: `promotional` ⇒ fade the unfunded pop (SHORT, the `short_fade` play);
 * `real` ⇒ ride the funded continuation (LONG, the `momentum_long` play). Same tape, flip `C`,
 * get the opposite well-typed book — this is the demo in {@link file ./book.ts}'s `PM`.
 */
export interface NewsShort<C extends Catalyst = Catalyst> {
  readonly kind: "NewsShort";
  readonly catalyst: C;
  readonly underlying: string;
  readonly orHigh: number;
  readonly orLow: number;
  readonly budgetR: BudgetR;
  readonly tpPct: number;
}

/** The Strategy ADT — the closed sum of the four constructors over both edges. */
export type Strategy =
  | ORB
  | Straddle<"large">
  | Straddle<"small">
  | Vertical<"up">
  | Vertical<"down">
  | NewsShort<"real">
  | NewsShort<"promotional">;

// ─────────────────────────────────────────────────────────────────────────────
// Smart constructors — enforce the edge-read at the VALUE level too (the type does it statically)
// ─────────────────────────────────────────────────────────────────────────────

/** Build an ORB breakout scalp. */
export function orb(args: {
  direction: OneSided;
  underlying: string;
  orHigh: number;
  orLow: number;
  budgetR: BudgetR;
  tpPct: number;
}): ORB {
  return { kind: "ORB", ...args };
}

/** Build a straddle from the magnitude edge (`large` ⇒ long, `small` ⇒ short). */
export function straddle<M extends StraddleMag>(magnitude: M, underlying: string): Straddle<M> {
  return { kind: "Straddle", magnitude, direction: "two-sided", underlying };
}

/** Build a vertical from the direction edge (`up` ⇒ call, `down` ⇒ put). */
export function vertical<D extends OneSided>(direction: D, underlying: string): Vertical<D> {
  return { kind: "Vertical", direction, magnitude: "moderate", underlying };
}

/** Build a news reaction from the catalyst edge (`promotional` ⇒ fade/short, `real` ⇒ momentum/long). */
export function newsShort<C extends Catalyst>(
  catalyst: C,
  args: { underlying: string; orHigh: number; orLow: number; budgetR: BudgetR; tpPct: number },
): NewsShort<C> {
  return { kind: "NewsShort", catalyst, ...args };
}

// ─────────────────────────────────────────────────────────────────────────────
// toPlay — the ADT → item-bank-play projection (proves the banks' plays are ADT instances)
// ─────────────────────────────────────────────────────────────────────────────

/** The bank play ids the ADT constructors stand for (superset of `OPTIONS_MENU`/`NEWS_MENU` actionables). */
export type Play =
  | "orb_breakout"
  | "long_straddle"
  | "short_straddle"
  | "call_vertical"
  | "put_vertical"
  | "short_fade"
  | "momentum_long";

/** Assert-never helper for exhaustive matches (mirrors `src/lang`'s `assertNever`). */
function unreachable(x: never): never {
  throw new Error(`unreachable Strategy variant: ${JSON.stringify(x)}`);
}

/**
 * The item-bank play a strategy stands for — total and exhaustive over the ADT. This is the
 * witness that `OPTIONS_MENU` / `NEWS_MENU`'s actionable plays are exactly the images of the four
 * typed constructors (asserted in the spine test).
 */
export function toPlay(s: Strategy): Play {
  switch (s.kind) {
    case "ORB":
      return "orb_breakout";
    case "Straddle":
      return s.magnitude === "large" ? "long_straddle" : "short_straddle";
    case "Vertical":
      return s.direction === "up" ? "call_vertical" : "put_vertical";
    case "NewsShort":
      return s.catalyst === "real" ? "momentum_long" : "short_fade";
    default:
      return unreachable(s);
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// lower — Strategy → a real plan.gbnf PLAN string (equity-expressible constructors only)
// ─────────────────────────────────────────────────────────────────────────────

/**
 * The constructors the shipped `plan.gbnf` equity subset can express: the equity breakout and the
 * equity long/short news reaction. `Straddle` and `Vertical` are DELIBERATELY excluded — a
 * two-sided or multi-leg options structure has no single-order equity elaboration in the shipped
 * grammar, and inventing one would be a lossy proxy. The type refuses `lower(straddle(...))`.
 */
export type EquityLowerable = ORB | NewsShort<"real"> | NewsShort<"promotional">;

/** Runtime witness for {@link EquityLowerable} (for dispatch sites that hold a wide `Strategy`). */
export function isEquityLowerable(s: Strategy): s is EquityLowerable {
  return s.kind === "ORB" || s.kind === "NewsShort";
}

/** Raised when a value cannot be elaborated into the shipped `plan.gbnf` language. */
export class LowerError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "LowerError";
  }
}

// The grammar's terminal productions, transcribed. The guards validate the FORMATTED string
// against these (not the raw number against an open interval): rounding at the band edge —
// planPrice(599.999).toFixed(2) === "600.00", planPct(9.97).toFixed(1) === "10.0" — would
// otherwise slip a non-member past a `px < 600` / `pct < 10` pre-check (PR #178 review finding 1).
const PRICE_PRODUCTION = /^[45][0-9][0-9]\.[0-9][0-9]$/; // price ::= [45][0-9][0-9] "." [0-9][0-9]
const PCT_PRODUCTION = /^[0-9]\.[0-9]$/; // pct ::= [0-9] "." [0-9]

/**
 * The grammar's FIXED underlying terminal: `root` hard-codes `USING exec SPY`. A non-SPY
 * elaboration would parse under `src/lang` (registry-open) yet be OUTSIDE the plan.gbnf language —
 * the honest path to more underlyings is a grammar extension, not a silent emission (finding 2).
 */
export const PLAN_GBNF_UNDERLYING = "SPY";

function planUnderlying(underlying: string): string {
  if (underlying !== PLAN_GBNF_UNDERLYING) {
    throw new LowerError(
      `lower: underlying ${JSON.stringify(underlying)} is outside the shipped plan.gbnf — the grammar's ` +
        `root production fixes "USING exec ${PLAN_GBNF_UNDERLYING}". Extend the grammar to add underlyings; ` +
        `lower will not emit a non-member string.`,
    );
  }
  return underlying;
}

/** Format a price into `plan.gbnf`'s price production, validating the FORMATTED string. */
function planPrice(px: number): string {
  const s = px.toFixed(2);
  if (!PRICE_PRODUCTION.test(s)) {
    throw new LowerError(
      `lower: price ${px} formats to ${JSON.stringify(s)}, outside plan.gbnf price ::= [45][0-9][0-9] "." [0-9][0-9] (400.00 .. 599.99)`,
    );
  }
  return s;
}

/** Format a take-profit percent into `plan.gbnf`'s pct production, validating the FORMATTED string. */
function planPct(pct: number): string {
  const s = pct.toFixed(1);
  if (!PCT_PRODUCTION.test(s)) {
    throw new LowerError(
      `lower: tpPct ${pct} formats to ${JSON.stringify(s)}, outside plan.gbnf pct ::= [0-9] "." [0-9] (0.0 .. 9.9)`,
    );
  }
  return s;
}

/** Format/validate a name against `plan.gbnf`'s `name ::= [a-z] [a-z-]{2,14}` (3..15 chars). */
function planName(name: string): string {
  if (!/^[a-z][a-z-]{2,14}$/.test(name)) {
    throw new Error(`lower: name ${JSON.stringify(name)} violates plan.gbnf name ::= [a-z] [a-z-]{2,14}`);
  }
  return name;
}

/**
 * Elaborate an equity-expressible strategy to a `plan.gbnf`-conformant PLAN document string.
 *
 * The emitted string is a member of the language `scripts/rl-poc/grammar/plan.gbnf` generates AND
 * parses under `src/lang`'s `parse()` — the real engine/validator the reward-bridge drives. The
 * `name` argument names the plan (defaulted per-constructor); it must satisfy the grammar's `name`
 * production.
 */
export function lower(s: EquityLowerable, name?: string): string {
  switch (s.kind) {
    case "ORB": {
      const nm = planName(name ?? `orb-${s.direction}`);
      // Breakout: cross the range edge in the breakout direction; defined risk at the far edge.
      const trigger = s.direction === "up" ? `above ${planPrice(s.orHigh)}` : `below ${planPrice(s.orLow)}`;
      const stopEdge = s.direction === "up" ? `below ${planPrice(s.orLow)}` : `above ${planPrice(s.orHigh)}`;
      return [
        `PLAN ${nm} budget ${s.budgetR}R ttl 16:00`,
        `  USING exec ${planUnderlying(s.underlying)}`,
        `  WHEN spot crosses ${trigger}`,
        `  DO buy 100 shares @ mid`,
        `  TP +${planPct(s.tpPct)}%`,
        `  EXIT spot crosses ${stopEdge} @ bid`,
      ].join("\n");
    }
    case "NewsShort": {
      // The decisive-field flip, elaborated: promotional ⇒ SELL (fade the unfunded pop);
      // real ⇒ BUY (ride the funded continuation). Same levels, opposite side ⇒ different PLAN.
      if (s.catalyst === "promotional") {
        const nm = planName(name ?? `news-fade`);
        return [
          `PLAN ${nm} budget ${s.budgetR}R ttl 16:00`,
          `  USING exec ${planUnderlying(s.underlying)}`,
          `  WHEN spot crosses above ${planPrice(s.orHigh)}`,
          `  DO sell 100 shares @ ask`,
          `  TP +${planPct(s.tpPct)}%`,
          `  EXIT spot crosses above ${planPrice(s.orHigh)} @ ask`,
        ].join("\n");
      }
      const nm = planName(name ?? `news-momo`);
      return [
        `PLAN ${nm} budget ${s.budgetR}R ttl 16:00`,
        `  USING exec ${planUnderlying(s.underlying)}`,
        `  WHEN spot crosses above ${planPrice(s.orHigh)}`,
        `  DO buy 100 shares @ mid`,
        `  TP +${planPct(s.tpPct)}%`,
        `  EXIT spot crosses below ${planPrice(s.orLow)} @ bid`,
      ].join("\n");
    }
    default:
      return unreachable(s);
  }
}
