/**
 * # protocol/wire — the PLATFORM HTTP/SSE wire shapes (kestrel-5rb, OSS-ADR-0046).
 *
 * The single, authoritative TypeScript mirror of the kestrel.markets contract as
 * served on the wire: **snake_case** bodies + the canonical SSE `OperationEvent.type`
 * vocabulary, transcribed from
 * `kestrel.markets/packages/contracts/openapi/kestrel-markets.openapi.json`
 * (schemas TrialCapability / ValidateResult / Operation / OfferResponse / Offer /
 * Money / SettlementMethod / HumanAction / Proof / OperationEvent; the schema is
 * authoritative for shapes, docs/contract/http-sse-contract.md for semantics).
 *
 * Per OSS-ADR-0046 these shapes live HERE in `src/protocol` — the same home as the
 * rest of the wire vocabulary (the `Scope` union, `RECEIPT_KINDS`, the SSE
 * constants) — so a shape change is one edit and drift is a red conformance run.
 * The CLI transport (`src/cli/backend/wire.ts`) re-exports this module rather than
 * hand-maintaining a parallel copy.
 *
 * This is DELIBERATELY distinct from the OSS camelCase domain types in
 * {@link ./index.ts}: the wire is snake_case, the Offer amount is a {@link WireMoney}
 * object (not a bare number), the cursor is an OPAQUE STRING (not `{sequence, token}`),
 * and the SSE events are `operation.*`. Both {@link ../cli/backend/remote.ts remote.ts}
 * (the client) and {@link ../cli/backend/mock.ts mock.ts} (the truthful in-process
 * server) consume it through the transport re-export, so neither can silently
 * re-fabricate a dialect: the SSE vocabulary in particular has exactly one definition
 * here, drift-guarded against the OpenAPI enum by `tests/cli.remote-conformance.test.ts`.
 *
 * Pure types + constants + pure decoders. ZERO runtime dependencies — the only import
 * is the intra-protocol domain types it decodes into (asserted by
 * tests/protocol.boundary.test.ts).
 */

import type { Scope, TrialCapability, Operation, OperationState } from "./index.ts";

/**
 * The OVERSIGHT wire mirror (contract §4.4, kestrel-telx.5) — `WireOversightEvent` + the
 * snake_case decoders, re-exported HERE so it sits beside {@link SSE_EVENT_TYPES} on the ONE
 * import path the CLI transport ({@link ../cli/backend/wire.ts}) re-exports. The definitions live
 * in their own leaf only because they are long; nothing there re-declares an SSE vocabulary (the
 * closed oversight enum has exactly one home, `./oversight.ts`).
 */
export * from "./oversight-wire.ts";

/* ───────────────────────────── SSE vocabulary ──────────────────────────── */

/** The canonical `OperationEvent.type` enum (openapi OperationEvent.type). The ONE
 *  definition of the SSE vocabulary; the conformance suite pins it to the fixture. */
export const SSE_EVENT_TYPES = [
  "operation.started",
  "operation.progress",
  "operation.artifact",
  "operation.receipt",
  "operation.suspended",
  "operation.resumed",
  "operation.completed",
  "operation.failed",
] as const;
export type SseEventType = (typeof SSE_EVENT_TYPES)[number];

/** The two terminal event types that end an Operation's stream (§9 Events). */
export const SSE_TERMINAL_TYPES = ["operation.completed", "operation.failed"] as const;

/** True iff `t` is a contract SSE event type. Anything else is a protocol violation
 *  the client must FAIL CLOSED on (AGENTS.md: unknown series/event → de-arm, never
 *  silently ignore). */
export function isSseEventType(t: string): t is SseEventType {
  return (SSE_EVENT_TYPES as readonly string[]).includes(t);
}
export function isTerminalSseEvent(t: string): t is (typeof SSE_TERMINAL_TYPES)[number] {
  return (SSE_TERMINAL_TYPES as readonly string[]).includes(t);
}

