/**
 * # cli/backend/mock — the TRUTHFUL in-process contract server (kestrel-5rb).
 *
 * A `fetch(url, init) → Response` that serves the EXACT platform contract
 * (kestrel.markets): the M1 routes, **snake_case** wire bodies, the structured
 * 402/OfferResponse for the paid-dataset boundary, a canonical resumable SSE stream
 * whose events are the `operation.*` {@link ./wire.ts OperationEvent.type} vocabulary
 * with **opaque-STRING** `id:` cursors, and `Idempotency-Key` effect-once replay.
 * Inject into RemoteBackend (`{ baseUrl, fetch: mockFetch }`) → the whole CLI runs
 * in CI with no real API.
 *
 * It shares its wire shapes and SSE event names with {@link ./remote.ts} through
 * `wire.ts` — one definition of the dialect for client AND server — so this mock can
 * never again hand-roll a fabricated dialect that hides a divergence (the failure
 * mode kestrel-5rb was filed to kill). The route/shape/vocabulary conformance is
 * independently pinned to the platform OpenAPI fixture by
 * `tests/cli.remote-conformance.test.ts`.
 */

import type { GradeResult } from "../../protocol/index.ts";
import { PROTOCOL_VERSION } from "../../protocol/index.ts";
import type {
  SseEventType,
  WireTrialCapability,
  WireValidateResult,
  WireOperation,
  WireOfferResponse,
  WireOperationEvent,
} from "./wire.ts";
// kestrel-djm.5: the BAKED catalog-conformant records this server ALSO serves so the SDK's HTTP transport
// returns the SAME protocol Blotter / transcript / grade / root the LOCAL projection yields (light: pure
// data, no engine — the server stays on the remote graph, tests/package.boundary.test.ts).
import { CATALOG_ENTRIES, CATALOG_RECORDS, SESSION_TO_ENTRY, type CatalogSessionRecord } from "./catalog-records.ts";
// kestrel-dnxq: the light contract server serves `GET /catalog` in the DEPLOYED snake_case listing dialect
// (`{ entries: HostedCatalogEntry[], version }`) — NOT the old bare camelCase CatalogEntry[] that hid the
// remote-vs-local divergence (kestrel-y9v1/uasi). Built through the SAME shared listing constructor the LOCAL
// transport uses, so `remoteTransport`'s decode reconstructs the byte-identical canonical CatalogListing.
import { buildCatalogListing, toCatalogListingWire } from "../../catalog/catalog-listing.ts";

const TRIAL: WireTrialCapability = {
  // `capability` IS the bearer token (openapi TrialCapability). Scopes exclude
  // paper/broker/live — the trial is a subset of the wallet-signable set (ADR-0002).
  capability: "cap_trial_mock_0001",
  subject_commitment: "sc_ephemeral_mock",
  expiry: "2099-01-01T00:00:00Z",
  scopes: ["data", "sim", "grade"],
  rate_limit: { requests_per_minute: 100, compute_quota: "trial" },
};

/** The `GET /catalog` body the light server serves — the deployed `{ entries, version }` snake_case listing
 *  wire, one row per baked catalog entry, built once for byte-stability through the SAME shared listing
 *  constructor the LOCAL transport uses (kestrel-dnxq). The `version` is a fixed content tag (the fixture is
 *  static); a self-hosted mirror may serve any opaque version — the client never interprets it. */
const CATALOG_LISTING_BODY = {
  entries: buildCatalogListing(CATALOG_ENTRIES.map((e) => e.id)).map(toCatalogListingWire),
  version: "cat_mock_1",
};

const SIM_OP_ID = "op_sim_1";
const GRADE_OP_ID = "op_grade_1";

function simOperation(): WireOperation {
  return { operation_id: SIM_OP_ID, intent_hash: "ih_" + SIM_OP_ID, status: "running", cursor: "c0", artifacts: [], receipts: [] };
}
function gradeOperation(): WireOperation {
  return { operation_id: GRADE_OP_ID, intent_hash: "ih_" + GRADE_OP_ID, status: "running", cursor: "c0", artifacts: [], receipts: [] };
}

