// playback.ts
// Scope: logs, AI conversation logs, code diffs, tape/history display, and dubs display state.
// Does not own restore execution, restore points, scrapes, snapshots, backups, or authority.

import { promises as fs } from "node:fs";
import path from "node:path";
import { controller, BACKSTAGE_SERIAL } from "./controller";
import { DataPaths } from "./pathList";

export type RecorderStatus = "YES" | "NO" | "RETRIEVED";
export type TapeKind = "run" | "diagnostic" | "enhancement" | "error";
export type LogStream = "system" | "ai" | "code" | "ai-chat" | "ai-diagnostic";
export type AIConversationStream = "ai-chat" | "ai-diagnostic";
export type LogTone = "pass" | "fail" | "neutral";

export type TapeItem = {
  dubID: string;
  status: "done" | "error" | "proposal";
  recorderStatus: RecorderStatus;
  kind: TapeKind;
  source?: string;
  project?: string;
  projectSerial?: string;
  manifest: any;
  result: any;
  timestamp: number;
};

function pad(n: number): string { return String(n).padStart(2, "0"); }
function currentSerial(): string { return String(controller.activeProjectInquiry().projectSerial || BACKSTAGE_SERIAL).trim() || BACKSTAGE_SERIAL; }
function projectLabel(serial: string): string { return serial === BACKSTAGE_SERIAL ? "universal" : serial; }
function normalizeLogStream(stream: LogStream): LogStream { return stream === "ai" ? "ai-chat" : stream; }
function tapeCurrentFile(): string { return path.join(DataPaths.logsDir, "tape-current.jsonl"); }
function tapeClearMarkerFile(): string { return path.join(DataPaths.logsDir, "tape-clear-current.json"); }
function buildSummary(payload: Record<string, any>): string {
  const skip = new Set(["event", "before", "after", "Payload", "project", "filePath", "FilePath", "path", "projectSerial"]);
  return Object.entries(payload || {})
    .filter(([k, v]) => !skip.has(k) && v != null)
    .map(([k, v]) => {
      const str = typeof v === "string" ? v : JSON.stringify(v);
      return str.length > 120 ? `${k}:${str.slice(0, 120)}…` : `${k}:${str}`;
    })
    .join(" ");
}
function normalizePathValue(value: unknown): string { return String(value || "").trim().replace(/\\/g, "/"); }
function extractFilePath(value: any): string {
  if (!value) return "";
  if (typeof value === "string") {
    const text = value.trim();
    for (const p of [/"FilePath"\s*:\s*"([^"]+)"/i, /"filePath"\s*:\s*"([^"]+)"/i]) {
      const m = text.match(p);
      if (m?.[1]) return normalizePathValue(m[1]);
    }
    return "";
  }
  if (typeof value !== "object") return "";
  for (const c of [value.FilePath, value.filePath, value.path]) {
    const clean = normalizePathValue(c);
    if (clean) return clean;
  }
  return extractFilePath(value.manifest) || extractFilePath(value.result) || extractFilePath(value.Payload) || extractFilePath(value.payload) || "";
}
async function readJsonFile<T>(filePath: string, fallback: T): Promise<T> {
  try {
    const raw = await fs.readFile(filePath, "utf8");
    return raw.trim() ? JSON.parse(raw) as T : fallback;
  } catch { return fallback; }
}
async function saveJsonFile(filePath: string, value: unknown): Promise<void> {
  await fs.mkdir(path.dirname(filePath), { recursive: true });
  await fs.writeFile(filePath, JSON.stringify(value, null, 2), "utf8");
}

