// sysutil/normalize-recording-history-serials.ts
// scope: one-time Ledger projectSerial normalization using current project registry

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

type ProjectEntry = {
  serialNo?: string;
  name?: string;
  location?: string;
  path?: string;
};

type ProjectIndex = {
  projects?: ProjectEntry[];
};

type LedgerRow = {
  timestamp?: number;
  recordID?: string;
  projectSerial?: string;
  filePath?: string;
  logType?: string;
  payload?: any;
  [key: string]: any;
};

type TapeItem = {
  projectSerial?: string;
  manifest?: any;
  result?: any;
  filePath?: string;
  FilePath?: string;
  [key: string]: any;
};

type CurrentProject = {
  serial: string;
  name: string;
  root: string;
};

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 readJsonValue<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 loadCurrentProjects(): Promise<CurrentProject[]> {
  const index = await readJsonValue<ProjectIndex>(path.join(ROOT, "settings", "projectIndex.json"), { projects: [] });
  return (index.projects || [])
    .map(project => {
      const serial = String(project.serialNo || "").trim();
      const location = normalizePathValue(project.location || project.path);
      if (!serial || !location) return null;
      return {
        serial,
        name: String(project.name || serial),
        root: path.resolve(ROOT, location)
      };
    })
    .filter((project): project is CurrentProject => Boolean(project))
    .sort((a, b) => b.root.length - a.root.length);
}

function resolveSerialFromPath(filePath: string, projects: CurrentProject[]): string {
  const clean = normalizePathValue(filePath);
  if (!clean) return "";
  const abs = path.resolve(ROOT, clean);
  const match = projects.find(project => abs === project.root || abs.startsWith(project.root + path.sep));
  return match?.serial || "";
}

function resolveSerialFromRecord(value: any, projects: CurrentProject[]): string {
  return resolveSerialFromPath(extractFilePath(value), projects);
}

function ledgerCurrentFile(): string { return path.join(DataPaths.logsDir, "ledger-current.jsonl"); }
function ledgerArchiveDir(): string { return path.join(DataPaths.logsDir, "archive"); }

async function ledgerFiles(): Promise<string[]> {
  const files = [ledgerCurrentFile()];
  try {
    const archives = (await fs.readdir(ledgerArchiveDir(), { withFileTypes: true }))
      .filter(entry => entry.isFile() && entry.name.endsWith(".jsonl"))
      .map(entry => path.join(ledgerArchiveDir(), entry.name))
      .sort((a, b) => a.localeCompare(b));
    files.push(...archives);
  } catch {}
  return files;
}

function tapeFileForSerial(serial: string): string { return path.join(DataPaths.logsDir, serial, "tape.json"); }

async function discoverTapeSerials(): Promise<string[]> {
  const serials = new Set<string>();
  try {
    const entries = await fs.readdir(DataPaths.logsDir, { withFileTypes: true });
    for (const entry of entries) {
      if (!entry.isDirectory()) continue;
      const serial = String(entry.name || "").trim();
      if (!serial || serial === "archive") continue;
      try {
        await fs.access(tapeFileForSerial(serial));
        serials.add(serial);
      } catch {}
    }
  } catch {}
  return [...serials].sort((a, b) => a.localeCompare(b));
}

function addSerialVote(votes: Map<string, Map<string, number>>, oldSerial: string, currentSerial: string): void {
  if (!oldSerial || !currentSerial || oldSerial === currentSerial) return;
  const bucket = votes.get(oldSerial) || new Map<string, number>();
  bucket.set(currentSerial, (bucket.get(currentSerial) || 0) + 1);
  votes.set(oldSerial, bucket);
}

function chooseSerialMap(votes: Map<string, Map<string, number>>): Record<string, string> {
  const map: Record<string, string> = {};
  for (const [oldSerial, bucket] of votes.entries()) {
    const ranked = [...bucket.entries()].sort((a, b) => b[1] - a[1]);
    if (ranked[0]) map[oldSerial] = ranked[0][0];
  }
  return map;
}

async function inferOldSerialMap(projects: CurrentProject[]): Promise<Record<string, string>> {
  const currentSerials = new Set(projects.map(project => project.serial));
  const votes = new Map<string, Map<string, number>>();

  for (const file of await ledgerFiles()) {
    let raw = "";
    try { raw = await fs.readFile(file, "utf8"); }
    catch { continue; }
    for (const line of raw.split("\n")) {
      if (!line.trim()) continue;
      try {
        const row = JSON.parse(line) as LedgerRow;
        const rowSerial = String(row.projectSerial || "").trim();
        if (!rowSerial || currentSerials.has(rowSerial)) continue;
        const resolved = resolveSerialFromRecord(row, projects) || resolveSerialFromRecord(row.payload, projects);
        addSerialVote(votes, rowSerial, resolved);
      } catch {}
    }
  }

  for (const serial of await discoverTapeSerials()) {
    const tape = await readJsonValue<TapeItem[]>(tapeFileForSerial(serial), []);
    for (const item of tape) {
      const itemSerial = String(item.projectSerial || serial || "").trim();
      if (!itemSerial || currentSerials.has(itemSerial)) continue;
      const resolved = resolveSerialFromRecord(item, projects);
      addSerialVote(votes, itemSerial, resolved);
      addSerialVote(votes, serial, resolved);
    }
  }

  return chooseSerialMap(votes);
}

async function rewriteLedgerFile(file: string, serialMap: Record<string, string>, projects: CurrentProject[]): Promise<{ file: string; rows: number; changed: number }> {
  let raw = "";
  try { raw = await fs.readFile(file, "utf8"); }
  catch { return { file, rows: 0, changed: 0 }; }

  const output: string[] = [];
  let rows = 0;
  let changed = 0;
  for (const line of raw.split("\n")) {
    if (!line.trim()) continue;
    rows++;
    try {
      const row = JSON.parse(line) as LedgerRow;
      const before = String(row.projectSerial || "").trim();
      const resolved = resolveSerialFromRecord(row, projects) || resolveSerialFromRecord(row.payload, projects) || serialMap[before] || before;
      if (resolved && resolved !== before) {
        row.projectSerial = resolved;
        changed++;
      }
      output.push(JSON.stringify(row));
    } catch {
      output.push(line);
    }
  }

  if (changed > 0) {
    await fs.copyFile(file, `${file}.serial-normalize-backup-${Date.now()}`);
    await fs.writeFile(file, output.join("\n") + (output.length ? "\n" : ""), "utf8");
  }

  return { file, rows, changed };
}

async function main(): Promise<void> {
  const projects = await loadCurrentProjects();
  const serialMap = await inferOldSerialMap(projects);
  const files = await ledgerFiles();
  const results = [];
  for (const file of files) results.push(await rewriteLedgerFile(file, serialMap, projects));
  console.log(JSON.stringify({ ok: true, currentProjects: projects.map(p => ({ serial: p.serial, name: p.name })), serialMap, results }, null, 2));
}

main().catch(err => {
  console.error(err?.stack || err?.message || err);
  process.exit(1);
});
