import { sha256 } from "./hwlab-caserun-evidence.ts";

type CommandResult = { stdout?: unknown } & Record<string, unknown>;

export type AgentDiffFileRecord = {
  path: string;
  source: "tracked" | "untracked";
  reason?: string;
  rule?: string;
  bytes?: number;
  sha256?: string;
};

type DiffCollectionRule = { pattern: string; reason: string };

export type AgentDiffCollectionSummary = {
  contractVersion: "hwpod-case-run-diff-collection-v1";
  included: AgentDiffFileRecord[];
  omitted: AgentDiffFileRecord[];
  config: { include: DiffCollectionRule[]; ignore: DiffCollectionRule[] };
  tracked: { paths: string[]; diffStat: string; diffPatchSha256: string };
  submodules: Array<{ path: string; commit: string; state: "clean" | "modified" | "uninitialized" | "conflicted" }>;
  untracked: { total: number; included: number; omitted: number };
};

export async function collectAgentWorkspaceDiff(input: {
  definition: Record<string, unknown>;
  subjectSubdir: string;
  worktreePath: string;
  statusShort: string;
  trackedDiffStat: string;
  trackedPatchText: string;
  requestGit: (args: string[]) => Promise<CommandResult>;
  requestProcess: (command: string, args: string[]) => Promise<CommandResult>;
}) {
  const config = diffCollectionConfig(input.definition);
  const roots = statusPaths(input.statusShort, "??");
  const requests: Array<{ label: string; result: CommandResult }> = [];
  const included: AgentDiffFileRecord[] = [];
  const omitted: AgentDiffFileRecord[] = [];
  const submoduleStatus = await input.requestGit(["submodule", "status", "--recursive"]);
  requests.push({ label: "submodule-status", result: submoduleStatus });
  const submodules = parseSubmodules(text(submoduleStatus.stdout));

  if (roots.length > 0) {
    const lsFiles = await input.requestGit(["ls-files", "--others", "--exclude-standard", "--", ...roots]);
    requests.push({ label: "untracked-ls-files", result: lsFiles });
    const paths = unique(text(lsFiles.stdout).split(/\r?\n/u).map(repoPath).filter(Boolean));
    const metadata = await fileMetadata(input, paths);
    requests.push(...metadata.requests);
    for (const filePath of paths) {
      const decision = classify(filePath, input.subjectSubdir, config);
      const record = compact({ path: filePath, source: "untracked" as const, reason: decision.reason, rule: decision.rule, ...metadata.records.get(filePath) });
      (decision.omit ? omitted : included).push(record);
    }
  }

  const patches: string[] = [];
  const stats: string[] = [];
  for (const file of included) {
    const stat = await input.requestGit(["diff", "--no-index", "--stat", "--", "/dev/null", file.path]);
    const patch = await input.requestGit(["diff", "--no-index", "--binary", "--", "/dev/null", file.path]);
    requests.push({ label: `untracked-diff-stat:${file.path}`, result: stat }, { label: `untracked-diff-patch:${file.path}`, result: patch });
    stats.push(text(stat.stdout));
    patches.push(normalizeNewFilePatch(text(patch.stdout), file.path));
  }

  const collection: AgentDiffCollectionSummary = {
    contractVersion: "hwpod-case-run-diff-collection-v1",
    included,
    omitted,
    config,
    tracked: { paths: patchPaths(input.trackedPatchText), diffStat: input.trackedDiffStat, diffPatchSha256: sha256(input.trackedPatchText) },
    submodules,
    untracked: { total: included.length + omitted.length, included: included.length, omitted: omitted.length }
  };
  return {
    patchText: joinPatch(input.trackedPatchText, patches.join("\n"), omitted),
    diffStat: joinStat(input.trackedDiffStat, stats.join("\n"), omitted),
    collection,
    requests
  };
}

function parseSubmodules(value: string) {
  return value.split(/\r?\n/u).flatMap((line) => {
    const match = line.match(/^([ +\-U])([0-9a-f]{7,64})\s+(.+?)(?:\s+\(.*\))?$/iu);
    if (!match) return [];
    const marker = match[1];
    return [{
      path: repoPath(match[3]),
      commit: match[2],
      state: marker === "-" ? "uninitialized" as const : marker === "+" ? "modified" as const : marker === "U" ? "conflicted" as const : "clean" as const
    }];
  });
}

async function fileMetadata(input: Parameters<typeof collectAgentWorkspaceDiff>[0], paths: string[]) {
  const records = new Map<string, { bytes?: number; sha256?: string }>();
  if (paths.length === 0) return { records, requests: [] as Array<{ label: string; result: CommandResult }> };
  const script = [
    "const fs=require('fs');const path=require('path');const crypto=require('crypto');",
    "const root=String(process.argv[1]||'');const files=process.argv.slice(2);",
    "const abs=p=>/^[A-Za-z]:[\\\\/]/.test(p)||p.startsWith('/')||p.startsWith('\\\\\\\\');",
    "const full=p=>abs(p)?p:(root.includes('\\\\')?path.win32.join(root,p):path.join(root,p));",
    "for(const rel of files){try{const buf=fs.readFileSync(full(rel));console.log(JSON.stringify({path:rel,bytes:buf.length,sha256:crypto.createHash('sha256').update(buf).digest('hex')}));}catch(error){console.log(JSON.stringify({path:rel,error:String(error&&error.message||error)}));}}"
  ].join("");
  const result = await input.requestProcess("node", ["-e", script, input.worktreePath, ...paths]);
  for (const line of text(result.stdout).split(/\r?\n/u)) {
    try {
      const value = JSON.parse(line);
      const filePath = repoPath(value.path);
      if (filePath) records.set(filePath, compact({ bytes: Number.isFinite(value.bytes) ? value.bytes : undefined, sha256: text(value.sha256) }));
    } catch {}
  }
  return { records, requests: [{ label: "untracked-file-metadata", result }] };
}