/** A running Operation whose id ENCODES a catalog entry (kestrel-djm.5) — its events serve the baked record. */
function catalogOperation(opId: string): WireOperation {
  return { operation_id: opId, intent_hash: "ih_" + opId, status: "running", cursor: "c0", artifacts: [], receipts: [] };
}

/** The paid boundary is a property of the DATASET (§4 Sim): a free-catalog artifact
 *  runs free; anything outside it triggers the 402. (The client sends no `scope`.) */
function isPaidDataset(artifactId: unknown): boolean {
  return typeof artifactId === "string" && /premium|paid|out-of-catalog/.test(artifactId);
}

// kestrel-djm.5 — catalog-conformant serving. Operation ids ENCODE the catalog entry so the events endpoint
// serves the right baked record on resume (the op id is the durable handle, ADR-0004).
const SIM_CAT_PREFIX = "op_sim_cat_";
const GRADE_CAT_PREFIX = "op_grade_cat_";

/** The baked catalog entry id a `POST /sim` dataset addresses, or `null` when the dataset is not a
 *  catalog-Session subject (a non-baked id / a raw dataset — those fall through to the generic mock behavior,
 *  unchanged). Accepts the BARE `<id>` handle — the dialect the deployed server speaks and the one the SDK's
 *  `remoteTransport` now sends after kestrel-p6hz dropped the client-invented `catalog:` prefix — AND the
 *  legacy `catalog:<id>` form, so both the new bare path and any prefix-passing caller resolve identically. */
function catalogSimEntry(artifactId: unknown): string | null {
  if (typeof artifactId !== "string") return null;
  const id = artifactId.startsWith("catalog:") ? artifactId.slice("catalog:".length) : artifactId;
  return Object.prototype.hasOwnProperty.call(CATALOG_RECORDS, id) ? id : null;
}

/** Resolve a baked Session record by its graded-Session id — the SDK's durable Operation/artifact handle
 *  (src/sdk/local.ts models an Operation id as the finalized Session id). `undefined` when unknown. */
function recordBySession(sessionId: string): CatalogSessionRecord | undefined {
  const entryId = SESSION_TO_ENTRY[sessionId];
  return entryId !== undefined ? CATALOG_RECORDS[entryId] : undefined;
}

/** Resolve a baked Session record by its conformance root (the graded-bus sha256). `undefined` when unknown. */
function recordByRoot(root: string): CatalogSessionRecord | undefined {
  return Object.values(CATALOG_RECORDS).find((r) => r.conformanceRoot === root);
}

function offerResponse(): WireOfferResponse {
  return {
    operation_id: SIM_OP_ID,
    offer: {
      offer_id: "of_mock_paper_01",
      operation_id: SIM_OP_ID,
      intent_hash: "ih_" + SIM_OP_ID, // the canonical intent this Offer prices
      scope: ["paper"], // out-of-trial scope → must convert
      ceiling: 500,
      amount: { value: "25.00", asset: "USDC" }, // Money = { value, asset }
      terms_digest: "sha256:termsdigest",
      expiry: "2099-01-01T00:00:00Z",
      nonce: "nonce_off_01",
      requires_human: false,
    },
    proof: {
      proof_id: "pf_" + SIM_OP_ID,
      operation_id: SIM_OP_ID,
      intent_hash: "ih_" + SIM_OP_ID,
      artifacts: [],
      url: "https://kestrel.markets/proof/" + SIM_OP_ID,
    },
    settlement_methods: [
      { method: "stripe-mpp", network: "tempo", accepts: ["USDC"] }, // MPP first (DESIGN §5)
      { method: "x402", network: "base", accepts: ["USDC"] },
    ],
    human_action: {
      kind: "claim-and-fund",
      url: "https://kestrel.markets/claim/" + SIM_OP_ID,
      required: false,
      label: "Send to your human to claim & fund",
    },
  };
}

