import { createHash } from "node:crypto";
import { constants as fsConstants } from "node:fs";
import {
  chmod,
  lstat,
  mkdir,
  open as openFile,
  readdir,
  readFile,
  realpath
} from "node:fs/promises";
import path from "node:path";
import {
  verifyRunrailAgentAssuranceResult
} from "./runrail-agent-assurance-core.mjs";

const agentIds = new Set(["codex", "claude", "gemini", "grok", "other"]);
const statuses = new Set([
  "blocked",
  "needs-correction",
  "ready-for-human-decision"
]);
const receiptKeys = Object.freeze([
  "schemaVersion",
  "artifactType",
  "profile",
  "outcomeId",
  "repoFingerprint",
  "beforeReviewId",
  "currentReviewId",
  "agentId",
  "recordedAt",
  "assuranceResultChecksumSha256",
  "status",
  "metrics",
  "boundaries",
  "checksumSha256"
]);
const metricsKeys = Object.freeze([
  "verifiedRevisionCount",
  "resolvedCount",
  "remainingCount",
  "newCount",
  "reintroducedCount",
  "coverageAttentionComplete",
  "readyForHumanDecision"
]);
const boundaryKeys = Object.freeze([
  "repositoryLocal",
  "sourceFree",
  "pathFree",
  "explicitAgentLabel",
  "verifiedRerunRequired",
  "globalModelScore",
  "crossRepositoryRanking",
  "agentQualityClaim",
  "agentOutputIncluded",
  "sourceIncluded",
  "diffIncluded",
  "commandExecuted",
  "sourceMutation",
  "networkUsed",
  "authority"
]);

export const RUNRAIL_AGENT_OUTCOME_LEDGER = Object.freeze({
  schemaVersion: 1,
  artifactType: "runrail-agent-outcome",
  profile: "runrail-agent-outcome-ledger-v1",
  summaryArtifactType: "runrail-agent-outcome-summary",
  outputDirectory: "agent-outcomes",
  maximumOutcomesPerRepository: 1_000,
  agents: Object.freeze([...agentIds]),
  authority: "none"
});

export class RunrailAgentOutcomeLedgerError extends Error {
  constructor(code, message) {
    super(message);
    this.name = "RunrailAgentOutcomeLedgerError";
    this.code = code;
  }
}

function fail(code, message) {
  throw new RunrailAgentOutcomeLedgerError(code, message);
}

function sha256(value) {
  return createHash("sha256").update(value).digest("hex");
}

function stableValue(value) {
  if (Array.isArray(value)) return value.map(stableValue);
  if (value && typeof value === "object") {
    return Object.fromEntries(
      Object.keys(value)
        .sort()
        .map((key) => [key, stableValue(value[key])])
    );
  }
  return value;
}

export function stableRunrailAgentOutcomeJson(value) {
  return `${JSON.stringify(stableValue(value), null, 2)}\n`;
}

function isPlainRecord(value) {
  return (
    value !== null &&
    typeof value === "object" &&
    !Array.isArray(value) &&
    Object.getPrototypeOf(value) === Object.prototype
  );
}

function exactKeys(value, expected, code = "agent-outcome-invalid") {
  if (!isPlainRecord(value)) fail(code, "Agent outcome must be a plain object.");
  const actual = Object.keys(value).sort();
  const required = [...expected].sort();
  if (
    actual.length !== required.length ||
    actual.some((key, index) => key !== required[index])
  ) {
    fail(code, "Agent outcome contains missing or unsupported fields.");
  }
}

function safeReviewId(value) {
  const text = String(value || "");
  if (
    text.length < 8 ||
    text.length > 80 ||
    !/^[A-Za-z0-9._:-]+$/.test(text)
  ) {
    fail("agent-outcome-review-invalid", "Agent outcome review ID is invalid.");
  }
  return text;
}

function safeRepoFingerprint(value) {
  const text = String(value || "");
  if (!/^RRF-[A-F0-9]{64}$/.test(text)) {
    fail(
      "agent-outcome-repository-invalid",
      "Agent outcome repository fingerprint is invalid."
    );
  }
  return text;
}

