// hashState.ts
// Scope: file hash tracking for external drift detection and per-write before/after capture.
// Verbatim's last-known state for any file it has touched, keyed by project serial.

import fs from "node:fs";
import { promises as fsp } from "node:fs";
import path from "node:path";
import * as crypto from "node:crypto";
import { getLogScopeDir } from "./pathList";


const ROOT = process.cwd();

// ── Types ─────────────────────────────────────────────────────────────────────

export type FileSummary = {
  hash: string;
  size: number;
  lineCount: number;
  mtime: number;
};

type FileStateMap = Record<string, FileSummary>;

// ── Hashing ───────────────────────────────────────────────────────────────────

export function hashText(text: string): string {
  return crypto.createHash("sha256").update(text, "utf8").digest("hex");
}

export function hashFile(absPath: string): string {
  const buf = fs.readFileSync(absPath);
  return crypto.createHash("sha256").update(buf).digest("hex");
}

export async function hashFileAsync(absPath: string): Promise<string> {
  const buf = await fsp.readFile(absPath);
  return crypto.createHash("sha256").update(buf).digest("hex");
}

// ── State path ────────────────────────────────────────────────────────────────

export function getStatePath(serial: string): string {
  return path.join(getLogScopeDir(serial), "state", "files.json");
}

// ── Load / save ───────────────────────────────────────────────────────────────

export async function loadFileState(serial: string): Promise<FileStateMap> {
  try {
    const raw = await fsp.readFile(getStatePath(serial), "utf8");
    const parsed = JSON.parse(raw);
    return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
  } catch {
    return {};
  }
}

export async function saveFileState(serial: string, state: FileStateMap): Promise<void> {
  const p = getStatePath(serial);
  await fsp.mkdir(path.dirname(p), { recursive: true });
  await fsp.writeFile(p, JSON.stringify(state, null, 2), "utf8");
}

// ── Per-file accessors ────────────────────────────────────────────────────────

/** Get Verbatim's last recorded hash for a file, or null if never seen. */
export async function getKnownFileHash(serial: string, relPath: string): Promise<string | null> {
  const state = await loadFileState(serial);
  return state[normalizeKey(relPath)]?.hash ?? null;
}

/** Update the known hash+summary after a successful write or capture. */
export async function updateKnownFileHash(serial: string, relPath: string, summary: FileSummary): Promise<void> {
  const state = await loadFileState(serial);
  state[normalizeKey(relPath)] = summary;
  await saveFileState(serial, state);
}

// ── Drift detection ───────────────────────────────────────────────────────────

/**
 * Compare the current file hash against the last-known hash.
 * Returns { drifted: false } if the file matches or was never tracked.
 * Returns { drifted: true, knownHash, currentHash } if the file changed externally.
 */
export async function detectExternalDrift(
  serial: string,
  relPath: string
): Promise<{ drifted: boolean; knownHash: string | null; currentHash: string | null }> {
  const known = await getKnownFileHash(serial, relPath);
  if (!known) return { drifted: false, knownHash: null, currentHash: null };

  try {
    const abs = path.isAbsolute(relPath) ? relPath : path.resolve(ROOT, relPath);
    const current = await hashFileAsync(abs);
    return { drifted: current !== known, knownHash: known, currentHash: current };
  } catch {
    // File deleted or unreadable — report as drifted
    return { drifted: true, knownHash: known, currentHash: null };
  }
}

// ── Summary builder ───────────────────────────────────────────────────────────

/** Capture hash, size, line count, and mtime for a file or text buffer. */
export async function summarizeFileState(
  absPathOrText: string,
  isText = false
): Promise<FileSummary> {
  if (isText) {
    const text = absPathOrText;
    return {
      hash: hashText(text),
      size: Buffer.byteLength(text, "utf8"),
      lineCount: text.split("\n").length,
      mtime: Date.now()
    };
  }

  const buf = await fsp.readFile(absPathOrText);
  const stat = await fsp.stat(absPathOrText);
  const text = buf.toString("utf8");

  return {
    hash: crypto.createHash("sha256").update(buf).digest("hex"),
    size: buf.length,
    lineCount: text.split("\n").length,
    mtime: stat.mtimeMs
  };
}

// ── Internal helpers ──────────────────────────────────────────────────────────

function normalizeKey(relPath: string): string {
  return String(relPath || "").trim().replace(/\\/g, "/").replace(/^\.\//, "");
}