const REPORT = {
  session: {
    date: "2024-05-07",
    instruments: ["SPY"],
    mode: "sim",
    fill_model: "maker-fair-v1",
    bus_events: 42,
    determinism_hash: "dh_mock",
    bus_sha256: "bs_mock",
    reprice_count: 0,
    settle_ts: 1715112000000,
  },
  plans: [],
  orders: [],
  totals: { realized_floor_usd: 27, expected_usd: 30, premium_spent: 12 },
  emitted: { PLAN: 1, ORDER: 2 },
};

const SAMPLE_BLOTTER = {
  sessionId: "bl_mock_1",
  orders: [],
  fills: [],
  positions: [],
  settlement: { realized: 0, asset: "usd", settledAt: "2024-05-07T20:00:00Z" },
};

const GRADE: GradeResult = { subjectSessionId: "dh_mock", metrics: { bankable_ev: 27.3, fill_support: 0.61 } };

/** client-facing idempotency ledger: Idempotency-Key → whether it has been seen. */
const seenIdempotencyKeys = new Set<string>();

/** The contract fetch handler. Pass to RemoteBackend as `fetch`. */
export function mockFetch(url: string, init?: RequestInit): Promise<Response> {
  const u = new URL(url);
  const p = u.pathname;
  const method = (init?.method ?? "GET").toUpperCase();
  const body: Record<string, unknown> = init?.body ? (JSON.parse(String(init.body)) as Record<string, unknown>) : {};
  const headers = lowerHeaders(init?.headers);

  const json = (o: unknown, status = 200, extra: Record<string, string> = {}): Response =>
    new Response(JSON.stringify(o), { status, headers: { "content-type": "application/json", ...extra } });

  // effect-once: a replayed Idempotency-Key returns the prior result marked replayed.
  const replay = (): Record<string, string> => {
    const key = headers["idempotency-key"];
    if (key === undefined) return {};
    if (seenIdempotencyKeys.has(key)) return { "idempotent-replay": "true" };
    seenIdempotencyKeys.add(key);
    return {};
  };

  // ── capability-discovery: POST /capabilities/trial ──
  if (p === "/capabilities/trial" && method === "POST") return Promise.resolve(json(TRIAL, 201, replay()));

  // ── validate: POST /validate (pure; no Operation, no idempotency) ──
  if (p === "/validate" && method === "POST") {
    const r: WireValidateResult = { valid: true, artifact_id: "art_plan_1", diagnostics: [] };
    return Promise.resolve(json(r, 200));
  }

  // ── catalog: GET /catalog → the deployed snake_case listing wire (kestrel-dnxq; public, no auth) ──
  if (p === "/catalog" && method === "GET") return Promise.resolve(json(CATALOG_LISTING_BODY, 200));

  // ── sim: POST /sim → 201 Operation (direct) | 402 OfferResponse (paid dataset) ──
  if (p === "/sim" && method === "POST") {
    const dataset = body["dataset"] as { artifact_id?: unknown } | undefined;
    // kestrel-djm.5: a real catalog-Session subject → an Operation whose id encodes the entry (its events
    // serve the baked catalog record). Non-catalog datasets are unchanged (paid → 402; else generic sim).
    const catEntry = catalogSimEntry(dataset?.artifact_id);
    if (catEntry !== null) return Promise.resolve(json(catalogOperation(SIM_CAT_PREFIX + catEntry), 201, replay()));
    if (isPaidDataset(dataset?.artifact_id)) return Promise.resolve(json(offerResponse(), 402));
    return Promise.resolve(json(simOperation(), 201, replay()));
  }

  // ── grade: POST /grade → 201 Operation ──
  if (p === "/grade" && method === "POST") {
    // kestrel-djm.5: grading a baked catalog Blotter (its sessionId) → an Operation whose id encodes the
    // entry (its events serve the baked grade). A generic Blotter id → the generic grade, unchanged.
    const blotters = body["blotters"] as unknown[] | undefined;
    const b0 = blotters?.[0];
    const catId = typeof b0 === "string" ? SESSION_TO_ENTRY[b0] : undefined;
    if (catId !== undefined) return Promise.resolve(json(catalogOperation(GRADE_CAT_PREFIX + catId), 201, replay()));
    return Promise.resolve(json(gradeOperation(), 201, replay()));
  }

  // ── artifacts: GET /artifacts/{ref} (kestrel-djm.5) ──
  // Resolve a content-addressed artifact ref to the SAME { ref, kind, value } the LOCAL projection returns
  // (src/sdk/local.ts artifact()): `blotter:<sessionId>` → the protocol Blotter; `sha256:<root>` → the
  // conformance root. So the SDK's HTTP transport returns a byte-identical ArtifactResult to LOCAL. An
  // unresolvable ref falls through to the hard 404 below (fail-closed, never a fabricated payload).
  const artMatch = p.match(/^\/artifacts\/([^/]+)$/);
  if (artMatch && method === "GET") {
    const ref = decodeURIComponent(artMatch[1]!);
    if (ref.startsWith("blotter:")) {
      const rec = recordBySession(ref.slice("blotter:".length));
      if (rec !== undefined) return Promise.resolve(json({ ref, kind: "blotter", value: rec.blotter }, 200));
    } else if (ref.startsWith("sha256:")) {
      const rec = recordByRoot(ref.slice("sha256:".length));
      if (rec !== undefined) return Promise.resolve(json({ ref, kind: "conformance-root", value: rec.conformanceRoot }, 200));
    }
    // unresolvable artifact ref → fall through to the hard 404 below
  }

  // ── operations: GET /operations/{id} — the STATUS envelope ONLY ──
  // kestrel-o3bi: the plain operation resource carries the operation's control-plane STATUS, NOT its domain
  // OUTCOME. A COMPLETED operation returns { operation_id, status, cursor } with NO `payload` — the outcome
  // (blotter / report / receipt + artifact refs) is carried on the RESUMABLE EVENT STREAM below
  // (`GET /operations/{id}/events`), the SAME stream sim/grade consume. This reflects production: the earlier
  // mock FABRICATED a `payload:{ blotter, grade, artifacts }` here that the real endpoint never serves, which
  // masked the empty-payload resume defect a wire-first integrator hit. `resumeOperation` now reads the event
  // stream, so this endpoint is deliberately payload-free — a client that reads it for the outcome gets {}
  // (the decisive delete-the-call-site probe: revert the transport to this endpoint → empty payload → RED).
  const opMatch = p.match(/^\/operations\/([^/]+)$/);
  if (opMatch && method === "GET") {
    const id = decodeURIComponent(opMatch[1]!);
    const rec = recordBySession(id);
    if (rec !== undefined) {
      // A finalized durable operation: COMPLETED, but the outcome lives on /events (no payload here).
      return Promise.resolve(json({ operation_id: id, status: "completed", cursor: null }, 200));
    }
    const op = id === GRADE_OP_ID ? gradeOperation() : simOperation();
    return Promise.resolve(json({ ...op, status: "completed", cursor: "c3" }, 200));
  }

  // ── events: GET /operations/{id}/events (resumable SSE) ──
  const evMatch = p.match(/^\/operations\/([^/]+)\/events$/);
  if (evMatch && method === "GET") {
    const id = decodeURIComponent(evMatch[1]!);
    const cursor = u.searchParams.get("cursor") ?? headers["last-event-id"] ?? null;
    // kestrel-o3bi: RESUME a finalized durable operation by its Session-id handle — replay the OUTCOME
    // ({ blotter, grade, artifacts }) on the resumable stream, byte-identical to the LOCAL projection. This is
    // the stream `resumeOperation` reads; a COMPLETED operation returns its full outcome here (the plain
    // status endpoint above is payload-free), closing the wire read-back the batch redirect points at.
    const finalizedRec = recordBySession(id);
    if (finalizedRec !== undefined) return Promise.resolve(resumptionSse(id, finalizedRec, cursor));
    // kestrel-djm.5: a catalog Operation streams the baked record (sim) or the baked grade (grade).
    if (id.startsWith(SIM_CAT_PREFIX)) {
      const rec = CATALOG_RECORDS[id.slice(SIM_CAT_PREFIX.length)];
      if (rec !== undefined) return Promise.resolve(catalogSse(id, rec, "sim", cursor));
    }
    if (id.startsWith(GRADE_CAT_PREFIX)) {
      const rec = CATALOG_RECORDS[id.slice(GRADE_CAT_PREFIX.length)];
      if (rec !== undefined) return Promise.resolve(catalogSse(id, rec, "grade", cursor));
    }
    return Promise.resolve(sseResponse(id, cursor));
  }

  // fail closed: an off-contract route is a hard 404 (problem+json), never a lie.
  return Promise.resolve(
    new Response(JSON.stringify({ type: "about:blank", title: "not_found", status: 404, code: "not_found" }), {
      status: 404,
      headers: { "content-type": "application/problem+json" },
    }),
  );
}