function safeAgentId(value) {
  const text = String(value || "").toLowerCase();
  if (!agentIds.has(text)) {
    fail(
      "agent-outcome-agent-invalid",
      "Agent outcome label must use a fixed supported agent ID."
    );
  }
  return text;
}

function safeChecksum(value) {
  const text = String(value || "").toLowerCase();
  if (!/^[a-f0-9]{64}$/.test(text)) {
    fail("agent-outcome-checksum-invalid", "Agent outcome checksum is invalid.");
  }
  return text;
}

function safeTimestamp(value) {
  const text = String(value || "");
  const timestamp = Date.parse(text);
  if (
    !Number.isFinite(timestamp) ||
    new Date(timestamp).toISOString() !== text
  ) {
    fail("agent-outcome-time-invalid", "Agent outcome timestamp is invalid.");
  }
  return text;
}

function safeCount(value) {
  if (!Number.isSafeInteger(value) || value < 0 || value > 10_000) {
    fail("agent-outcome-metrics-invalid", "Agent outcome metric is invalid.");
  }
  return value;
}

function expectedBoundaries() {
  return {
    repositoryLocal: true,
    sourceFree: true,
    pathFree: true,
    explicitAgentLabel: true,
    verifiedRerunRequired: true,
    globalModelScore: false,
    crossRepositoryRanking: false,
    agentQualityClaim: false,
    agentOutputIncluded: false,
    sourceIncluded: false,
    diffIncluded: false,
    commandExecuted: false,
    sourceMutation: false,
    networkUsed: false,
    authority: "none"
  };
}

export function createRunrailAgentOutcome({
  repoFingerprint,
  beforeReviewId,
  currentReviewId,
  agentId,
  recordedAt,
  assuranceResult
}) {
  const verifiedResult = verifyRunrailAgentAssuranceResult(assuranceResult);
  const safeRepo = safeRepoFingerprint(repoFingerprint);
  const safeBefore = safeReviewId(beforeReviewId);
  const safeCurrent = safeReviewId(currentReviewId);
  const safeAgent = safeAgentId(agentId);
  if (safeBefore === safeCurrent) {
    fail(
      "agent-outcome-review-invalid",
      "Agent outcome requires a distinct verified rerun."
    );
  }
  const body = {
    schemaVersion: RUNRAIL_AGENT_OUTCOME_LEDGER.schemaVersion,
    artifactType: RUNRAIL_AGENT_OUTCOME_LEDGER.artifactType,
    profile: RUNRAIL_AGENT_OUTCOME_LEDGER.profile,
    outcomeId: `RAO-${sha256(
      `${safeRepo}\0${safeBefore}\0${safeCurrent}`
    )
      .slice(0, 24)
      .toUpperCase()}`,
    repoFingerprint: safeRepo,
    beforeReviewId: safeBefore,
    currentReviewId: safeCurrent,
    agentId: safeAgent,
    recordedAt: safeTimestamp(recordedAt),
    assuranceResultChecksumSha256: verifiedResult.checksumSha256,
    status: verifiedResult.status,
    metrics: {
      verifiedRevisionCount: 1,
      resolvedCount: verifiedResult.summary.resolvedCount,
      remainingCount: verifiedResult.summary.remainingCount,
      newCount: verifiedResult.summary.newCount,
      reintroducedCount: verifiedResult.summary.reintroducedCount,
      coverageAttentionComplete:
        verifiedResult.currentEvidence.attentionComplete,
      readyForHumanDecision:
        verifiedResult.status === "ready-for-human-decision"
    },
    boundaries: expectedBoundaries()
  };
  return verifyRunrailAgentOutcome({
    ...body,
    checksumSha256: sha256(stableRunrailAgentOutcomeJson(body))
  });
}

