/**
 * # sdk/local — the LOCAL transport: the in-process djm.4 controller + djm.8 catalog + engine projection
 *
 * The HEAVY transport (kestrel-djm.5). `localTransport()` binds, IN-PROCESS: the djm.8 public Session
 * catalog (catalog discovery + subject resolution), the engine parser (validation), the djm.4 incremental
 * controller (the real Session progression — Bus → Blotter → graded bus), and a deterministic projection of
 * the rich engine {@link EngineBlotter} into the djm.2 PROTOCOL {@link Blotter} + portable {@link GradeResult}
 * this SDK returns. It NEVER forks Session progression — a {@link LocalSession} is a thin wrapper over the
 * djm.4 {@link SessionController}; finalize projects, registers, and seals.
 *
 * ── DESIGN FORK LOGGED (hard rules forbid .beads writes) — the engine→protocol Blotter PROJECTION ──
 * djm.2's {@link Blotter} is the GENERIC public view ("generic instruments only"); the engine Blotter is the
 * rich internal record (per-order lifecycle, fill support, settle-mark provenance). The MINIMAL, HONEST,
 * DETERMINISTIC projection this SDK adopts ({@link toProtocolBlotter}): carry the deterministic Session
 * identity and the settlement (realized floor + asset + settle time, all fully determined by the graded
 * bus), and leave the generic `orders`/`fills`/`positions` arrays EMPTY — the engine `OrderRecord` lacks a
 * contract expiry and generic per-order ISO timestamps, and the protocol option {@link import("../protocol/index.ts").Instrument}
 * REQUIRES an expiry, so fabricating one would violate the never-fabricate non-negotiable (AGENTS.md). The
 * full per-order detail stays on the rich engine Blotter (the LOCAL path exposes it); the managed backend's
 * own richer server-side projection is out of djm.5's scope. Both transports agree BY CONSTRUCTION: the HTTP
 * transport replays this SAME projection's output (baked from a LOCAL run — see scripts/sdk/bake-catalog-records.ts).
 *
 * This is the DEFAULT, heaviest surface — it pulls the engine aggregate; a remote-only consumer imports
 * `remote.ts` (light) instead (the djm.3 light/heavy seam, pinned by tests/package.boundary.test.ts).
 */

import type { Blotter as EngineBlotter } from "../blotter/index.ts";
// The pure engine→protocol Blotter projection now lives in a LIGHT blotter module (kestrel-b0li) so the
// node-only `certify` verb can grade a re-projection without dragging `lang`/`session` in; re-exported
// here so existing `from "../sdk"` importers keep working unchanged.
import { toProtocolBlotter } from "../blotter/protocol-view.ts";
export { toProtocolBlotter } from "../blotter/protocol-view.ts";
import { KestrelParseError, parse } from "../lang/index.ts";
import { grade, type GradeResult } from "../protocol/index.ts";
import type { SessionId } from "../protocol/session.ts";
import {
  BACKTEST_CONFIG,
  openSession as openController,
  type FillModelName,
  type SessionController,
  type SessionFinalized,
} from "../session/index.ts";
import {
  SESSION_CATALOG,
  catalogListing,
  loadSessionCatalog,
  resolveTapeEvents,
} from "../catalog/session-catalog.ts";

import type {
  ArtifactResult,
  AuthoredResponse,
  BoundResponse,
  Delivery,
  GradeRequest,
  KestrelSession,
  OperationRef,
  OperationResumption,
  SdkFinalized,
  SessionResponse,
  SessionTranscript,
  Transport,
  ValidateOutcome,
  Gated,
  GradeOutcome,
} from "./types.ts";
import { SDK_VERSION } from "./facade.ts";

/** A typed, fail-closed refusal from the LOCAL transport (agents match on `code`, not prose). Never a
 * `RangeError` / silent load — an unknown subject/artifact is a stated, typed protocol refusal. */