function diffCollectionConfig(definition: Record<string, unknown>) {
  const raw = record(definition.diffCollection ?? definition.agentDiffCollection);
  return {
    include: rules(raw.include ?? raw.includes, "included by CaseRun diff collection rule"),
    ignore: rules(raw.ignore ?? raw.ignores ?? raw.omit ?? raw.omitted, "omitted by CaseRun diff collection rule")
  };
}

function rules(value: unknown, defaultReason: string): DiffCollectionRule[] {
  if (Array.isArray(value)) return value.flatMap((item) => rules(item, defaultReason));
  if (typeof value === "string") return repoPath(value) ? [{ pattern: repoPath(value), reason: defaultReason }] : [];
  const raw = record(value);
  const pattern = text(raw.pattern ?? raw.path ?? raw.glob);
  if (pattern) return [{ pattern: repoPath(pattern), reason: text(raw.reason) || defaultReason }];
  return Object.entries(raw).flatMap(([key, item]) => repoPath(key) ? [{ pattern: repoPath(key), reason: typeof item === "string" ? item : text(record(item).reason) || defaultReason }] : []);
}

function classify(filePath: string, subdir: string, config: { include: DiffCollectionRule[]; ignore: DiffCollectionRule[] }) {
  const candidates = unique([filePath, filePath.startsWith(`${repoPath(subdir)}/`) ? filePath.slice(repoPath(subdir).length + 1) : filePath]);
  const matches = (rule: DiffCollectionRule) => candidates.some((candidate) => globMatch(rule.pattern, candidate));
  const include = config.include.find(matches);
  const ignore = config.ignore.find(matches);
  if (ignore && !include) return { omit: true, reason: ignore.reason, rule: ignore.pattern };
  if (include) return { omit: false, reason: include.reason, rule: include.pattern };
  return { omit: false, reason: "included by default because untracked files are part of the agent diff", rule: "default-include-untracked" };
}

function globMatch(patternInput: string, pathInput: string) {
  const pattern = repoPath(patternInput);
  const filePath = repoPath(pathInput);
  if (!pattern || !filePath) return false;
  if (!/[?*]/u.test(pattern)) return filePath === pattern || filePath.startsWith(`${pattern}/`);
  if (pattern.endsWith("/**") && filePath === pattern.slice(0, -3)) return true;
  let source = "";
  for (let index = 0; index < pattern.length; index += 1) {
    const char = pattern[index];
    if (char === "*") { source += pattern[index + 1] === "*" ? ".*" : "[^/]*"; if (pattern[index + 1] === "*") index += 1; }
    else if (char === "?") source += "[^/]";
    else source += char?.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
  }
  return new RegExp(`^${source}${pattern.endsWith("/") ? ".*" : ""}$`, "u").test(filePath);
}

function statusPaths(status: string, marker: string) { return unique(status.split(/\r?\n/u).flatMap((line) => line.startsWith(`${marker} `) ? [repoPath(parseQuoted(line.slice(3)))] : []).filter(Boolean)); }
function parseQuoted(value: string) { const raw = value.trim(); if (!raw.startsWith('"')) return raw; try { return JSON.parse(raw); } catch { return raw.replace(/^"|"$/gu, ""); } }
function patchPaths(value: string) { return unique(text(value).split(/\r?\n/u).flatMap((line) => line.match(/^diff --git a\/(.*?) b\/(.*)$/u)?.[2] ?? []).map(repoPath)); }
function normalizeNewFilePatch(value: string, filePath: string) { const rel = repoPath(filePath); return text(value).replace(/^diff --git a\/dev\/null b\/(.*?)$/mu, `diff --git a/${rel} b/${rel}`).replace(/^--- a\/dev\/null$/mu, "--- /dev/null").replace(/^\+\+\+ b\/(.*?)$/mu, `+++ b/${rel}`); }
function joinPatch(tracked: string, untracked: string, omitted: AgentDiffFileRecord[]) { return [text(tracked), text(untracked), omitted.length ? ["# CaseRun diffCollection omitted files from patch body:", ...omitted.map((file) => `# - ${file.path} rule=${file.rule ?? ""} reason=${file.reason ?? ""} bytes=${file.bytes ?? ""} sha256=${file.sha256 ?? ""}`)].join("\n") : ""].filter(Boolean).join("\n"); }
function joinStat(tracked: string, untracked: string, omitted: AgentDiffFileRecord[]) { return [text(tracked), text(untracked), omitted.length ? ["[omitted by CaseRun diffCollection]", ...omitted.map((file) => ` ${file.path} | omitted rule=${file.rule ?? ""} reason=${file.reason ?? ""} bytes=${file.bytes ?? ""} sha256=${file.sha256 ?? ""}`)].join("\n") : ""].filter(Boolean).join("\n"); }
function repoPath(value: unknown) { return text(value).replace(/\\/gu, "/").replace(/^\.\//u, "").replace(/^\/+|\/+$/gu, ""); }
function text(value: unknown) { return typeof value === "string" ? value.trim() : value == null ? "" : String(value).trim(); }
function record(value: unknown): Record<string, unknown> { return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {}; }
function compact<T extends Record<string, unknown>>(value: T) { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== "")) as T; }
function unique(values: string[]) { return [...new Set(values.filter(Boolean))]; }