/** A canonical SSE stream (§9 Events + §"Cursor-based SSE resume"): `id:` is an
 *  OPAQUE STRING; each `event:` is an OperationEvent.type; the domain payload rides
 *  opaquely in the OperationEvent `data`; terminal is `operation.completed`. A client
 *  re-opening at cursor `c2` gets only the tail (resume). */
function sseResponse(opId: string, resumeFrom: string | null): Response {
  const isGrade = opId === GRADE_OP_ID;
  const artifactType: SseEventType = isGrade ? "operation.receipt" : "operation.artifact";
  const artifactData: Record<string, unknown> = isGrade
    ? { artifact: { artifact_id: "gr_mock_1", kind: "grade" }, grade: GRADE }
    : { artifact: { artifact_id: SAMPLE_BLOTTER.sessionId, kind: "blotter" }, report: REPORT, blotter: SAMPLE_BLOTTER, wakes: [] };

  const all: { id: string; type: SseEventType; data: Record<string, unknown> }[] = [
    { id: "c1", type: "operation.started", data: {} },
    { id: "c2", type: artifactType, data: artifactData },
    { id: "c3", type: "operation.completed", data: {} },
  ];
  // resume: replay only events strictly AFTER the given opaque cursor.
  const start = resumeFrom ? all.findIndex((e) => e.id === resumeFrom) + 1 : 0;
  const emit = all.slice(Math.max(0, start));

  const frames = emit.map((e) => {
    const env: WireOperationEvent = { cursor: e.id, type: e.type, operation_id: opId, data: e.data };
    return `id: ${e.id}\nevent: ${e.type}\ndata: ${JSON.stringify(env)}\n\n`;
  });
  const stream = new ReadableStream<Uint8Array>({
    start(c) {
      const enc = new TextEncoder();
      for (const f of frames) c.enqueue(enc.encode(f));
      c.close();
    },
  });
  return new Response(stream, { status: 200, headers: { "content-type": "text/event-stream" } });
}