function ledgerCurrentFile(): string { return path.join(DataPaths.logsDir, "ledger-current.jsonl"); }
function ledgerArchiveDir(): string { return path.join(DataPaths.logsDir, "archive"); }
export function nextDubID(): string { return Date.now().toString() + Math.random().toString(36).slice(2, 6); }
function tapeItemMatchesScope(item: TapeItem): boolean {
  const serial = currentSerial();
  if (serial === BACKSTAGE_SERIAL) return true;
  return String(item.projectSerial || "") === serial;
}
async function readTapeCurrentAll(): Promise<TapeItem[]> {
  let raw = "";
  try { raw = await fs.readFile(tapeCurrentFile(), "utf8"); }
  catch { return []; }
  return raw.split("\n")
    .filter(Boolean)
    .map(line => { try { return JSON.parse(line) as TapeItem; } catch { return null; } })
    .filter((item): item is TapeItem => Boolean(item?.dubID));
}
async function writeTapeCurrentAll(items: TapeItem[]): Promise<void> {
  await fs.mkdir(path.dirname(tapeCurrentFile()), { recursive: true });
  await fs.writeFile(tapeCurrentFile(), items.map(item => JSON.stringify(item)).join("\n") + (items.length ? "\n" : ""), "utf8");
}
async function loadTapeAll(): Promise<TapeItem[]> { return (await readTapeCurrentAll()).filter(tapeItemMatchesScope); }
export async function loadTape(): Promise<TapeItem[]> { return await loadTapeAll(); }
export async function queueProposalToTape(input: { manifest: any; report?: any; kind?: TapeKind; source?: string; recorderStatus?: RecorderStatus }): Promise<TapeItem> {
  const tape = await readTapeCurrentAll();
  const serial = currentSerial();
  const item: TapeItem = { dubID: nextDubID(), status: "proposal", recorderStatus: input.recorderStatus ?? "NO", kind: input.kind ?? "diagnostic", source: input.source || "eq", project: projectLabel(serial), projectSerial: serial, manifest: input.manifest, result: input.report ?? {}, timestamp: Date.now() };
  tape.push(item);
  await writeTapeCurrentAll(tape);
  return item;
}
function playbackObject(value: unknown): value is Record<string, any> {
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}

function manifestFieldKey(manifest: any, names: string[]): string {
  if (!playbackObject(manifest)) return "";
  const wanted = new Set(names.map(name => String(name || "").toLowerCase()));
  return Object.keys(manifest).find(key => wanted.has(String(key || "").toLowerCase())) || "";
}

function taskManifestForApproval(manifest: any, preferredKind?: TapeKind): any {
  if (!playbackObject(manifest)) return manifest;
  if (String(manifestFieldKey(manifest, ["Task"]) ? manifest[manifestFieldKey(manifest, ["Task"])] : "").trim()) return manifest;

  const preferredNames = preferredKind === "enhancement"
    ? ["Enhancement", "enhancement", "Diagnostic", "diagnostic"]
    : ["Diagnostic", "diagnostic", "Enhancement", "enhancement"];
  const proposalKey = manifestFieldKey(manifest, preferredNames);
  const task = String(proposalKey ? manifest[proposalKey] : "").trim();
  if (!task) return manifest;

  const next = { ...manifest, Task: task };
  delete next[proposalKey];
  return next;
}

export async function approveManifestOnTape(manifest: any, opts?: { source?: string; kind?: TapeKind }): Promise<string> {
  const tape = await readTapeCurrentAll();
  const serial = currentSerial();
  const originalManifestText = JSON.stringify(manifest);
  const approvedManifest = taskManifestForApproval(manifest, opts?.kind);
  const existing = tape.find(item => tapeItemMatchesScope(item) && item.status === "proposal" && JSON.stringify(item.manifest) === originalManifestText);
  if (existing) {
    existing.recorderStatus = "YES";
    existing.source = opts?.source || existing.source;
    if (opts?.kind) existing.kind = opts.kind;
    existing.manifest = approvedManifest;
    await writeTapeCurrentAll(tape);
    return existing.dubID;
  }
  const item: TapeItem = { dubID: nextDubID(), status: "proposal", recorderStatus: "YES", kind: opts?.kind || "diagnostic", source: opts?.source || "monitor", project: projectLabel(serial), projectSerial: serial, manifest: approvedManifest, result: {}, timestamp: Date.now() };
  tape.push(item);
  await writeTapeCurrentAll(tape);
  return item.dubID;
}

