/**
 * # builders — the programmatic authoring surface (ADR-0004)
 *
 * Ergonomic constructors for the {@link ../ast.ts} object model. An ESM module composing
 * these objects and a `.kestrel` file are the same kind of module (ADR-0004): programs
 * compose here, agents/humans write text, neither is second-class.
 *
 * Every helper returns a plain readonly AST node with its `kind` tag filled in. Optional
 * fields are omitted (never set to `undefined`) so the result satisfies
 * `exactOptionalPropertyTypes`.
 */

import type {
  AbsolutePrice,
  Anchor,
  AnchorName,
  Arbitration,
  ArmClause,
  Baseline,
  BookStatement,
  Budget,
  CancelIfClause,
  Citation,
  Comparison,
  CompareOp,
  Coverage,
  Counterfactual,
  CrossEvent,
  DeliverSpec,
  DoTicket,
  Duration,
  ExitClause,
  ExpirySelector,
  FillEvent,
  GradeDimension,
  GradeStatement,
  GradeSubject,
  HeldQuantifier,
  ImportDecl,
  InvalidateClause,
  Instrument,
  Leg,
  Lean,
  Module,
  Operand,
  OrderPolicy,
  PaneArg,
  PaneRef,
  PhaseEvent,
  PlanClause,
  PlanStatement,
  PodStatement,
  PriceExpr,
  PriceMinMax,
  PriceOffset,
  Provenance,
  Quantity,
  RegimeGate,
  RegimeTagBinding,
  ReloadClause,
  RiskLine,
  SeriesRef,
  Standing,
  Statement,
  StrikeSpec,
  StructuralEvent,
  TimeOfDay,
  TimeUnit,
  TimeWindowTrig,
  TpClause,
  TpTarget,
  Trigger,
  Ttl,
  Using,
  ViewStatement,
  WakeBudget,
  WakeStatement,
  Window,
} from "./ast.ts";
import {
  refuseAtomic,
  refuseBadCitation,
  refuseExitOnMark,
  refuseHeldOverCross,
  refuseNonPositiveBand,
  refuseNonPositiveBudget,
  refuseProvenanceElevation,
} from "./validate.ts";

// Strip keys whose value is undefined, so optional AST fields stay absent rather than
// present-and-undefined (exactOptionalPropertyTypes). The narrowing back to the AST type
// happens at each call site's return.
function compact<T extends Record<string, unknown>>(o: T): Partial<T> {
  const r: Record<string, unknown> = {};
  for (const [k, v] of Object.entries(o)) if (v !== undefined) r[k] = v;
  return r as Partial<T>;
}

// ── Lexical units ────────────────────────────────────────────────────────────

export const win = (value: number, unit: TimeUnit): Window => ({ kind: "window", value, unit });
export const dur = (value: number, unit: TimeUnit): Duration => ({ kind: "duration", value, unit });
export const tod = (hour: number, minute: number): TimeOfDay => ({
  kind: "time-of-day",
  hour,
  minute,
});
export const num = (value: number, unit?: Quantity["unit"]): Quantity =>
  ({ kind: "quantity", ...compact({ value, unit }) }) as Quantity;
export const p = (n: number): Baseline => ({ kind: "baseline", stat: "p", n });
export const sigma = (n: number): Baseline => ({ kind: "baseline", stat: "sigma", n });

// ── Series ───────────────────────────────────────────────────────────────────

/** A series reference: `series("spot")`, `series("velocity", { window: win(1,"m") })`.
 * Dotted paths pass segments: `series(["regime","intraday"])`. The market/org distinction
 * is registry metadata, not syntax (ADR-0004), so it is not encoded here. */
export function series(path: string | readonly string[], opts: { window?: Window } = {}): SeriesRef {
  const names = typeof path === "string" ? [path] : path;
  return {
    kind: "series",
    segments: names.map((name) => ({ name })),
    ...compact({ window: opts.window }),
  } as SeriesRef;
}

/** An org-aggregate path with a quantified segment: `children(any).drawdown`. */
export const childrenAny = (field: string): SeriesRef => ({
  kind: "series",
  segments: [{ name: "children", selector: { kind: "sel-quant", q: "any" } }, { name: field }],
});

/** A named plan-state path: `plan(chase-urgent).state`. */
export const planState = (planName: string, field = "state"): SeriesRef => ({
  kind: "series",
  segments: [{ name: "plan", selector: { kind: "sel-name", name: planName } }, { name: field }],
});

// ── Triggers ───────────────────────────────────────────────────────────────

