/**
 * @taprun/spec — public type subset of Tap's v2 plan schema.
 *
 * This file is a STRICT PUBLIC SUBSET re-vendored from the upstream
 * `core/types.ts` in the proprietary Tap engine. Per ADR
 * `2026-05-04-ecosystem-v2-launch.md` §2.4, only the types needed to
 * CONSTRUCT or DISPLAY a Plan are exposed; engine-internal types
 * (Run, IntentRecord, Transition, TransitionKind, Fingerprint,
 * DoctorOutcome, Substrate, OpContext) are intentionally absent.
 *
 * Drift between this file and the upstream is enforced by an
 * architecture test in tap-core (`spec_public_subset_test.ts`) that
 * runs in CI before any release.
 *
 * Schema version: v2 (see ADR `2026-05-03-unified-tap-primitive.md`).
 *
 * No imports allowed in this file — it is a leaf in the import graph.
 */

// ═══════════════════════════════════════════════════════════════════════════
// L1 — Identity and primitives
// ═══════════════════════════════════════════════════════════════════════════

/** Pure identity for a tap. Two TapIds equal iff site AND name match. */
export interface TapId {
  site: string;
  name: string;
}

/** A CEL expression source string. Validated by the engine's CEL
 *  Environment at lint time; here it's a transparent string alias. */
export type CelExpr = string;

/** Generic JSON value — used for opaque substrate I/O. */
// deno-lint-ignore no-explicit-any
export type Json = any;

/** Argument schema declaration on a Plan. */
export interface ArgSpec {
  type: "string" | "int" | "number" | "boolean" | "object" | "array";
  default?: Json;
  required?: true;
  description?: string;
}

// ═══════════════════════════════════════════════════════════════════════════
// L2 — Op closure (11 ops, closed union)
// ═══════════════════════════════════════════════════════════════════════════

/** Substrate ops (7) — cross runtime RPC boundary. */

export interface FetchOp {
  op: "fetch";
  url: string;
  method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
  headers?: Record<string, string>;
  body?: Json;
  format?: "json" | "text" | "arrayBuffer";
  /** Credential source. Same-origin only when "page-session". */
  credentials?: "deno-host" | "page-session";
  save?: string;
}

/** Per ADR `2026-05-14-op-nav-attach.md` — closed 3-arm match mode for
 *  the `op:nav.attach` directive. Growth requires ADR amendment + arch
 *  test NA1 fail. PUBLIC mirror of core/types.ts:NAV_ATTACH_MATCH_MODES. */
export const NAV_ATTACH_MATCH_MODES = [
  "url-prefix",
  "origin",
  "exact",
] as const;
export type NavAttachMatchMode = typeof NAV_ATTACH_MATCH_MODES[number];

/** Tab-attach directive on `op:nav`. PUBLIC mirror of NavAttach.
 *  - `true` shorthand → `{ match: "url-prefix" }` (the dominant case)
 *  - explicit object → caller-chosen match mode
 *  Semantics: when set, peer queries Chrome tabs under the match mode;
 *  on match, binds sessionId → existing tabId (preserving sessionStorage
 *  for same-origin navigations) before running the standard nav path. */
export type NavAttach =
  | true
  | {
    match: NavAttachMatchMode;
    /** `false` ⇒ when a tab MATCHED, bind WITHOUT navigating (no
     *  `tabs.update`, no reload) — preserves live page state (form
     *  drafts) in the user's tab; the author accepts acting wherever
     *  the matched tab currently is. 0 matches still falls through to
     *  create + navigate. Absent/`true` ⇒ navigate-always. */
    reload?: boolean;
  };

export interface NavOp {
  op: "nav";
  url: string;
  /** Optional attach directive. Absent = always-create (today's default).
   *  Present = find-or-create. See ADR `2026-05-14-op-nav-attach.md`. */
  attach?: NavAttach;
  save?: string;
}

export interface WaitOp {
  op: "wait";
  ms?: number;
  selector?: string;
  timeout_ms?: number;
  save?: string;
}