export async function rejectManifestOnTape(input: { dubID?: string; manifest?: any; source?: string }): Promise<string> {
  const tape = await readTapeCurrentAll();
  const dubID = String(input?.dubID || "").trim();
  const manifestText = input?.manifest ? JSON.stringify(input.manifest) : "";
  const existing = tape.find(item => tapeItemMatchesScope(item) && item.status === "proposal" && ((dubID && item.dubID === dubID) || (manifestText && JSON.stringify(item.manifest) === manifestText)));
  if (!existing) throw new Error("proposal_not_found");
  existing.recorderStatus = "NO";
  existing.source = input.source || existing.source;
  await writeTapeCurrentAll(tape);
  return existing.dubID;
}
export async function markTapeItemsRetrieved(dubIDs: string[]): Promise<void> {
  const wanted = new Set(dubIDs);
  const tape = await readTapeCurrentAll();
  let changed = false;
  for (const item of tape) {
    if (tapeItemMatchesScope(item) && wanted.has(item.dubID) && item.recorderStatus === "YES") { item.recorderStatus = "RETRIEVED"; changed = true; }
  }
  if (changed) await writeTapeCurrentAll(tape);
}
export async function appendExecutionToTape(input: { dubID: string; status: "done" | "error"; manifest: any; result: any; source?: string }): Promise<void> {
  const tape = await readTapeCurrentAll();
  const serial = currentSerial();
  const original = tape.find(item => tapeItemMatchesScope(item) && item.dubID === input.dubID && item.status === "proposal");
  const originalKind: TapeKind = original?.kind === "diagnostic" || original?.kind === "enhancement" || original?.kind === "error" ? original.kind : "run";
  tape.push({ dubID: input.dubID, status: input.status, recorderStatus: "RETRIEVED", kind: input.status === "done" ? originalKind : "error", source: input.source || original?.source || "recorder", project: projectLabel(serial), projectSerial: serial, manifest: input.manifest, result: { ...input.result, acceptedFromTape: Boolean(original), originalKind }, timestamp: Date.now() });
  await writeTapeCurrentAll(tape);
}

export async function replaceCurrentIssuesOnTape(input: { source?: string; issues: Array<{ message: string; filePath?: string; line?: number; column?: number; code?: string }> }): Promise<TapeItem[]> {
  const tape = await readTapeCurrentAll();
  const serial = currentSerial();
  const issueSource = String(input.source || "typecheck").trim();
  const issueSerial = issueSource === "typecheck" ? BACKSTAGE_SERIAL : serial;
  const kept = tape.filter(item => !(item.kind === "error" && String(item.source || "") === "typecheck"));
  const issues = Array.isArray(input.issues) ? input.issues : [];
  const nextItems = issues.map((issue, index) => ({
    dubID: `issue-${Date.now()}-${index}-${Math.random().toString(36).slice(2, 6)}`,
    status: "proposal" as const,
    recorderStatus: "NO" as const,
    kind: "error" as const,
    source: input.source || "typecheck",
    project: projectLabel(issueSerial),
    projectSerial: issueSerial,
    manifest: null,
    result: {
      summary: issue.message,
      error: issue.message,
      filePath: issue.filePath || "",
      line: issue.line || null,
      column: issue.column || null,
      code: issue.code || "",
      issueType: "typescript"
    },
    timestamp: Date.now()
  }));
  await writeTapeCurrentAll([...kept, ...nextItems]);
  return nextItems;
}
async function readTapeClearMarkers(): Promise<Record<string, number>> {
  return await readJsonFile<Record<string, number>>(tapeClearMarkerFile(), {});
}
async function writeTapeClearMarkers(markers: Record<string, number>): Promise<void> {
  await saveJsonFile(tapeClearMarkerFile(), markers);
}
export async function getDubsClearAfter(): Promise<number> {
  const markers = await readTapeClearMarkers();
  return Number(markers[currentSerial()] || 0);
}
export async function setDubsClearAfter(clearAfter = Date.now()): Promise<{ ok: true; clearAfter: number }> {
  const markers = await readTapeClearMarkers();
  markers[currentSerial()] = clearAfter;
  await writeTapeClearMarkers(markers);
  return { ok: true, clearAfter };
}
export async function loadDubsTape(): Promise<TapeItem[]> {
  const markers = await readTapeClearMarkers();
  const serial = currentSerial();
  const globalClearAfter = Number(markers[serial] || 0);
  const tape = await loadTape();
  return tape.filter(item => {
    const itemClearAfter = Number(markers[`${serial}:${item.kind}`] || 0);
    const clearAfter = Math.max(globalClearAfter, itemClearAfter);
    return clearAfter ? Number(item.timestamp || 0) >= clearAfter : true;
  });
}
export async function clearTapeItems(kinds: string[]): Promise<{ cleared: number; remaining: number }> {
  const wanted = new Set((Array.isArray(kinds) ? kinds : [])
    .map(kind => String(kind || "").trim().toLowerCase())
    .filter(kind => kind === "diagnostic" || kind === "enhancement"));

  if (!wanted.size) {
    const clearAfter = Date.now();
    await setDubsClearAfter(clearAfter);
    return { cleared: 0, remaining: (await loadDubsTape()).length };
  }

  const markers = await readTapeClearMarkers();
  const serial = currentSerial();
  const clearAfter = Date.now();
  for (const kind of wanted) markers[`${serial}:${kind}`] = clearAfter;
  await writeTapeClearMarkers(markers);
  return { cleared: wanted.size, remaining: (await loadDubsTape()).length };
}