export const cmp = (left: Operand, op: CompareOp, right: Operand): Comparison => ({
  kind: "cmp",
  op,
  left,
  right,
});
export const gt = (l: Operand, r: Operand): Comparison => cmp(l, "gt", r);
export const ge = (l: Operand, r: Operand): Comparison => cmp(l, "ge", r);
export const lt = (l: Operand, r: Operand): Comparison => cmp(l, "lt", r);
export const le = (l: Operand, r: Operand): Comparison => cmp(l, "le", r);
export const eq = (l: Operand, r: Operand): Comparison => cmp(l, "eq", r);
export const ne = (l: Operand, r: Operand): Comparison => cmp(l, "ne", r);

/** A cross edge event. `opts.touch` picks the at-or-touch predicate (`touches`, `>=`/`<=`);
 * `opts.band` is a positive re-arm width so a lone spurious tick cannot phantom-fire.
 * Both are omitted (a bare strict cross) by default. The band>0 doctrine invariant is enforced
 * at construction (fail closed early) via ./validate.ts, symmetric with parse/print. */
export const cross = (
  left: Operand,
  direction: "above" | "below",
  right: Operand,
  opts: { touch?: boolean; band?: Quantity } = {},
): CrossEvent => {
  refuseNonPositiveBand(opts.band);
  return { kind: "cross", left, dir: direction, right, ...compact(opts) } as CrossEvent;
};

/** Refuse to CONSTRUCT held-over-cross (the sfg8 anti-pattern; ./validate.ts). `within` over a
 * cross stays legal. */
export const held = (inner: Trigger, d: Duration): Trigger => {
  refuseHeldOverCross(inner);
  return { kind: "break-hold", inner, dur: d };
};
export const within = (inner: Trigger, d: Duration): Trigger => ({ kind: "within", inner, dur: d });
/** `held <dur>` in lead position — a 0DTE time-held stop (`EXIT held 90m`); a bare hold-clock on
 * the acquired position, NOT the `held(inner, dur)` break-hold postfix (docs/results/fomc-options).*/
export const heldStop = (d: Duration): Trigger => ({ kind: "held-stop", dur: d });
/** `clockET <clock>` — a 0DTE wall-clock time-stop (`EXIT clockET 15:40`). */
export const clockStop = (time: TimeOfDay): Trigger => ({ kind: "clock-stop", at: time });
/** `inner until <clock>` — the thesis temporal envelope (kestrel-rtf), a postfix sibling of
 * `within` over the same predicate surface (no second predicate language). */
export const until = (inner: Trigger, time: TimeOfDay): Trigger => ({ kind: "until", inner, at: time });
/** `inner at <clock>` — the thesis temporal envelope (kestrel-rtf), a postfix sibling of `within`. */
export const at = (inner: Trigger, time: TimeOfDay): Trigger => ({ kind: "at", inner, at: time });
export const nth = (ordinal: number, event: Trigger): Trigger => {
  // Ordinals are 1-based (`first`, `second`, …) — the parser rejects 0/negatives and the
  // printer cannot render them lexably. Refuse to construct one that could never round-trip.
  if (!Number.isInteger(ordinal) || ordinal < 1) {
    throw new Error(`kestrel/builders: nth ordinal must be a positive integer (>= 1); got ${String(ordinal)}`);
  }
  return { kind: "nth", ordinal, event };
};
export const event = (name: string, of?: Operand): StructuralEvent =>
  ({ kind: "event", name, ...compact({ of }) }) as StructuralEvent;
export const phase = (name: string): PhaseEvent => ({ kind: "phase", phase: name });
export const timeWindow = (opts: { from?: TimeOfDay; to?: TimeOfDay }): TimeWindowTrig =>
  ({ kind: "time-window", ...compact(opts) }) as TimeWindowTrig;
export const fill = (evt: FillEvent["event"], leg?: number): FillEvent =>
  ({ kind: "fill", event: evt, ...compact({ leg }) }) as FillEvent;
export const and = (...terms: Trigger[]): Trigger => ({ kind: "and", terms });
export const or = (...terms: Trigger[]): Trigger => ({ kind: "or", terms });
export const not = (term: Trigger): Trigger => ({ kind: "not", term });

// ── Prices ───────────────────────────────────────────────────────────────────