/**
 * The catalog-conformant SSE stream (kestrel-djm.5): the SAME resumable framing as {@link sseResponse}, but
 * the artifact/receipt frame carries the BAKED record — for `sim`, the protocol Blotter PLUS the full
 * server-computed Session record (`session`: id + pentad-chained entries + delivered frames + tip + root +
 * artifacts) the HTTP transport replays; for `grade`, the baked portable GradeResult. So the HTTP transport
 * returns byte-identical protocol objects to the LOCAL projection (the djm.5 headline).
 */
function catalogSse(opId: string, record: CatalogSessionRecord, mode: "sim" | "grade", resumeFrom: string | null): Response {
  const artifactType: SseEventType = mode === "grade" ? "operation.receipt" : "operation.artifact";
  const artifactData: Record<string, unknown> =
    mode === "grade"
      ? { artifact: { artifact_id: `gr_cat_${record.entryId}`, kind: "grade" }, grade: record.grade }
      : {
          artifact: { artifact_id: record.blotter.sessionId, kind: "blotter" },
          blotter: record.blotter,
          session: {
            sessionId: record.sessionId,
            blotter: record.blotter,
            entries: record.entries,
            frames: record.frames,
            tipHash: record.tipHash,
            conformanceRoot: record.conformanceRoot,
            artifacts: record.artifacts,
          },
        };

  const all: { id: string; type: SseEventType; data: Record<string, unknown> }[] = [
    { id: "c1", type: "operation.started", data: {} },
    { id: "c2", type: artifactType, data: artifactData },
    { id: "c3", type: "operation.completed", data: {} },
  ];
  const start = resumeFrom ? all.findIndex((e) => e.id === resumeFrom) + 1 : 0;
  const emit = all.slice(Math.max(0, start));
  const frames = emit.map((e) => {
    const env: WireOperationEvent = { cursor: e.id, type: e.type, operation_id: opId, data: e.data };
    return `id: ${e.id}\nevent: ${e.type}\ndata: ${JSON.stringify(env)}\n\n`;
  });
  const stream = new ReadableStream<Uint8Array>({
    start(c) {
      const enc = new TextEncoder();
      for (const f of frames) c.enqueue(enc.encode(f));
      c.close();
    },
  });
  return new Response(stream, { status: 200, headers: { "content-type": "text/event-stream" } });
}