export function verifyRunrailAgentOutcome(outcome) {
  exactKeys(outcome, receiptKeys);
  if (
    outcome.schemaVersion !== RUNRAIL_AGENT_OUTCOME_LEDGER.schemaVersion ||
    outcome.artifactType !== RUNRAIL_AGENT_OUTCOME_LEDGER.artifactType ||
    outcome.profile !== RUNRAIL_AGENT_OUTCOME_LEDGER.profile ||
    !/^RAO-[A-F0-9]{24}$/.test(String(outcome.outcomeId || "")) ||
    !statuses.has(outcome.status)
  ) {
    fail("agent-outcome-invalid", "Agent outcome identity is invalid.");
  }
  safeRepoFingerprint(outcome.repoFingerprint);
  safeReviewId(outcome.beforeReviewId);
  safeReviewId(outcome.currentReviewId);
  safeAgentId(outcome.agentId);
  safeTimestamp(outcome.recordedAt);
  safeChecksum(outcome.assuranceResultChecksumSha256);
  exactKeys(outcome.metrics, metricsKeys, "agent-outcome-metrics-invalid");
  for (const key of [
    "verifiedRevisionCount",
    "resolvedCount",
    "remainingCount",
    "newCount",
    "reintroducedCount"
  ]) {
    safeCount(outcome.metrics[key]);
  }
  if (
    outcome.metrics.verifiedRevisionCount !== 1 ||
    typeof outcome.metrics.coverageAttentionComplete !== "boolean" ||
    typeof outcome.metrics.readyForHumanDecision !== "boolean" ||
    outcome.metrics.readyForHumanDecision !==
      (outcome.status === "ready-for-human-decision")
  ) {
    fail("agent-outcome-metrics-invalid", "Agent outcome metrics are inconsistent.");
  }
  exactKeys(outcome.boundaries, boundaryKeys, "agent-outcome-boundary-invalid");
  const boundaries = expectedBoundaries();
  if (
    boundaryKeys.some(
      (key) => outcome.boundaries[key] !== boundaries[key]
    )
  ) {
    fail("agent-outcome-boundary-invalid", "Agent outcome expands authority.");
  }
  const expectedId = `RAO-${sha256(
    `${outcome.repoFingerprint}\0${outcome.beforeReviewId}\0${outcome.currentReviewId}`
  )
    .slice(0, 24)
    .toUpperCase()}`;
  if (outcome.outcomeId !== expectedId) {
    fail("agent-outcome-invalid", "Agent outcome ID is invalid.");
  }
  const { checksumSha256, ...body } = outcome;
  if (
    safeChecksum(checksumSha256) !==
    sha256(stableRunrailAgentOutcomeJson(body))
  ) {
    fail("agent-outcome-tampered", "Agent outcome checksum is invalid.");
  }
  return outcome;
}

function inside(root, candidate) {
  const relative = path.relative(root, candidate);
  return (
    relative !== "" &&
    relative !== ".." &&
    !relative.startsWith(`..${path.sep}`) &&
    !path.isAbsolute(relative)
  );
}

async function verifiedHistoryRoot(historyRoot) {
  const root = path.resolve(historyRoot);
  const entry = await lstat(root).catch(() => null);
  if (!entry || !entry.isDirectory() || entry.isSymbolicLink()) {
    fail(
      "agent-outcome-root-invalid",
      "Agent outcome history root is not an ordinary directory."
    );
  }
  if (await realpath(root) !== root) {
    fail(
      "agent-outcome-root-invalid",
      "Agent outcome history root traverses a symbolic link."
    );
  }
  return root;
}

async function outcomeRepositoryRoot(
  historyRoot,
  repoFingerprint,
  { create = true } = {}
) {
  const root = await verifiedHistoryRoot(historyRoot);
  const outputRoot = path.join(
    root,
    RUNRAIL_AGENT_OUTCOME_LEDGER.outputDirectory,
    safeRepoFingerprint(repoFingerprint)
  );
  if (!inside(root, outputRoot)) {
    fail("agent-outcome-path-invalid", "Agent outcome path escaped history.");
  }
  const parentRoot = path.dirname(outputRoot);
  const parentEntry = await lstat(parentRoot).catch(() => null);
  if (!parentEntry && !create) return null;
  if (
    parentEntry &&
    (!parentEntry.isDirectory() || parentEntry.isSymbolicLink())
  ) {
    fail("agent-outcome-path-invalid", "Agent outcome directory is unsafe.");
  }
  if (create) {
    await mkdir(outputRoot, { recursive: true, mode: 0o700 });
  }
  const outputEntry = await lstat(outputRoot).catch(() => null);
  if (!outputEntry && !create) return null;
  if (
    !outputEntry ||
    !outputEntry.isDirectory() ||
    outputEntry.isSymbolicLink() ||
    await realpath(outputRoot) !== outputRoot
  ) {
    fail("agent-outcome-path-invalid", "Agent outcome directory is unsafe.");
  }
  return outputRoot;
}

