/**
 * # bus/write — the single-writer, append-only JSONL bus writer (RUNTIME §1)
 *
 * `BusWriter` is the ONE thing that assigns `seq`. It is a **single writer** by contract:
 * one writer owns one file; `seq` is monotonic and gap-free from 0; every `append` emits
 * exactly one JSONL line and **flushes it** (each event is durable the instant it is
 * written — that is what makes the reader's torn-final-line tolerance the only crash mode
 * it must handle, RUNTIME §1).
 *
 * The envelope is serialized in canonical field order `{seq, ts, stream, type, ...payload}`
 * (RUNTIME §1). Serialization is a pure function of the event, so the same event sequence
 * always produces byte-identical text — the determinism substrate for replay (RUNTIME §0)
 * and for byte-identical synthetic sessions.
 */

import type {
  BookPayload,
  BusEvent,
  ControlPayload,
  DetectorPayload,
  HeartbeatPayload,
  JournalEvent,
  JournalKind,
  MetaPayload,
  NewBusEvent,
  OrderAction,
  OrderPayload,
  PlanPayload,
  RegimePayload,
  SpotPayload,
  Stream,
  WakeKind,
  WakePayload,
} from "./types.ts";

// ─────────────────────────────────────────────────────────────────────────────
// Pure serialization (the byte-stable substrate)
// ─────────────────────────────────────────────────────────────────────────────

/** Serialize one event to its single JSONL line (including the trailing newline).
 * Canonical field order: `seq, ts, stream, type`, then payload in declaration order. */
export function serializeEvent(event: BusEvent): string {
  return JSON.stringify(event) + "\n";
}

/** Serialize a whole event sequence to bus text. Pure and total: same events ⇒ same bytes. */
export function serializeBus(events: Iterable<BusEvent>): string {
  let out = "";
  for (const ev of events) out += serializeEvent(ev);
  return out;
}

/** Write a whole (already-`seq`-stamped) event sequence to a file in one shot. Used to
 * persist a synthetic session; `BusWriter` is for incremental engine writes. */
export async function writeBusFile(path: string, events: Iterable<BusEvent>): Promise<void> {
  await Bun.write(path, serializeBus(events));
}

// ─────────────────────────────────────────────────────────────────────────────
// Envelope assembly (seq stamping) — the one place seq is minted
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Stamp a `seq` onto a seq-less event, producing the CANONICAL-ORDER envelope
 * `{seq, ts, stream, type, ...payload}` (RUNTIME §1) — **the one owner of bus field order**.
 *
 * It re-canonicalizes rather than prepending, and that is the whole point: `serializeEvent` is
 * `JSON.stringify`, which is INSERTION-ORDER-dependent, so a caller spreading a payload that itself
 * carries `ts`/`stream`/`type` mid-object (`{ stream, type, ...held.thetaBleed }` where `thetaBleed`
 * owns a `ts`) would otherwise serialize a NON-canonical record onto the graded bus. Destructuring the
 * envelope fields out and re-emitting them first makes the canonical order a property of this function
 * instead of a discipline every call site must remember (kestrel-byfl; the pta5 instance).
 *
 * `type` is re-emitted ONLY when the caller has one: JOURNAL's discriminant is `kind`, not `type`
 * ({@link BusWriter.appendJournal}), and an unconditional `type` would inject a `type: undefined` key
 * into the in-memory envelope — invisible in the bytes (`JSON.stringify` drops it) but visible to any
 * reader that asks `"type" in ev`. Never a silent default.
 *
 * The cast is the single boundary where the flat union is reassembled; the overloads on
 * {@link BusWriter.append} and the {@link NewBusEvent} shape guarantee the fields are right.
 */
export function canonicalEnvelope(seq: number, ev: NewBusEvent): BusEvent {
  const { ts, stream, type, ...payload } = ev as NewBusEvent & { ts: number; stream: Stream; type?: string };
  return (type === undefined ? { seq, ts, stream, ...payload } : { seq, ts, stream, type, ...payload }) as BusEvent;
}

// ─────────────────────────────────────────────────────────────────────────────
// BusWriter
// ─────────────────────────────────────────────────────────────────────────────

/**
 * The single writer of a bus file. Construct one per session; `append` stamps the next
 * `seq`, writes the JSONL line, and flushes. `close()` ends the underlying sink; any
 * `append` after `close()` throws (single-writer, no-reopen).
 *
 * `append(stream, type, payload, ts)` is overloaded per stream so the payload is checked
 * against the stream/type pair. The returned value is the fully-stamped event (with `seq`),
 * so a caller can echo it or fold it immediately.
 */
export class BusWriter {
  #sink: Bun.FileSink;
  #seq = 0;
  #closed = false;

  constructor(path: string) {
    this.#sink = Bun.file(path).writer();
  }

  /** The next `seq` that will be assigned (i.e. the count of events written so far). */
  get nextSeq(): number {
    return this.#seq;
  }

  append(stream: "META", type: "session", payload: MetaPayload, ts: number): BusEvent;
  append(stream: "TICK", type: "SPOT", payload: SpotPayload, ts: number): BusEvent;
  append(stream: "TICK", type: "BOOK", payload: BookPayload, ts: number): BusEvent;
  append(stream: "TICK", type: "HEARTBEAT", payload: HeartbeatPayload, ts: number): BusEvent;
  append(stream: "DETECTOR", type: string, payload: DetectorPayload, ts: number): BusEvent;
  append(stream: "PLAN", type: "lifecycle", payload: PlanPayload, ts: number): BusEvent;
  append(stream: "ORDER", type: OrderAction, payload: OrderPayload, ts: number): BusEvent;
  append(stream: "WAKE", type: WakeKind, payload: WakePayload, ts: number): BusEvent;
  append(stream: "CONTROL", type: string, payload: ControlPayload, ts: number): BusEvent;
  append(stream: "REGIME", type: "tag", payload: RegimePayload, ts: number): BusEvent;
  append(stream: Stream, type: string, payload: object, ts: number): BusEvent {
    if (this.#closed) {
      throw new Error("BusWriter: append after close (single-writer, no reopen)");
    }
    const seq = this.#seq++;
    const event = canonicalEnvelope(seq, { ts, stream, type, ...payload } as NewBusEvent);
    this.#sink.write(serializeEvent(event));
    this.#sink.flush();
    return event;
  }

  /**
   * Append a JOURNAL record — the author's reasoning, full markdown `body` inline (bus_schema
   * v2). JOURNAL has no engine `type`; its discriminant is `kind`, so it takes its own writer
   * method rather than an {@link append} overload. Mints the next `seq` exactly like `append`,
   * which is what makes the pre-hoc/post-hoc seq contract a fact of the log: write an `author`
   * record before you arm and its `seq` is provably below the first armed PLAN's; append a
   * `debrief` after CLOSE and its `seq` is provably above the CLOSE's.
   *
   * JOURNAL is author metadata, never an engine input — appending these records cannot move the
   * emitted engine event stream or the determinism hash (the determinism invariant, a57.11).
   */
  appendJournal(kind: JournalKind, body: string, ts: number): BusEvent {
    if (this.#closed) {
      throw new Error("BusWriter: append after close (single-writer, no reopen)");
    }
    const seq = this.#seq++;
    const event: JournalEvent = { seq, ts, stream: "JOURNAL", kind, body };
    this.#sink.write(serializeEvent(event));
    this.#sink.flush();
    return event;
  }

  /** End the sink and forbid further appends. Idempotent. */
  async close(): Promise<void> {
    if (this.#closed) return;
    this.#closed = true;
    await this.#sink.end();
  }
}
