import { existsSync, mkdirSync, readFileSync, appendFileSync } from "node:fs";
import { dirname } from "node:path";
import { type Item, ItemSchema } from "./schema";

interface Tombstone {
  resolved: true;
  id: string;
  ts: string;
}

/** Sentinel that never equals a real item's JSON.stringify output — forces re-append after a tombstone. */
const TOMBSTONE_MARKER = " tombstone";

function isTombstone(record: unknown): record is Tombstone {
  return (
    typeof record === "object" &&
    record !== null &&
    (record as { resolved?: unknown }).resolved === true &&
    typeof (record as { id?: unknown }).id === "string"
  );
}

export class Journal {
  private readonly path: string;
  private readonly lastKnown = new Map<string, string>();

  constructor(path: string) {
    this.path = path;
  }

  /** Replays the journal in file order — upserts set, tombstones delete — and returns surviving items. */
  load(): Item[] {
    if (!existsSync(this.path)) {
      return [];
    }
    const byId = new Map<string, Item>();
    const raw = readFileSync(this.path, "utf8");
    for (const line of raw.split("\n")) {
      const trimmed = line.trim();
      if (!trimmed) continue;

      let parsedJson: unknown;
      try {
        parsedJson = JSON.parse(trimmed);
      } catch {
        continue;
      }

      if (isTombstone(parsedJson)) {
        byId.delete(parsedJson.id);
        this.lastKnown.set(parsedJson.id, TOMBSTONE_MARKER);
        continue;
      }

      const parsed = ItemSchema.safeParse(parsedJson);
      if (parsed.success) {
        byId.set(parsed.data.id, parsed.data);
        this.lastKnown.set(parsed.data.id, JSON.stringify(parsed.data));
      }
    }
    return [...byId.values()];
  }

  /** Appends the item when its id is unseen or its content changed vs the last known record. */
  append(item: Item): boolean {
    const serialized = JSON.stringify(item);
    if (this.lastKnown.get(item.id) === serialized) {
      return false;
    }
    this.writeLine(item);
    this.lastKnown.set(item.id, serialized);
    return true;
  }

  /** Appends a tombstone marking id resolved; replay treats it as a delete. */
  resolve(id: string, ts: string): void {
    const tombstone: Tombstone = { resolved: true, id, ts };
    this.writeLine(tombstone);
    this.lastKnown.set(id, TOMBSTONE_MARKER);
  }

  private writeLine(record: Item | Tombstone): void {
    mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
    appendFileSync(this.path, `${JSON.stringify(record)}\n`);
  }
}
