import { promises as fs } from "node:fs";
import { basename, join, relative } from "node:path";

import { compareUnicodeCodePoints, sha256Digest } from "./canonical.ts";

export interface TmpaSourceRevisionRecord {
  schema_version: "1.0";
  tmpa_version: "S0.5" | "S0.6" | "S1.0";
  object_id: string;
  source_path: string;
  content_digest: string;
  captured_at: string;
  capture_reason: "before_mutation" | "after_mutation";
  raw_text: string;
}

function normalizeSlashes(value: string): string {
  return value.replace(/\\/g, "/");
}

function projectRootFromFcopPath(sourcePath: string): string | null {
  const normalized = normalizeSlashes(sourcePath);
  const marker = normalized.toLowerCase().indexOf("/fcop/");
  if (marker < 0) return null;
  return normalized.slice(0, marker);
}

function sourceObjectId(sourcePath: string, raw: string): string {
  const frontmatterId = raw.match(
    /^---\r?\n[\s\S]*?^task_id:\s*["']?([^\r\n"']+)/im,
  )?.[1];
  return String(frontmatterId ?? basename(sourcePath, ".md")).trim();
}

function safeDirectoryName(value: string): string {
  return value.replace(/[^A-Za-z0-9_.-]+/g, "_").slice(0, 180) || "unknown";
}

export async function captureTmpaSourceRevision(
  sourcePath: string,
  raw: string,
  reason: TmpaSourceRevisionRecord["capture_reason"],
): Promise<string | null> {
  const projectRoot = projectRootFromFcopPath(sourcePath);
  if (!projectRoot) return null;
  const objectId = sourceObjectId(sourcePath, raw);
  const contentDigest = sha256Digest(Buffer.from(raw, "utf8"));
  const digestHex = contentDigest.slice("sha256:".length);
  const revisionDir = join(
    projectRoot,
    ".codeflowmu",
    "tmpa",
    "source-revisions",
    safeDirectoryName(objectId),
  );
  const revisionPath = join(revisionDir, `${digestHex}.json`);
  await fs.mkdir(revisionDir, { recursive: true });
  const record: TmpaSourceRevisionRecord = {
    schema_version: "1.0",
    tmpa_version: "S1.0",
    object_id: objectId,
    source_path: normalizeSlashes(relative(projectRoot, sourcePath)),
    content_digest: contentDigest,
    captured_at: new Date().toISOString(),
    capture_reason: reason,
    raw_text: raw,
  };
  try {
    await fs.writeFile(revisionPath, `${JSON.stringify(record, null, 2)}\n`, {
      encoding: "utf8",
      flag: "wx",
    });
  } catch (error) {
    const code = (error as NodeJS.ErrnoException).code;
    if (code !== "EEXIST") throw error;
  }
  return revisionPath;
}

export async function readTmpaSourceRevisionRecords(
  projectRoot: string,
): Promise<TmpaSourceRevisionRecord[]> {
  const root = join(projectRoot, ".codeflowmu", "tmpa", "source-revisions");
  const records: TmpaSourceRevisionRecord[] = [];
  let objectDirs: string[];
  try {
    objectDirs = await fs.readdir(root);
  } catch {
    return records;
  }
  for (const objectDir of objectDirs.sort(compareUnicodeCodePoints)) {
    const fullDir = join(root, objectDir);
    let files: string[];
    try {
      files = await fs.readdir(fullDir);
    } catch {
      continue;
    }
    for (const file of files
      .filter((value) => value.endsWith(".json"))
      .sort(compareUnicodeCodePoints)) {
      try {
        const parsed = JSON.parse(
          await fs.readFile(join(fullDir, file), "utf8"),
        ) as TmpaSourceRevisionRecord;
        if (
          parsed.schema_version === "1.0" &&
          (parsed.tmpa_version === "S0.5" ||
            parsed.tmpa_version === "S0.6" ||
            parsed.tmpa_version === "S1.0") &&
          parsed.object_id &&
          parsed.content_digest &&
          typeof parsed.raw_text === "string"
        ) {
          records.push(parsed);
        }
      } catch {
        // Malformed revision records stay on disk and are ignored by this typed reader.
      }
    }
  }
  return records.sort(
    (left, right) =>
      compareUnicodeCodePoints(left.object_id, right.object_id) ||
      compareUnicodeCodePoints(left.captured_at, right.captured_at) ||
      compareUnicodeCodePoints(left.content_digest, right.content_digest),
  );
}