export const anchor = (name: AnchorName): Anchor => ({ kind: "anchor", name });
export const fair: Anchor = anchor("fair");
export const intrinsic: Anchor = anchor("intrinsic");
export const basis: Anchor = anchor("basis");
export const bid: Anchor = anchor("bid");
export const ask: Anchor = anchor("ask");
export const mid: Anchor = anchor("mid");
export const last: Anchor = anchor("last");
export const join: Anchor = anchor("join");
export const improve: Anchor = anchor("improve");
export const stub: Anchor = anchor("stub");
// The `spot` PRICE anchor (ADR-0030 / kestrel-ipc). No collision with the `spot` SERIES: that is
// the function-call `series("spot")`, this is the anchor-value node.
export const spot: Anchor = anchor("spot");

export const px = (value: number): AbsolutePrice => ({ kind: "price-abs", value });
export const minus = (base: PriceExpr, amount: number, unit: "c" | "%" = "c"): PriceOffset => ({
  kind: "price-offset",
  base,
  sign: "-",
  amount,
  unit,
});
export const plus = (base: PriceExpr, amount: number, unit: "c" | "%" = "c"): PriceOffset => ({
  kind: "price-offset",
  base,
  sign: "+",
  amount,
  unit,
});
export const lean = (a: PriceExpr, b: PriceExpr, x: number): Lean => ({ kind: "lean", a, b, x });
export const pmin = (...args: PriceExpr[]): PriceMinMax => ({ kind: "price-fn", fn: "min", args });
export const pmax = (...args: PriceExpr[]): PriceMinMax => ({ kind: "price-fn", fn: "max", args });

// ── Legs & order policy ──────────────────────────────────────────────────────

export const rel = (steps: number): StrikeSpec => ({ kind: "strike-rel", steps });
export const abs = (strike: number): StrikeSpec => ({ kind: "strike-abs", strike });
export const atm: StrikeSpec = { kind: "strike-atm" };
export const delta = (d: number): StrikeSpec => ({ kind: "strike-delta", delta: d });

/** An option leg `buy <qty> <strike> <right>`, optionally with its OWN expiry (`buy 2 +1 C exp 0dte`,
 * kestrel-ih5h seam 1). Omit `expiry` and the leg inherits the ambient `USING exec` tenor — the object
 * form of leaving `exp` off the text, so the two authoring surfaces (ADR-0004: objects and text are
 * peers) stay mechanically equivalent. */
export const buy = (qty: number, strike: StrikeSpec, right: "C" | "P", expiry?: ExpirySelector): Leg =>
  ({ kind: "leg", side: "buy", qty, strike, right, ...compact({ expiry }) }) as Leg;
export const sell = (qty: number, strike: StrikeSpec, right: "C" | "P", expiry?: ExpirySelector): Leg =>
  ({ kind: "leg", side: "sell", qty, strike, right, ...compact({ expiry }) }) as Leg;

/** An equity/spot leg `buy <qty> shares` (ADR-0017) — no strike/right; the symbol is the
 * ambient `USING exec`. */
export const buyShares = (qty: number): Leg => ({ kind: "equity-leg", side: "buy", qty });
export const sellShares = (qty: number): Leg => ({ kind: "equity-leg", side: "sell", qty });

export const esc = (to: PriceExpr, after: Duration): { kind: "esc-stage"; to: PriceExpr; after: Duration } => ({
  kind: "esc-stage",
  to,
  after,
});

export const policy = (opts: {
  pricing?: "peg" | "fix";
  esc?: readonly ReturnType<typeof esc>[];
  caps?: readonly PriceExpr[];
  floors?: readonly PriceExpr[];
  cancelIf?: Trigger;
  gtc?: boolean;
}): OrderPolicy => ({ kind: "order-policy", ...compact(opts) }) as OrderPolicy;

export const foreachHeld: HeldQuantifier = { kind: "held-foreach" };
export const anyHeld: HeldQuantifier = { kind: "held-any" };

// ── Plan clauses ─────────────────────────────────────────────────────────────

export const doTicket = (opts: {
  legs: readonly Leg[];
  price: PriceExpr;
  policy?: OrderPolicy;
  atomic?: boolean;
}): DoTicket => (refuseAtomic(opts.atomic), { kind: "do", ...compact(opts) } as DoTicket);

export const also = (opts: {
  legs: readonly Leg[];
  price: PriceExpr;
  policy?: OrderPolicy;
  atomic?: boolean;
}): PlanClause => (refuseAtomic(opts.atomic), { kind: "also", ...compact(opts) } as PlanClause);

export const reload = (opts: {
  when?: Trigger;
  legs: readonly Leg[];
  price: PriceExpr;
  policy?: OrderPolicy;
  atomic?: boolean;
}): ReloadClause => (refuseAtomic(opts.atomic), { kind: "reload", ...compact(opts) } as ReloadClause);

