// playbackCore.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 ts(): string {
  const d = new Date();
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
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 projectLogDir(serial = currentSerial()): string { return path.join(DataPaths.logsDir, serial); }
function projectLogFile(serial: string, stream: LogStream): string { return path.join(projectLogDir(serial), `${normalizeLogStream(stream)}.log`); }
function projectTapeFile(serial = currentSerial()): string { return path.join(projectLogDir(serial), "tape.json"); }
function terminalIcon(tag: string, event: string): string {
  const t = String(tag || "").toLowerCase(), e = String(event || "").toLowerCase();
  if (t.includes("eq") || t.includes("ai")) return "🧠";
  if (t.includes("recorder") || e.includes("record") || e.includes("execute")) return "⏺️";
  if (t.includes("deck") || t.includes("power") || t.includes("playback") || t.includes("restore")) return "⚙️";
  return "•";
}
function terminalStatusIcon(event: string, summary: string): string {
  const text = `${event} ${summary}`.toLowerCase();
  if (/fail|error|missing|blocked|unavailable|invalid|rejected/.test(text)) return "🔴";
  if (/ready|started|complete|done|created|success|ok|pass|verified|allowed|refreshed/.test(text)) return "🟢";
  return "⚪";
}
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 append(file: string, line: string): Promise<void> {
  await fs.mkdir(path.dirname(file), { recursive: true });
  await fs.appendFile(file, line + "\n", "utf8");
}
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");
}

export function getScopeLogDir(serial: string): string { return projectLogDir(serial); }
export function getScopeLogFile(serial: string, stream: LogStream): string { return projectLogFile(serial, stream); }
export function getScopeTapeFile(serial: string): string { return path.join(projectLogDir(serial), "tape.json"); }
export function nextDubID(): string { return Date.now().toString() + Math.random().toString(36).slice(2, 6); }
export function classifyTone(event: string): LogTone {
  const e = event.toLowerCase();
  if (["error", "fail", "missing", "blocked", "unavailable", "invalid"].some(p => e.includes(p))) return "fail";
  if (["ready", "complete", "done", "created", "success", "ok", "pass", "restored"].some(p => e.includes(p))) return "pass";
  return "neutral";
}

export function logSystem(tag: string, payload: Record<string, any>): void {
  const event = String(payload?.event ?? "event");
  const filePath = extractFilePath(payload);
  const serial = String(payload?.projectSerial || currentSerial());
  const summary = buildSummary(payload);
  const terminal = [new Date().toLocaleTimeString(), terminalIcon(tag, event), terminalStatusIcon(event, summary), `[${tag}]`, event, `serial:${serial}`, filePath ? `file:${filePath}` : "", summary].filter(Boolean).join(" ");
  console.log(terminal);
  const line = [ts(), `[${tag}]`, `serial:${serial}`, `project:${projectLabel(serial)}`, filePath ? `filePath:${filePath}` : "", event, summary].filter(Boolean).join(" ");
  void append(projectLogFile(serial, "system"), line).catch(() => {});
}

export function logManifestFull(tag: string, event: string, payload: unknown): void {
  const filePath = extractFilePath(payload);
  const serial = currentSerial();
  const header = `${ts()} [Manifest:${tag}] serial:${serial} project:${projectLabel(serial)}${filePath ? ` filePath:${filePath}` : ""} ${event}`;
  void append(projectLogFile(serial, "system"), `${header}\n${JSON.stringify(payload ?? null, null, 2)}`).catch(() => {});
}

export function logAI(direction: "in" | "out", role: string, text: string): void;
export function logAI(stream: AIConversationStream, direction: "in" | "out", role: string, text: string): void;
export function logAI(a: AIConversationStream | "in" | "out", b: "in" | "out" | string, c: string, d?: string): void {
  const stream = d === undefined ? "ai-chat" : a as AIConversationStream;
  const direction = d === undefined ? a as "in" | "out" : b as "in" | "out";
  const role = d === undefined ? b as string : c;
  const text = d === undefined ? c : d;
  const serial = currentSerial();
  void append(projectLogFile(serial, stream as LogStream), `${ts()} serial:${serial} project:${projectLabel(serial)} [AI:${direction}] ${role} ${String(text ?? "").trim()}`).catch(() => {});
}

function buildDiff(before: string, after: string, ctx = 2): { lines: string[]; range: string } {
  const bL = before.split("\n"), aL = after.split("\n");
  let start = 0;
  while (start < bL.length && start < aL.length && bL[start] === aL[start]) start++;
  let bEnd = bL.length - 1, aEnd = aL.length - 1;
  while (bEnd > start && aEnd > start && bL[bEnd] === aL[aEnd]) { bEnd--; aEnd--; }
  const ctxStart = Math.max(0, start - ctx), ctxBEnd = Math.min(bL.length - 1, bEnd + ctx);
  const lines: string[] = [];
  for (let i = ctxStart; i < start; i++) lines.push(`  ${bL[i]}`);
  for (let i = start; i <= bEnd; i++) lines.push(`- ${bL[i]}`);
  for (let i = start; i <= aEnd; i++) lines.push(`+ ${aL[i]}`);
  for (let i = bEnd + 1; i <= ctxBEnd; i++) lines.push(`  ${bL[i]}`);
  return { lines, range: `${ctxStart + 1}-${ctxBEnd + 1}` };
}