/* ─────────────────────────────── wire shapes ───────────────────────────── */

/** openapi TrialCapability. `capability` is the bearer token (NOT `capabilityId`). */
export interface WireTrialCapability {
  readonly capability: string;
  readonly subject_commitment: string;
  readonly expiry: string;
  readonly scopes: readonly Scope[];
  readonly rate_limit?: { readonly requests_per_minute?: number; readonly compute_quota?: string };
}

/** openapi Diagnostic (validate/parse diagnostics; also a ValidationProblem member). */
export interface WireDiagnostic {
  readonly severity: "error" | "warning" | "hint";
  readonly code: string;
  readonly message: string;
  readonly span?: { readonly line?: number; readonly column?: number; readonly length?: number };
}

/** openapi ValidateResult (200 from POST /validate; pure — no Operation). */
export interface WireValidateResult {
  readonly valid: true;
  readonly artifact_id?: string;
  readonly diagnostics?: readonly WireDiagnostic[];
}

export type WireOperationStatus = "running" | "completed" | "suspended" | "failed";

/** openapi Operation. `cursor` is an opaque STRING; `status` (not `state`); the
 *  domain artifacts ride as opaque refs, the real payload arrives over SSE. */
export interface WireOperation {
  readonly operation_id: string;
  readonly intent_hash: string;
  readonly status: WireOperationStatus;
  readonly checkpoint?: string;
  readonly cursor: string;
  readonly artifacts?: readonly unknown[];
  readonly receipts?: readonly unknown[];
  readonly offer?: WireOffer | null;
  readonly continuation?: unknown | null;
}

/** openapi Money — a decimal string + asset code (NOT a bare number). */
export interface WireMoney {
  readonly value: string;
  readonly asset: string;
}

/** openapi SettlementMethod — discriminated on `method` (NOT `kind`). */
export interface WireSettlementMethod {
  readonly method: "stripe-mpp" | "x402";
  readonly network?: string;
  readonly accepts?: readonly string[];
  readonly params?: Record<string, unknown>;
}

/** openapi HumanAction — the contextual human path; `url` is DATA, never followed. */
export interface WireHumanAction {
  readonly kind: "claim-and-fund";
  readonly url: string;
  readonly required: boolean;
  readonly label?: string;
}

/** openapi Proof — the free result already earned at the sealed checkpoint. */
export interface WireProof {
  readonly proof_id: string;
  readonly operation_id: string;
  readonly intent_hash?: string;
  readonly artifacts?: readonly unknown[];
  readonly receipts?: readonly unknown[];
  readonly url?: string;
}

/** openapi Offer — `amount` is {@link WireMoney}; bound to exactly one Operation. */
export interface WireOffer {
  readonly offer_id: string;
  readonly operation_id: string;
  readonly intent_hash: string;
  readonly scope: readonly Scope[];
  readonly ceiling: number;
  readonly budget?: number;
  readonly amount: WireMoney;
  readonly terms_digest: string;
  readonly expiry: string;
  readonly nonce: string;
  readonly signature?: string;
  readonly requires_human?: boolean;
}

/** openapi OfferResponse — the STRUCTURED 402 body (§"The 402 Offer"). Every face
 *  surfaces it as DATA; none may silently redirect into a browser checkout. */
export interface WireOfferResponse {
  readonly operation_id: string;
  readonly offer: WireOffer;
  readonly proof?: WireProof | null;
  readonly settlement_methods: readonly WireSettlementMethod[];
  readonly human_action?: WireHumanAction;
}

/** openapi OperationEvent — the JSON `data:` payload of an SSE frame; `cursor`
 *  echoes the SSE `id:`; the opaque domain payload rides in `data`. */
export interface WireOperationEvent {
  readonly cursor: string;
  readonly type: SseEventType;
  readonly operation_id: string;
  readonly data?: Record<string, unknown>;
}