export class SdkLocalError extends Error {
  override readonly name = "SdkLocalError";
  readonly code: string;
  constructor(code: string, message: string) {
    super(message);
    this.code = code;
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// The engine → protocol projection (pure, deterministic — the ONE source of truth both transports agree on)
// ─────────────────────────────────────────────────────────────────────────────

/** The conformance root of a run = the graded-bus sha256 (`SimRunId`), a pure function of the graded bus. */
export function conformanceRootOf(b: EngineBlotter): string {
  return b.session.bus.sha256;
}

/**
 * Derive the portable {@link GradeResult} for a finalized run — the SAME OSS {@link grade} Judge an outsider
 * runs, applied to the projected protocol Blotter (kestrel-h314 D4: `grade` REPLACES the old bespoke
 * `gradeOf`). Both metric families come out of the certified evidence carried on the wire; grading one
 * Blotter yields a GradeResult whose `subjectSessionId` is that session's id. Deterministic; both transports
 * agree by construction.
 */
export function gradeOf(sessionId: SessionId, b: EngineBlotter): GradeResult {
  return grade([toProtocolBlotter(sessionId, b)]);
}

/** The content-addressed artifact handles a finalized Session delivers: the graded-bus root + the Blotter
 * artifact id (the Blotter's `sessionId` IS its artifact id). Deterministic; identical on both transports. */
export function artifactsOf(sessionId: SessionId, conformanceRoot: string): readonly string[] {
  return [`sha256:${conformanceRoot}`, `blotter:${sessionId}`];
}

// ─────────────────────────────────────────────────────────────────────────────
// The transport registry — finalized runs cached for grade / artifact / operation resume
// ─────────────────────────────────────────────────────────────────────────────

interface FinalizedRecord {
  readonly finalized: SdkFinalized;
  readonly grade: GradeResult;
  readonly engineBlotter: EngineBlotter;
}

type Register = (id: SessionId, record: FinalizedRecord) => void;

// ─────────────────────────────────────────────────────────────────────────────
// LocalSession — a thin wrapper over the djm.4 controller (never a forked progression)
// ─────────────────────────────────────────────────────────────────────────────

class LocalSession implements KestrelSession {
  readonly sessionId: SessionId;

  constructor(
    private readonly ctrl: SessionController,
    private readonly register: Register,
  ) {
    this.sessionId = ctrl.sessionId;
  }

  start(): Promise<Delivery> {
    return this.ctrl.start();
  }

  advance(response: AuthoredResponse): Promise<SessionResponse> {
    return this.ctrl.advance(response);
  }

  revise(response: AuthoredResponse): Promise<SessionResponse> {
    return this.ctrl.revise(response);
  }

  submit(response: BoundResponse): Promise<SessionResponse> {
    return this.ctrl.submit(response);
  }

  async resume(after?: Parameters<SessionController["resume"]>[0]): Promise<SessionTranscript> {
    return this.ctrl.resume(after);
  }

  async finalize(): Promise<SdkFinalized> {
    const fin: SessionFinalized = await this.ctrl.finalize();
    const conformanceRoot = conformanceRootOf(fin.blotter);
    const blotter = toProtocolBlotter(fin.sessionId, fin.blotter);
    const artifacts = artifactsOf(fin.sessionId, conformanceRoot);
    const finalized: SdkFinalized = {
      blotter,
      sessionId: fin.sessionId,
      tipHash: fin.tipHash,
      conformanceRoot,
      artifacts,
    };
    const grade = gradeOf(fin.sessionId, fin.blotter);
    this.register(fin.sessionId, { finalized, grade, engineBlotter: fin.blotter });
    return finalized;
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// The LOCAL transport factory
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Build the LOCAL transport — the in-process djm.4 controller + djm.8 catalog + engine projection. The
 * caller wraps it with `createSdk(localTransport())`. It never gates (the free in-process path invents no
 * 402); an unknown subject / artifact / operation is a typed {@link SdkLocalError} (fail-closed).
 */
export function localTransport(): Transport {
  const registry = new Map<string, FinalizedRecord>();
  const register: Register = (id, record) => registry.set(id, record);

  return {
    kind: "local",
    version: `${SDK_VERSION}+local`,

    async catalog() {
      // The catalog DISCOVERY projection as a canonical CatalogPage (kestrel-adge): the byte-identical
      // CatalogListing[] (kestrel-dnxq) the offline in-process sample lists, wrapped in `{ entries }`. The
      // LOCAL transport is a self-hosted mirror with NO price sheet, so it serves NO `pricing` block —
      // `pricing` is absent (a bare page), which is exactly the backward-compatible "today's listing" case.
      // The reproducibility-complete CatalogEntry surface resolves at openSession, not here.
      return { entries: catalogListing() };
    },

    async validate(document) {
      try {
        parse(document);
        return { ok: true, diagnostics: [] } satisfies ValidateOutcome;
      } catch (e) {
        if (e instanceof KestrelParseError) {
          return { ok: false, diagnostics: [e.message] } satisfies ValidateOutcome;
        }
        throw e;
      }
    },

    // The optional `document` (the customer strategy the HTTP transport threads to POST /sim, kestrel-88u2)
    // is accepted for signature parity but IGNORED here: the LOCAL catalog subject already carries its pinned
    // recipe's strategy (`entry.recipe.document`), so there is no author-no-strategy fence to satisfy.
    async openSession(subject, _document): Promise<Gated<KestrelSession>> {
      if (subject.kind !== "catalog") {
        throw new SdkLocalError(
          "unsupported-subject",
          "the LOCAL transport runs catalog subjects; a raw dataset needs the managed backend (HTTP transport)",
        );
      }
      const entries = loadSessionCatalog(SESSION_CATALOG);
      const entry = entries.find((e) => e.id === subject.id);
      if (entry === undefined) {
        throw new SdkLocalError("unknown-subject", `no catalog entry ${JSON.stringify(subject.id)}`);
      }
      const events = resolveTapeEvents(entry);
      const ctrl = openController({
        events,
        config: BACKTEST_CONFIG,
        wakes: entry.recipe.wakes,
        fillModel: entry.entry.permittedFillModel as FillModelName,
        rUsd: entry.recipe.rUsd,
        fairTauYears: entry.recipe.fairTauYears,
      });
      return { gated: false, value: new LocalSession(ctrl, register) };
    },

    async grade(request: GradeRequest): Promise<Gated<GradeOutcome>> {
      const id = request.blotters[0];
      if (id === undefined) throw new SdkLocalError("empty-grade", "grade requires at least one Blotter id");
      const record = registry.get(id);
      if (record === undefined) {
        throw new SdkLocalError("unknown-artifact", `no finalized Blotter for ${JSON.stringify(id)} — finalize the Session first`);
      }
      return { gated: false, value: record.grade };
    },

    async artifact(ref: string): Promise<ArtifactResult> {
      for (const record of registry.values()) {
        if (record.finalized.artifacts.includes(ref)) {
          const isBlotter = ref.startsWith("blotter:");
          return {
            ref,
            kind: isBlotter ? "blotter" : "conformance-root",
            value: isBlotter ? record.finalized.blotter : record.finalized.conformanceRoot,
          };
        }
      }
      throw new SdkLocalError("unknown-artifact", `artifact ${JSON.stringify(ref)} not found`);
    },

    async resumeOperation(ref: OperationRef): Promise<OperationResumption> {
      // The LOCAL path is un-metered: an Operation id maps to a finalized Session id (no control plane).
      const record = registry.get(ref.operationId);
      if (record === undefined) {
        throw new SdkLocalError("unknown-operation", `no finalized Operation ${JSON.stringify(ref.operationId)}`);
      }
      return {
        operationId: ref.operationId,
        cursor: null,
        payload: { blotter: record.finalized.blotter, grade: record.grade, artifacts: record.finalized.artifacts },
      };
    },
  };
}