/**
 * The RESUME stream for a FINALIZED durable operation (kestrel-o3bi): the SAME resumable framing as
 * {@link sseResponse}, whose single `operation.artifact` frame carries the operation's OUTCOME —
 * `{ blotter, grade, artifacts }` — so a client that replays this stream (`resumeOperation`) accumulates
 * exactly the payload the LOCAL projection returns (src/sdk/local.ts resumeOperation). This is the stream the
 * outcome is served on; the plain `GET /operations/{id}` status endpoint is payload-free.
 */
function resumptionSse(opId: string, record: CatalogSessionRecord, resumeFrom: string | null): Response {
  const all: { id: string; type: SseEventType; data: Record<string, unknown> }[] = [
    { id: "c1", type: "operation.started", data: {} },
    { id: "c2", type: "operation.artifact", data: { blotter: record.blotter, grade: record.grade, artifacts: record.artifacts } },
    { id: "c3", type: "operation.completed", data: {} },
  ];
  const start = resumeFrom ? all.findIndex((e) => e.id === resumeFrom) + 1 : 0;
  const emit = all.slice(Math.max(0, start));
  const frames = emit.map((e) => {
    const env: WireOperationEvent = { cursor: e.id, type: e.type, operation_id: opId, data: e.data };
    return `id: ${e.id}\nevent: ${e.type}\ndata: ${JSON.stringify(env)}\n\n`;
  });
  const stream = new ReadableStream<Uint8Array>({
    start(c) {
      const enc = new TextEncoder();
      for (const f of frames) c.enqueue(enc.encode(f));
      c.close();
    },
  });
  return new Response(stream, { status: 200, headers: { "content-type": "text/event-stream" } });
}

function lowerHeaders(h: RequestInit["headers"]): Record<string, string> {
  const out: Record<string, string> = {};
  if (!h) return out;
  if (h instanceof Headers) {
    h.forEach((v, k) => (out[k.toLowerCase()] = v));
  } else if (Array.isArray(h)) {
    for (const pair of h) {
      const k = pair[0];
      if (k !== undefined) out[k.toLowerCase()] = String(pair[1] ?? "");
    }
  } else {
    for (const [k, v] of Object.entries(h)) out[k.toLowerCase()] = String(v);
  }
  return out;
}

export const CONTRACT_PROTOCOL_VERSION = PROTOCOL_VERSION;