/**
 * openapi Operation.resume (ResumeAffordance) — the self-describing next call that drives a
 * NON-TERMINAL Operation forward (bead kestrel-markets-770.14, ADR-0009). It names the SAME
 * canonical `POST /api/simulate` the caller made, re-driving the SAME durable Operation from
 * `cursor` without repeating completed work. `requires_settlement:true` marks the SPEND
 * BOUNDARY (Operation `status:"suspended"`): the attached `offer` must be settled before the
 * resume POST. `detail` is the actionable protocol note the platform ships inline.
 */
export interface WireResumeAffordance {
  readonly method: "POST";
  readonly path: string;
  readonly operation_id: string;
  readonly cursor: string;
  readonly requires_settlement: boolean;
  readonly detail: string;
}

/**
 * openapi SettlementAffordance — the machine-followable SETTLE rail an Offer advertises
 * (bead kestrel-markets-1gr8, PR #334). Embedded in the 402 OfferResponse and, from
 * wave-3, in the suspended `SimOperationResponse` too: the EXACT wired URL an agent POSTs
 * settlement evidence to (`POST /api/operations/{id}/settlement`), the method, the accepted
 * rails, and the `SettlementRequest` body shape. This is "where to actually pay" — distinct
 * from the {@link WireResumeAffordance} `resume` (which re-drives `/api/simulate` AFTER the
 * Offer is settled). Optional on the wire: a response minted before the rail was wired
 * carries none, so the CLI degrades to the resume affordance rather than fabricating a URL.
 */
export interface WireSettlementAffordance {
  readonly method: "POST";
  readonly url: string;
  readonly accepts?: readonly string[];
  readonly body_schema?: {
    readonly offer_id: string;
    readonly method: string;
    readonly evidence: { readonly provider_payment_id: string };
  };
  readonly detail?: string;
}

/* ─────────────────────────── decoders (pure) ───────────────────────────── */

/** Decode a wire {@link WireTrialCapability} into the OSS {@link TrialCapability}.
 *  The bearer token (`capability`) becomes the OSS `capabilityId` (the capability id
 *  IS the bearer for a trial cap); `rate_limit.requests_per_minute` → the OSS
 *  requests-per-window shape. */
export function decodeTrialCapability(w: WireTrialCapability): TrialCapability {
  return {
    kind: "trial",
    capabilityId: w.capability,
    scopes: w.scopes,
    expiry: w.expiry,
    subjectCommitment: w.subject_commitment,
    rateLimit: { limit: w.rate_limit?.requests_per_minute ?? 0, windowSeconds: 60 },
  };
}

/** Map a wire operation `status` onto the OSS {@link OperationState}. */
function decodeStatus(w: WireOperation): OperationState {
  switch (w.status) {
    case "running":
      return "open";
    case "suspended":
      return w.offer?.requires_human ? "awaiting-human" : "awaiting-settlement";
    case "completed":
      return "completed";
    case "failed":
      return "failed";
    default: {
      // fail-closed: an unmodelled status is a protocol drift, not a silent default.
      const never: never = w.status;
      throw new Error(`wire: unknown Operation.status ${JSON.stringify(never)}`);
    }
  }
}

/** Decode a wire {@link WireOperation} into the OSS {@link Operation}. The opaque
 *  wire `cursor` string is carried verbatim in `EventCursor.token` (its `sequence`
 *  is not on the wire — the cursor is an opaque bookmark, handed back by string). */
export function decodeOperation(w: WireOperation): Operation {
  return {
    operationId: w.operation_id,
    intentHash: w.intent_hash,
    state: decodeStatus(w),
    cursor: { sequence: 0, token: w.cursor },
    ...(w.checkpoint !== undefined ? { checkpoint: w.checkpoint } : {}),
    authorityEpoch: 0,
    resumeGeneration: 0,
  };
}