export const tpPct = (pct: number): TpTarget => ({ kind: "tp-pct", pct });
export const tpMult = (mult: number): TpTarget => ({ kind: "tp-mult", mult });
export const tpPrice = (price: PriceExpr): TpTarget => ({ kind: "tp-price", price });

export const tp = (opts: {
  target: TpTarget;
  frac?: number;
  over?: HeldQuantifier;
  price?: PriceExpr;
  policy?: OrderPolicy;
}): TpClause => ({ kind: "tp", ...compact(opts) }) as TpClause;

export const exit = (opts: {
  when: Trigger;
  over?: HeldQuantifier;
  price?: PriceExpr;
  policy?: OrderPolicy;
}): ExitClause => (
  // marks lie (ADR-0005): refuse an EXIT conditioned on a mark at construction, symmetric with
  // parse/print, so the object never exists to break `parse(print(x))`.
  refuseExitOnMark(opts.when), { kind: "exit", ...compact(opts) } as ExitClause
);

export const invalidate = (when: Trigger): InvalidateClause => ({ kind: "invalidate", when });
export const cancelIf = (when: Trigger): CancelIfClause => ({ kind: "cancel-if", when });
export const arm = (opts: { when?: Trigger; basis?: PriceExpr; over?: HeldQuantifier } = {}): ArmClause =>
  ({ kind: "arm", ...compact(opts) }) as ArmClause;

// ── Instruments, USING, provenance ───────────────────────────────────────────

export const dte = (n: number): ExpirySelector => ({ kind: "expiry-dte", dte: n });
export const expDate = (date: string): ExpirySelector => ({ kind: "expiry-date", date });
export const expTag = (tag: string): ExpirySelector => ({ kind: "expiry-tag", tag });

export const instrument = (symbol: string, expiry?: ExpirySelector): Instrument =>
  ({ kind: "instrument", symbol, ...compact({ expiry }) }) as Instrument;

export const using = (opts: { signal?: Instrument; exec?: Instrument }): Using =>
  ({ kind: "using", ...compact(opts) }) as Using;

export const provenance = (opts: {
  tier?: Provenance["tier"];
  author?: string;
  origin?: string;
  replay?: string;
}): Provenance => ({ kind: "provenance", ...compact(opts) }) as Provenance;

/** A `because` pre-registration citation (kestrel-rtf): a content hash of the fixed shape
 * `sha256:<64 hex>`, no inline body. Refuses a malformed digest at construction, symmetric with
 * parse/print (./validate.ts), so the object never exists to break `parse(print(x))`. */
export const citation = (hash: string, algo: Citation["algo"] = "sha256"): Citation => {
  const c: Citation = { kind: "citation", algo, hash };
  refuseBadCitation(c);
  return c;
};

// ── Statement helpers ────────────────────────────────────────────────────────

export const budget = (value: number): Budget => (
  refuseNonPositiveBudget(value), { kind: "budget", value, unit: "R" }
);
export const ttlIn = (d: Duration): Ttl => ({ kind: "ttl-rel", dur: d });
export const ttlAt = (at: TimeOfDay): Ttl => ({ kind: "ttl-at", at });
export const regime = (...tags: RegimeTagBinding[]): RegimeGate => ({ kind: "regime-gate", tags });
export const tag = (scope: string, value: string): RegimeTagBinding => ({ scope, value });

export const argIdent = (name: string): PaneArg => ({ kind: "arg-ident", name });
export const argWindow = (w: Window): PaneArg => ({ kind: "arg-window", window: w });
export const argCount = (count: number): PaneArg => ({ kind: "arg-count", count });
/** A SessionOrdinal literal `d-<n>` (Train 1B / ADR-0041 §1): `d-0` current session, `d-1` prior… */
export const argOrdinal = (ordinal: number): PaneArg => ({ kind: "arg-ordinal", ordinal });
/** An ExpiryOrdinal literal `e-<n>` (Train 1B / ADR-0041 §1): `e-0` nearest expiry, `e-1` next out… */
export const argExpiry = (expiry: number): PaneArg => ({ kind: "arg-expiry", expiry });
export const pane = (name: string, ...args: PaneArg[]): PaneRef => ({ kind: "pane", name, args });

export const view = (
  name: string,
  panes: readonly PaneRef[],
  opts: { budget?: number } = {},
): ViewStatement => ({ kind: "view", name, panes, ...compact(opts) }) as ViewStatement;