export interface InputOp {
  op: "input";
  kind:
    | "click"
    | "type"
    | "fill"
    | "press"
    | "upload"
    | "setHtml"
    | "hover"
    | "keytype"
    | "blur";
  target?: string;
  /** kind=type/fill → text written to `.value`; kind=setHtml → HTML assigned
   *  to `target.innerHTML` (rich-text / contenteditable editors). Receives
   *  `{{$args}}` substitution as DATA (unlike `eval.fn`), so large per-run
   *  HTML flows in as an arg without baking. PUBLIC mirror of
   *  core/types.ts:InputOp.value. */
  value?: string;
  /** Substrate-tier hint. Absent/false → L1 (JS-injection click; default;
   *  no DevTools warning bar). true → L2 (CDP trusted click at element
   *  coordinates; emits isTrusted:true events). Set by `capture` when
   *  forge detects L1 fails the `expect` predicate. PUBLIC mirror of
   *  core/types.ts:InputOp.trusted per ADR
   *  `2026-05-19-forge-tier-discovery.md` §2A. */
  trusted?: boolean;
  /** Per-op postcondition predicate. Truthy → ok; falsy → tap_drifted.
   *  PUBLIC mirror of core/types.ts:InputOp.expect per ADR
   *  `2026-05-08-failure-detection-phase-2.md` §2B (extended to InputOp
   *  by `2026-05-19-forge-tier-discovery.md`). */
  expect?: CelExpr;
  save?: string;
}

export interface ExtractSpec {
  attr?: string;
  text?: string;
  innerText?: string;
  html?: string;
  exists?: string;
  selector?: string;
}

export interface ExtractOp {
  op: "extract";
  root: string;
  per_item: Record<string, ExtractSpec>;
  save?: string;
}

export interface CookiesOp {
  op: "cookies";
  domain?: string;
  name?: string;
  save?: string;
}

export interface TapOp {
  op: "tap";
  site: string;
  name: string;
  args?: Record<string, Json>;
  save?: string;
}

/** Control flow (3). */

export interface IfOp {
  op: "if";
  cond: CelExpr;
  then: Op[];
  else?: Op[];
  save?: string;
}

export interface ForeachOp {
  op: "foreach";
  over?: CelExpr;
  count?: number;
  body: Op[];
  until?: CelExpr;
  save?: string;
}

export interface ParallelOp {
  op: "parallel";
  branches: Op[][];
  save?: string;
}

/** Escape (1) — value-only; lint forbids side effects in `fn`.
 *  `returns.type` is MANDATORY (runtime schema-validates output). */
export interface EvalOp {
  op: "eval";
  fn: string;
  returns: { type: "string" | "number" | "boolean" | "object" | "array" };
  args?: Json[];
  save?: string;
}

/** Host op (1) — browser-harness management (tabs / tab-groups). Acts on
 *  the user's own browser chrome, not a foreign substrate; tab-free,
 *  peer-routed to the extension. Per ADR 2026-06-11-op-tab-host-op.md. */
export interface TabOp {
  op: "tab";
  action: TabAction;
  /** number[] literal OR a template string resolving to number[].
   *  Required for group/ungroup/close/pin/unpin; omit for list. */
  tabIds?: (number | string)[] | string;
  title?: string;
  color?: TabGroupColor;
  save?: string;
}

/** Closed list of op:tab actions. Growth requires ADR amendment. */
export const TAB_ACTIONS = [
  "list", "group", "ungroup", "close", "pin", "unpin",
] as const;
export type TabAction = typeof TAB_ACTIONS[number];

/** Closed tab-group color set — mirrors chrome.tabGroups.Color. */
export const TAB_GROUP_COLORS = [
  "grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange",
] as const;
export type TabGroupColor = typeof TAB_GROUP_COLORS[number];

/** Host op (1) — bookmark-tree management. Same tier as TabOp: acts on
 *  the user's own browser chrome (chrome.bookmarks), not a foreign
 *  substrate; tab-free, peer-routed to the extension. Per ADR
 *  2026-06-11-op-bookmark-host-op.md. */
export interface BookmarkOp {
  op: "bookmark";
  action: BookmarkAction;
  /** Target node id (move/update/remove/removeTree). */
  id?: string;
  /** Destination parent folder id (create/move). */
  parentId?: string;
  /** Position within the parent (create/move). */
  index?: number;
  /** Title (create folder/bookmark; update). */
  title?: string;
  /** URL (create/update a bookmark; omit for folder). */
  url?: string;
  save?: string;
}

/** Closed list of op:bookmark actions. Growth requires ADR amendment. */
export const BOOKMARK_ACTIONS = [
  "tree", "create", "move", "update", "remove", "removeTree",
] as const;
export type BookmarkAction = typeof BOOKMARK_ACTIONS[number];

/** The closed Op union — exactly 13 members. */
export type Op =
  | FetchOp | NavOp | WaitOp | InputOp | ExtractOp | CookiesOp | TapOp
  | IfOp | ForeachOp | ParallelOp
  | EvalOp | TabOp | BookmarkOp;

/** Runtime constant for the 13-op closure. */
export const OP_NAMES_V2 = [
  "fetch", "nav", "wait", "input", "extract", "cookies", "tap",
  "if", "foreach", "parallel",
  "eval", "tab", "bookmark",
] as const;

export type OpName = typeof OP_NAMES_V2[number];