/**
 * Fail-closed structural check that `v` is a SETTLEABLE {@link WireOffer} — a full,
 * platform-signed Offer carrying terms (an `offer_id`, a non-empty `scope[]`, a finite
 * `ceiling`, and a {@link WireMoney} `amount`), NOT the degraded `{offer_id, operation_id}`
 * boundary fallback the platform emits when no signing key is wired (session.ts@toResponse).
 * The bare fallback carries no itemized terms to surface as data, so it returns `null` and the
 * caller keeps its existing typed behavior — never a silent default, never a guessed Offer.
 */
export function asWireOffer(v: unknown): WireOffer | null {
  if (typeof v !== "object" || v === null) return null;
  const o = v as Record<string, unknown>;
  if (typeof o["offer_id"] !== "string" || typeof o["operation_id"] !== "string") return null;
  if (!Array.isArray(o["scope"]) || o["scope"].length === 0) return null;
  if (typeof o["ceiling"] !== "number" || !Number.isFinite(o["ceiling"])) return null;
  const amount = o["amount"];
  if (typeof amount !== "object" || amount === null || !("value" in amount) || !("asset" in amount)) return null;
  return v as WireOffer;
}

/**
 * Fail-closed check that `v` is a SETTLEMENT-GATED {@link WireResumeAffordance} bound to
 * `operationId`: the canonical `POST /api/simulate` re-drive with `requires_settlement:true`.
 * Anything else (a non-canonical path, a foreign operation, `requires_settlement` other than
 * `true`) returns `null` — the Spend-boundary shape is recognized exactly, or not at all.
 */
export function asSettlementResume(v: unknown, operationId: string): WireResumeAffordance | null {
  if (typeof v !== "object" || v === null) return null;
  const r = v as Record<string, unknown>;
  if (r["method"] !== "POST" || r["path"] !== "/api/simulate") return null;
  if (r["operation_id"] !== operationId) return null;
  if (typeof r["cursor"] !== "string") return null;
  if (r["requires_settlement"] !== true) return null;
  if (typeof r["detail"] !== "string") return null;
  return v as WireResumeAffordance;
}

/**
 * Fail-closed check that `v` is a well-formed {@link WireSettlementAffordance}: the wired
 * settle rail an Offer advertises (`POST /api/operations/{id}/settlement`). Requires the two
 * load-bearing fields an agent needs to REACH the rail — `method:"POST"` and a non-empty
 * `url` — and shape-checks the optional descriptive fields when present. Returns `null` for a
 * response that carries no settle affordance (minted before the rail was wired, bead
 * kestrel-markets-1gr8), so the caller degrades to the resume affordance rather than printing
 * a fabricated URL — never a silent default.
 */
export function asSettlementAffordance(v: unknown): WireSettlementAffordance | null {
  if (typeof v !== "object" || v === null) return null;
  const s = v as Record<string, unknown>;
  if (s["method"] !== "POST") return null;
  if (typeof s["url"] !== "string" || s["url"].length === 0) return null;
  if (s["accepts"] !== undefined && !Array.isArray(s["accepts"])) return null;
  if (s["detail"] !== undefined && typeof s["detail"] !== "string") return null;
  return v as WireSettlementAffordance;
}

/** Fail-closed structural check that a decoded 402 body is a real OfferResponse
 *  (the required members per openapi OfferResponse). Returns the narrowed value or
 *  `null` — the caller turns `null` into a loud error, never a silent default. */
export function asOfferResponse(body: unknown): WireOfferResponse | null {
  if (typeof body !== "object" || body === null) return null;
  const o = body as Record<string, unknown>;
  const offer = o["offer"];
  if (typeof o["operation_id"] !== "string") return null;
  if (typeof offer !== "object" || offer === null) return null;
  const of = offer as Record<string, unknown>;
  const amount = of["amount"];
  if (typeof of["offer_id"] !== "string" || !Array.isArray(of["scope"])) return null;
  if (typeof amount !== "object" || amount === null || !("value" in amount) || !("asset" in amount)) return null;
  if (!Array.isArray(o["settlement_methods"])) return null;
  return body as WireOfferResponse;
}