export async function writeRunrailAgentOutcome({
  historyRoot,
  outcome
}) {
  const verified = verifyRunrailAgentOutcome(outcome);
  const outputRoot = await outcomeRepositoryRoot(
    historyRoot,
    verified.repoFingerprint
  );
  const names = await readdir(outputRoot);
  const filename = `${verified.outcomeId}.json`;
  if (
    !names.includes(filename) &&
    names.length >=
      RUNRAIL_AGENT_OUTCOME_LEDGER.maximumOutcomesPerRepository
  ) {
    fail("agent-outcome-limit-reached", "Agent outcome limit was reached.");
  }
  const outcomePath = path.join(outputRoot, filename);
  if (!inside(outputRoot, outcomePath)) {
    fail("agent-outcome-path-invalid", "Agent outcome path escaped.");
  }
  const serialized = stableRunrailAgentOutcomeJson(verified);
  const existing = await lstat(outcomePath).catch(() => null);
  if (existing) {
    if (
      !existing.isFile() ||
      existing.isSymbolicLink() ||
      existing.size < 2 ||
      existing.size > 32 * 1024 ||
      await realpath(outcomePath) !== outcomePath
    ) {
      fail("agent-outcome-artifact-invalid", "Existing agent outcome is unsafe.");
    }
    const current = verifyRunrailAgentOutcome(
      JSON.parse(await readFile(outcomePath, "utf8"))
    );
    if (
      current.checksumSha256 !== verified.checksumSha256 ||
      await readFile(outcomePath, "utf8") !== serialized
    ) {
      fail(
        "agent-outcome-artifact-conflict",
        "Agent outcome label or evidence conflicts with its immutable record."
      );
    }
    return { outcome: current, outcomePath, created: false };
  }
  const handle = await openFile(
    outcomePath,
    fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY,
    0o600
  );
  try {
    await handle.writeFile(serialized, "utf8");
    await handle.sync();
  } finally {
    await handle.close();
  }
  if (process.platform !== "win32") await chmod(outcomePath, 0o600);
  return { outcome: verified, outcomePath, created: true };
}

export async function listRunrailAgentOutcomes({
  historyRoot,
  repoFingerprint
}) {
  const outputRoot = await outcomeRepositoryRoot(
    historyRoot,
    repoFingerprint,
    { create: false }
  );
  if (!outputRoot) return [];
  const names = (await readdir(outputRoot))
    .filter((name) => /^RAO-[A-F0-9]{24}\.json$/.test(name))
    .sort();
  if (
    names.length >
    RUNRAIL_AGENT_OUTCOME_LEDGER.maximumOutcomesPerRepository
  ) {
    fail("agent-outcome-limit-reached", "Agent outcome count is invalid.");
  }
  const outcomes = [];
  for (const name of names) {
    const outcomePath = path.join(outputRoot, name);
    const entry = await lstat(outcomePath);
    if (
      !entry.isFile() ||
      entry.isSymbolicLink() ||
      entry.size < 2 ||
      entry.size > 32 * 1024 ||
      await realpath(outcomePath) !== outcomePath
    ) {
      fail("agent-outcome-artifact-invalid", "Agent outcome is unsafe.");
    }
    const outcome = verifyRunrailAgentOutcome(
      JSON.parse(await readFile(outcomePath, "utf8"))
    );
    if (`${outcome.outcomeId}.json` !== name) {
      fail("agent-outcome-artifact-invalid", "Agent outcome filename is invalid.");
    }
    outcomes.push(outcome);
  }
  return outcomes.sort(
    (left, right) =>
      left.recordedAt.localeCompare(right.recordedAt) ||
      left.outcomeId.localeCompare(right.outcomeId)
  );
}

