import { createHash, randomUUID } from "node:crypto";
import { constants } from "node:fs";
import { link, lstat, open, readFile, realpath, unlink } from "node:fs/promises";
import { basename, join } from "node:path";
import { parseTaxonomy } from "./dispatch-brief";

interface VerifyRequest {
  incidentId: string;
  incidentType: string | null;
  artifact: string;
  summary?: string;
}

interface ArtifactDocument {
  kind: "incident.result";
  disposition: "resolved";
  summary: string;
}

export interface VerifiedResolutionEvidence {
  incidentId: string;
  incidentType: string | null;
  artifact: string;
  artifactSha256: string;
  sourceIncidentSha256: string;
  operatorNote: string | null;
  howSolved: string;
  evidence: string[];
  targetSkill: string | null;
}

interface ResolutionLearningOptions {
  assetsDir: string;
  workspaceFor(incidentId: string): string;
}

function sha256(value: string | Buffer): string {
  return createHash("sha256").update(value).digest("hex");
}

function safeId(value: string): boolean {
  return value === basename(value) && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value);
}

function parseArtifact(raw: string): ArtifactDocument {
  let value: unknown;
  try {
    value = JSON.parse(raw);
  } catch {
    throw new Error("resolution artifact is not valid JSON");
  }
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("resolution artifact has invalid shape");
  const record = value as Record<string, unknown>;
  const keys = Object.keys(record).sort();
  if (JSON.stringify(keys) !== JSON.stringify(["disposition", "kind", "summary"])) throw new Error("resolution artifact has invalid fields");
  if (record["kind"] !== "incident.result") throw new Error("resolution artifact has invalid kind");
  if (record["disposition"] !== "resolved") throw new Error("resolution artifact is not resolved");
  if (typeof record["summary"] !== "string" || record["summary"].trim().length === 0 || record["summary"].length > 2_000) throw new Error("resolution artifact has invalid summary");
  return { kind: "incident.result", disposition: "resolved", summary: record["summary"].trim() };
}

async function taxonomySkill(assetsDir: string, incidentType: string | null): Promise<string | null> {
  if (!incidentType) return null;
  const parsed = parseTaxonomy(await readFile(join(assetsDir, "taxonomy.json"), "utf8"));
  const entry = parsed.find((item) => item.id === incidentType);
  if (!entry) throw new Error("incident type is absent from taxonomy");
  const skill = entry["skill"];
  if (typeof skill !== "string" || !/^od-[a-z0-9-]+$/.test(skill)) throw new Error("incident taxonomy has no valid skill authority");
  return skill;
}

async function requireDurableDirectory(path: string): Promise<void> {
  const info = await lstat(path);
  if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("durable output directory is unsafe");
}

async function atomicCreateOrMatch(path: string, content: string): Promise<void> {
  try {
    const existing = await readFile(path, "utf8");
    if (existing === content) return;
    throw new Error("conflicting resolution evidence");
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
  }
  const temp = `${path}.${randomUUID()}.tmp`;
  try {
    const handle = await open(temp, "wx", 0o600);
    try {
      await handle.writeFile(content, "utf8");
      await handle.sync();
    } finally {
      await handle.close();
    }
    try {
      await link(temp, path);
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
      const existing = await readFile(path, "utf8");
      if (existing !== content) throw new Error("conflicting resolution evidence");
    }
  } finally {
    await unlink(temp).catch((error: NodeJS.ErrnoException) => {
      if (error.code !== "ENOENT") throw error;
    });
  }
}

export function createResolutionLearning({ assetsDir, workspaceFor }: ResolutionLearningOptions) {
  return {
    async verify(request: VerifyRequest): Promise<VerifiedResolutionEvidence> {
      if (!safeId(request.incidentId)) throw new Error("incident id is unsafe");
      const canonicalArtifact = join(await realpath(workspaceFor(request.incidentId)), ".overdeck", `${request.incidentId}.json`);
      if (request.artifact !== canonicalArtifact) throw new Error("resolution artifact path mismatch");
      let handle;
      try {
        handle = await open(request.artifact, constants.O_RDONLY | constants.O_NOFOLLOW);
      } catch {
        throw new Error("resolution artifact must be a regular file");
      }
      let raw: Buffer;
      try {
        const stats = await handle.stat();
        if (!stats.isFile()) throw new Error("resolution artifact must be a regular file");
        if (stats.size > 64 * 1024) throw new Error("resolution artifact is too large");
        raw = await handle.readFile();
      } finally {
        await handle.close();
      }
      const document = parseArtifact(raw.toString("utf8"));
      if (basename(request.artifact) !== `${request.incidentId}.json`) throw new Error("resolution artifact path mismatch");
      const artifactSha256 = sha256(raw);
      const sourceIncidentSha256 = sha256(JSON.stringify({ incidentId: request.incidentId, incidentType: request.incidentType, artifactSha256 }));
      return {
        incidentId: request.incidentId,
        incidentType: request.incidentType,
        artifact: request.artifact,
        artifactSha256,
        sourceIncidentSha256,
      operatorNote: request.summary?.trim() || null,
        howSolved: document.summary,
        evidence: [document.summary],
        targetSkill: await taxonomySkill(assetsDir, request.incidentType),
      };
    },
    async record(evidence: VerifiedResolutionEvidence): Promise<void> {
      if (!safeId(evidence.incidentId)) throw new Error("incident id is unsafe");
      const kbDir = join(assetsDir, "kb");
      const proposalsDir = join(assetsDir, "proposals");
      await requireDurableDirectory(kbDir);
      if (evidence.targetSkill) await requireDurableDirectory(proposalsDir);
      const kb = `---\nid: ${evidence.incidentId}\ntype: ${evidence.incidentType ?? "unresolved"}\ndate: unresolved\nartifact: ${JSON.stringify(evidence.artifact)}\nartifact_sha256: ${evidence.artifactSha256}\nsource_incident_sha256: ${evidence.sourceIncidentSha256}\n---\n${evidence.howSolved}\nEvidence: ${evidence.evidence.join("; ")}\n`;
      await atomicCreateOrMatch(join(kbDir, `${evidence.incidentId}.md`), kb);
      if (evidence.targetSkill) {
        const proposal = `${JSON.stringify({ schema: "incident-skill-update-proposal/v1", incidentId: evidence.incidentId, incidentType: evidence.incidentType, targetSkill: evidence.targetSkill, evidence: { artifact: evidence.artifact, artifactSha256: evidence.artifactSha256, sourceIncidentSha256: evidence.sourceIncidentSha256, statements: evidence.evidence }, proposedRule: evidence.howSolved }, null, 2)}\n`;
        await atomicCreateOrMatch(join(proposalsDir, `${evidence.incidentId}.json`), proposal);
      }
    },
  };
}