// ═══════════════════════════════════════════════════════════════════════════
// L3 — Plan
// ═══════════════════════════════════════════════════════════════════════════

// ─── Plan lifecycle (per ADR 2026-05-10-plan-lifecycle-scoped-tabs) ─────
//
// Closed union: a plan declares its tab lifetime intent. Mirror of
// `PLAN_LIFECYCLES` / `PlanLifecycle` / `resolveLifecycle` in
// core/types.ts. Growing past 2 values requires ADR amendment + arch
// test PL1 update on the engine side.

/** Tab lifetime policy values. */
export const PLAN_LIFECYCLES = ["scoped", "interactive"] as const;
export type PlanLifecycle = typeof PLAN_LIFECYCLES[number];

/** Resolution helper: explicit field wins; absent defaults to "scoped"
 *  (RAII-safe default per the ADR's Decision Standard 1). */
export function resolveLifecycle(plan: { lifecycle?: PlanLifecycle }): PlanLifecycle {
  return plan.lifecycle ?? "scoped";
}

/** Common Plan fields (shared by read and write variants). */
interface PlanCommon {
  /** Self-declared schema URL for forward-compatibility (per ADR
   *  `2026-05-09-userspace-via-standards.md` INV-2). When absent,
   *  the engine backward-fills to the current schema reader. When
   *  present and known, dispatch is exact. */
  $schema?: string;
  id: TapId;
  description?: string;
  args?: Record<string, ArgSpec>;
  /** CEL constraints over args. Each must return bool. */
  arg_constraints?: CelExpr[];
  requires?: { runtime?: "extension" | "playwright" };
  /** Tab lifetime policy (per ADR 2026-05-10-plan-lifecycle-scoped-tabs.md).
   *  Absent ⇒ resolveLifecycle returns "scoped" (RAII-safe default). */
  lifecycle?: PlanLifecycle;
  /** Pure read of current state. */
  observe?: Op[];
  /** What the tap returns. CEL over $args + phase outputs. */
  return: CelExpr;
  /** CEL boolean evaluated after `observe` and before `act`, declaring
   *  substrate-state that must hold for the plan to proceed. When false,
   *  runtime emits `precondition_unmet` and the user-action arm names
   *  what the user must do. Scope = `$args` + `$observe`. Per ADR
   *  `2026-05-04-user-recoverable-failures.md`. */
  expects?: CelExpr;
  /** The source URL the tap was originally captured from. Set by
   *  `capture` so subsequent calls (heal-by-recapture, drift diagnosis)
   *  know where the substrate lives without manual passing. */
  source_url?: string;
  /** The user's intent expressed in natural language at capture time.
   *  Forge consumes this for template selection + AI-fallback routing.
   *  Plan.description may be derived from intent if absent. */
  source_intent?: string;
}

/** Discriminated union: act non-empty ⇒ key required at type level.
 *  `never` (not `undefined`) makes absence a hard type-level invariant. */
export type Plan =
  | (PlanCommon & {
      // Pure read variant — write fields unrepresentable
      act?: never;
      key?: never;
      precondition?: never;
      postcondition?: never;
      return_when_skipped?: never;
      dedup_ttl_seconds?: never;
      confirm?: never;
    })
  | (PlanCommon & {
      // Write variant — act + key both required
      act: Op[];
      key: CelExpr;
      confirm?: Op[];
      precondition?: CelExpr;
      postcondition?: CelExpr;
      return_when_skipped?: CelExpr;
      dedup_ttl_seconds?: number;
    });

// ═══════════════════════════════════════════════════════════════════════════
// L5 — Intent state enum (PUBLIC; full IntentRecord is INTERNAL)
// ═══════════════════════════════════════════════════════════════════════════

export const INTENT_STATES = [
  "preflight", "in_flight", "committed", "aborted", "uncertain",
] as const;
export type IntentState = typeof INTENT_STATES[number];

// ═══════════════════════════════════════════════════════════════════════════
// L7 — Doctor verdict enum (PUBLIC; full DoctorOutcome is INTERNAL)
// ═══════════════════════════════════════════════════════════════════════════

/** 3-arm closed enum returned by `verify`.
 *  - live: every observe op succeeded; any op.expect predicate truthy
 *  - drifted: 4xx/5xx, op.expect → falsy, JSON parse fail, or unimplemented op
 *  - unreachable: network failure / timeout / DNS / connection refused
 *
 *  Per ADR `2026-05-10-snapshot-dissolved.md` — the v0.x 4-arm
 *  (equivalent/drifted/baseline-set/unreachable) retired with the
 *  snapshot subsystem. */
export const VERDICT_VALUES = [
  "live", "drifted", "unreachable",
] as const;
export type Verdict = typeof VERDICT_VALUES[number];