export const deliver = (
  viewName: string,
  opts: { mandatory?: boolean; keyframe?: boolean } = {},
): DeliverSpec => ({ kind: "deliver", view: viewName, ...compact(opts) }) as DeliverSpec;

export const wakeBudget = (opts: { wakesPerDay?: number; tokensPerDay?: number }): WakeBudget =>
  ({ kind: "wake-budget", ...compact(opts) }) as WakeBudget;

export const wake = (
  name: string,
  when: Trigger,
  opts: {
    because?: Citation;
    deliver?: DeliverSpec;
    priority?: number;
    coalesce?: string;
    budget?: WakeBudget;
    universe?: { kind: "universe"; universe: string };
  } = {},
): WakeStatement => ({ kind: "wake", name, when, ...compact(opts) }) as WakeStatement;

export const universe = (name: string): { kind: "universe"; universe: string } => ({
  kind: "universe",
  universe: name,
});

export const plan = (
  name: string,
  opts: {
    budget?: Budget;
    ttl?: Ttl;
    regime?: RegimeGate;
    priority?: number;
    standing?: Standing;
    provenance?: Provenance;
    using?: Using;
    when?: Trigger;
    because?: Citation;
    clauses?: readonly PlanClause[];
    atomic?: boolean;
  } = {},
): PlanStatement => (
  refuseAtomic(opts.atomic),
  ({ kind: "plan", name, clauses: opts.clauses ?? [], ...compact({ ...opts, clauses: undefined }) }) as PlanStatement
);

export const subject = (what: GradeSubject["what"], name: string): GradeSubject => ({
  kind: "grade-subject",
  what,
  name,
});
export const over = (from: string, to: string): { kind: "corpus-range"; from: string; to: string } => ({
  kind: "corpus-range",
  from,
  to,
});
export const vsUngated: Counterfactual = { kind: "cf-ungated" };
export const vsNull: Counterfactual = { kind: "cf-null" };
export const vsBracket: Counterfactual = { kind: "cf-bracket" };
export const bySeries = (s: SeriesRef): GradeDimension => ({ kind: "dim-series", series: s });
export const byVehicle: GradeDimension = { kind: "dim-vehicle" };
export const byName: GradeDimension = { kind: "dim-name" };
export const byLineage: GradeDimension = { kind: "dim-lineage" };

export const grade = (
  gradeSubject: GradeSubject,
  opts: {
    over?: { kind: "corpus-range"; from: string; to: string };
    fill?: string;
    versus?: readonly Counterfactual[];
    by?: readonly GradeDimension[];
  } = {},
): GradeStatement =>
  ({
    kind: "grade",
    subject: gradeSubject,
    versus: opts.versus ?? [],
    by: opts.by ?? [],
    ...compact({ over: opts.over, fill: opts.fill }),
  }) as GradeStatement;

export const coverage = (instruments: readonly Instrument[], thesis: string): Coverage => ({
  kind: "coverage",
  instruments,
  thesis,
});
export const arbitration = (policy: string, concurrency?: number): Arbitration =>
  ({ kind: "arbitration", policy, ...compact({ concurrency }) }) as Arbitration;
export const risk = (metric: string, threshold: Quantity, action: string): RiskLine => ({
  kind: "risk",
  metric,
  threshold,
  action,
});

export const book = (
  name: string,
  opts: {
    budget?: Budget;
    coverage?: Coverage;
    arbitration?: Arbitration;
    risk?: readonly RiskLine[];
  } = {},
): BookStatement => ({ kind: "book", name, ...compact(opts) }) as BookStatement;

export const pod = (
  name: string,
  opts: { risk?: readonly RiskLine[]; children?: readonly (PodStatement | BookStatement)[] } = {},
): PodStatement => ({
  kind: "pod",
  name,
  risk: opts.risk ?? [],
  children: opts.children ?? [],
});

export const importFrom = (names: readonly string[], from: string): ImportDecl => ({
  kind: "import",
  names,
  from,
});

export const module = (opts: {
  imports?: readonly ImportDecl[];
  using?: Using;
  statements?: readonly Statement[];
  provenance?: Provenance;
}): Module => {
  const statements = opts.statements ?? [];
  // Provenance ceiling (ARCHITECTURE §6): refuse a module whose statement elevates above its
  // declared tier at construction, symmetric with parse/print (./validate.ts).
  refuseProvenanceElevation(opts.provenance?.tier, statements);
  return {
    kind: "module",
    imports: opts.imports ?? [],
    statements,
    ...compact({ using: opts.using, provenance: opts.provenance }),
  } as Module;
};