function revisionsToReadyForAgent(rows) {
  const byCurrent = new Map(
    rows.map((row) => [row.currentReviewId, row])
  );
  const completed = [];
  for (const row of rows) {
    if (!row.metrics.readyForHumanDecision) continue;
    let count = 1;
    let cursor = row;
    const visited = new Set([row.outcomeId]);
    while (byCurrent.has(cursor.beforeReviewId)) {
      const prior = byCurrent.get(cursor.beforeReviewId);
      if (
        prior.agentId !== row.agentId ||
        prior.metrics.readyForHumanDecision ||
        visited.has(prior.outcomeId)
      ) {
        break;
      }
      visited.add(prior.outcomeId);
      count += 1;
      cursor = prior;
    }
    completed.push(count);
  }
  return completed;
}

function ratio(numerator, denominator) {
  return denominator === 0
    ? null
    : Number((numerator / denominator).toFixed(4));
}

export function buildRunrailAgentOutcomeSummary({
  repoFingerprint,
  outcomes
}) {
  const safeRepo = safeRepoFingerprint(repoFingerprint);
  if (
    !Array.isArray(outcomes) ||
    outcomes.length >
      RUNRAIL_AGENT_OUTCOME_LEDGER.maximumOutcomesPerRepository
  ) {
    fail("agent-outcome-summary-invalid", "Agent outcome summary input is invalid.");
  }
  const verified = outcomes.map(verifyRunrailAgentOutcome);
  if (verified.some((outcome) => outcome.repoFingerprint !== safeRepo)) {
    fail(
      "agent-outcome-summary-invalid",
      "Agent outcome summary crossed a repository boundary."
    );
  }
  const agents = [];
  for (const agentId of RUNRAIL_AGENT_OUTCOME_LEDGER.agents) {
    const rows = verified.filter((outcome) => outcome.agentId === agentId);
    if (rows.length === 0) continue;
    const readyRows = rows.filter(
      (row) => row.metrics.readyForHumanDecision
    );
    const revisionsToReady = revisionsToReadyForAgent(rows);
    const totals = {
      resolvedCount: rows.reduce(
        (total, row) => total + row.metrics.resolvedCount,
        0
      ),
      remainingCount: rows.reduce(
        (total, row) => total + row.metrics.remainingCount,
        0
      ),
      newCount: rows.reduce(
        (total, row) => total + row.metrics.newCount,
        0
      ),
      reintroducedCount: rows.reduce(
        (total, row) => total + row.metrics.reintroducedCount,
        0
      )
    };
    agents.push({
      agentId,
      sampleSize: rows.length,
      readyOutcomeCount: readyRows.length,
      readyOutcomeRate: ratio(readyRows.length, rows.length),
      evidenceCompletionRate: ratio(
        rows.filter((row) => row.metrics.coverageAttentionComplete).length,
        rows.length
      ),
      averageRevisionsToReady:
        revisionsToReady.length === 0
          ? null
          : Number(
              (
                revisionsToReady.reduce((total, value) => total + value, 0) /
                revisionsToReady.length
              ).toFixed(2)
            ),
      totals,
      uncertainty:
        rows.length < 5
          ? "insufficient-sample"
          : rows.length < 20
            ? "directional-repository-local"
            : "repository-local-observation"
    });
  }
  const body = {
    schemaVersion: 1,
    artifactType: RUNRAIL_AGENT_OUTCOME_LEDGER.summaryArtifactType,
    profile: RUNRAIL_AGENT_OUTCOME_LEDGER.profile,
    repoFingerprint: safeRepo,
    sampleSize: verified.length,
    agents,
    boundaries: {
      repositoryLocal: true,
      sourceFree: true,
      pathFree: true,
      explicitAgentLabelsOnly: true,
      verifiedRerunsOnly: true,
      globalModelScore: false,
      crossRepositoryRanking: false,
      agentQualityClaim: false,
      sourceMutation: false,
      networkUsed: false,
      authority: "none"
    }
  };
  return {
    ...body,
    checksumSha256: sha256(stableRunrailAgentOutcomeJson(body))
  };
}