export function logCode(opts: { action: string; filePath: string; before?: string; after?: string; changed: boolean; outcome?: string }): void {
  const serial = currentSerial();
  if (!opts.changed || opts.before == null || opts.after == null || opts.action === "newfile" || opts.action === "rewritefile") {
    const line = `${ts()} [Code] serial:${serial} project:${projectLabel(serial)} filePath:${opts.filePath} ${opts.action} changed:${Boolean(opts.changed)}${opts.outcome ? ` outcome:${opts.outcome}` : ""}`;
    void append(projectLogFile(serial, "code"), line).catch(() => {});
    return;
  }
  const diff = buildDiff(opts.before, opts.after);
  void append(projectLogFile(serial, "code"), [`${ts()} [Code] serial:${serial} project:${projectLabel(serial)} filePath:${opts.filePath} ${opts.action} @@ ${diff.range} @@`, ...diff.lines].join("\n")).catch(() => {});
}

async function loadTapeAll(): Promise<TapeItem[]> { return await readJsonFile<TapeItem[]>(projectTapeFile(), []); }
async function saveTape(items: TapeItem[]): Promise<void> { await saveJsonFile(projectTapeFile(), items); }
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 loadTapeAll();
  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 saveTape(tape);
  return item;
}
export async function approveManifestOnTape(manifest: any, opts?: { source?: string; kind?: TapeKind }): Promise<string> {
  const tape = await loadTapeAll();
  const serial = currentSerial();
  const manifestText = JSON.stringify(manifest);
  const existing = tape.find(item => item.status === "proposal" && JSON.stringify(item.manifest) === manifestText);
  if (existing) {
    existing.recorderStatus = "YES";
    existing.source = opts?.source || existing.source;
    if (opts?.kind) existing.kind = opts.kind;
    await saveTape(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, result: {}, timestamp: Date.now() };
  tape.push(item);
  await saveTape(tape);
  return item.dubID;
}
export async function markTapeItemsRetrieved(dubIDs: string[]): Promise<void> {
  const wanted = new Set(dubIDs);
  const tape = await loadTapeAll();
  let changed = false;
  for (const item of tape) {
    if (wanted.has(item.dubID) && item.recorderStatus === "YES") { item.recorderStatus = "RETRIEVED"; changed = true; }
  }
  if (changed) await saveTape(tape);
}
export async function appendExecutionToTape(input: { dubID: string; status: "done" | "error"; manifest: any; result: any; source?: string }): Promise<void> {
  const tape = await loadTapeAll();
  const serial = currentSerial();
  const original = tape.find(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 saveTape(tape);
}

function dubsClearMarkerPath(): string { return path.join(projectLogDir(), "dubs-clear-marker.json"); }
export async function getDubsClearAfter(): Promise<number> { return Number((await readJsonFile<any>(dubsClearMarkerPath(), {}))?.clearAfter || 0); }
export async function setDubsClearAfter(clearAfter = Date.now()): Promise<{ ok: true; clearAfter: number }> {
  await saveJsonFile(dubsClearMarkerPath(), { clearAfter });
  logSystem("Playback", { event: "dubs_display_reset", clearAfter });
  return { ok: true, clearAfter };
}
export async function loadDubsTape(): Promise<TapeItem[]> {
  const clearAfter = await getDubsClearAfter();
  const tape = await loadTape();
  return clearAfter ? tape.filter(item => Number(item.timestamp || 0) >= clearAfter) : tape;
}
export async function clearTapeItems(kinds: string[]): Promise<{ cleared: number; remaining: number }> {
  const clearAfter = Date.now();
  await setDubsClearAfter(clearAfter);
  logSystem("Playback", { event: "dubs_display_start_moved", clearAfter, kinds: Array.isArray(kinds) && kinds.length ? kinds.join(",") : "all" });
  return { cleared: 0, remaining: (await loadDubsTape()).length };
}

function readLogBlocks(raw: string): string[][] {
  const lines = raw.split("\n").filter(Boolean);
  const blocks: string[][] = [];
  let current: string[] = [];
  for (const line of lines) {
    if (/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s+/.test(line)) { if (current.length) blocks.push(current); current = [line]; continue; }
    if (!current.length) current = [line]; else current.push(line);
  }
  if (current.length) blocks.push(current);
  return blocks;
}
function limitLogBlocks(blocks: string[][], maxLines: number): string[] {
  const limit = Math.max(1, Number(maxLines || 200));
  const selected: string[][] = [];
  let total = 0;
  for (let i = blocks.length - 1; i >= 0; i--) {
    const block = blocks[i] || [];
    if (!block.length) continue;
    selected.push(block);
    total += block.length;
    if (total >= limit) break;
  }
  return selected.flat();
}
async function readMergedLog(stream: LogStream, maxLines = 200): Promise<string[]> {
  const limit = Math.max(1, Number(maxLines || 200));
  const normalized = normalizeLogStream(stream);
  const files: string[] = [];

  try {
    for (const entry of await fs.readdir(DataPaths.logsDir, { withFileTypes: true })) {
      if (!entry.isDirectory()) continue;
      files.push(path.join(DataPaths.logsDir, entry.name, `${normalized}.log`));
    }
  } catch {}

  const lines: string[] = [];
  for (const file of files) {
    try {
      lines.push(...(await fs.readFile(file, "utf8")).split("\n").filter(Boolean));
    } catch {}
  }

  return lines
    .sort((a, b) => a.localeCompare(b))
    .slice(Math.max(0, lines.length - limit));
}

export async function readLog(stream: LogStream, maxLines = 200): Promise<string[]> {
  if (stream === "system") return await readMergedLog(stream, maxLines);
  try { return limitLogBlocks(readLogBlocks(await fs.readFile(projectLogFile(currentSerial(), stream), "utf8")), maxLines); }
  catch { return []; }
}
export async function clearLog(stream: LogStream): Promise<void> {
  await fs.writeFile(projectLogFile(currentSerial(), stream), "", "utf8").catch(() => {});
}