type LedgerRow = {
  timestamp?: number;
  recordID?: string;
  projectSerial?: string;
  source?: string;
  logType?: string;
  status?: string;
  filePath?: string;
  payload?: any;
};

function formatLedgerTimestamp(value: unknown): string {
  const d = new Date(Number(value || 0) || Date.now());
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
function ledgerRowMatchesScope(row: LedgerRow): boolean {
  const serial = currentSerial();
  if (serial === BACKSTAGE_SERIAL) return true;
  return String(row.projectSerial || "") === serial;
}
function ledgerRowMatchesStream(row: LedgerRow, stream: LogStream): boolean {
  const normalized = normalizeLogStream(stream);
  const logType = String(row.logType || "").toUpperCase();
  if (normalized === "system") return logType === "SYSTEM" || logType === "MANIFEST";
  if (normalized === "code") return logType === "DIFF";
  if (normalized === "ai-chat" || normalized === "ai-diagnostic") {
    return logType === "AI_CALL" && String(row.payload?.stream || "ai-chat") === normalized;
  }
  return false;
}
function formatLedgerLogLine(row: LedgerRow): string {
  const payload = playbackObject(row.payload) ? row.payload : {};
  const timestamp = formatLedgerTimestamp(row.timestamp);
  const source = String(row.source || payload.tag || row.logType || "Log");
  const event = String(payload.event || row.status || "recorded");
  const serial = String(row.projectSerial || "");
  const filePath = normalizePathValue(row.filePath || extractFilePath(payload));
  const summary = buildSummary(payload);
  const diffLines = row.logType === "DIFF" && Array.isArray(payload.diff?.lines) ? payload.diff.lines.join("\n") : "";
  const detail = row.logType === "MANIFEST" ? JSON.stringify(payload.manifest ?? payload) : (diffLines || summary);
  return [timestamp, `[${source}]`, `serial:${serial}`, filePath ? `filePath:${filePath}` : "", event, detail].filter(Boolean).join(" ");
}
async function readLedgerLog(stream: LogStream, maxLines = 200, archiveDepth = 0): Promise<string[]> {
  const limit = Math.max(1, Number(maxLines || 200));
  const files = [ledgerCurrentFile()];
  const depth = Math.max(0, Math.min(24, Number(archiveDepth || 0)));

  if (depth > 0) {
    try {
      const archiveFiles = (await fs.readdir(ledgerArchiveDir(), { withFileTypes: true }))
        .filter(entry => entry.isFile() && entry.name.endsWith(".jsonl"))
        .map(entry => path.join(ledgerArchiveDir(), entry.name))
        .sort((a, b) => b.localeCompare(a))
        .slice(0, depth);
      files.push(...archiveFiles);
    } catch {}
  }

  const rows: LedgerRow[] = [];
  for (const file of files) {
    let raw = "";
    try { raw = await fs.readFile(file, "utf8"); }
    catch { continue; }
    rows.push(...raw.split("\n")
      .filter(Boolean)
      .map(line => { try { return JSON.parse(line) as LedgerRow; } catch { return null; } })
      .filter((row): row is LedgerRow => Boolean(row) && ledgerRowMatchesScope(row) && ledgerRowMatchesStream(row, stream)));
  }

  rows.sort((a, b) => Number(b.timestamp || 0) - Number(a.timestamp || 0));
  return rows.slice(0, limit).map(formatLedgerLogLine);
}

export async function readLog(stream: LogStream, maxLines = 200, archiveDepth = 0): Promise<string[]> {
  return await readLedgerLog(stream, maxLines, archiveDepth);
}
export async function clearLog(_stream: LogStream): Promise<void> {
  return;
}
