const riskRank = Object.freeze({ low: 0, medium: 1, high: 2 });

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

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

function isRecord(value) {
  return value !== null && typeof value === "object" && !Array.isArray(value);
}

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

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

function assertComparableAudit(audit, label) {
  if (
    !isRecord(audit) ||
    ![2, 3, 4].includes(audit.schemaVersion) ||
    audit.artifactType !== "runrail-candidate-branch-review" ||
    audit.product !== "Runrail Local v1" ||
    typeof audit.runId !== "string" ||
    !/^RRV-[A-F0-9]{16}-[a-f0-9]{8}$/.test(audit.runId) ||
    typeof audit.checksumSha256 !== "string" ||
    !/^[a-f0-9]{64}$/.test(audit.checksumSha256)
  ) {
    fail("comparison-audit-invalid", `${label} must be a verified Runrail schema-v2, schema-v3, or schema-v4 candidate audit.`);
  }
  if (
    !isRecord(audit.source) ||
    !/^[a-f0-9]{40,64}$/.test(audit.source.baseCommit || "") ||
    !/^[a-f0-9]{40,64}$/.test(audit.source.candidateCommit || "") ||
    audit.source.sourceCheckoutUnchanged !== true ||
    audit.source.sourceApplyPerformed !== false
  ) {
    fail("comparison-source-boundary", `${label} does not preserve the Runrail source boundary.`);
  }
  if (
    !isRecord(audit.scope) ||
    audit.scope.projectTestsExecuted !== false ||
    audit.scope.semanticCorrectnessEvaluated !== false ||
    audit.scope.securityReviewPerformed !== false
  ) {
    fail("comparison-scope-boundary", `${label} overstates the Runrail review scope.`);
  }
  if (
    !isRecord(audit.decision) ||
    !(audit.decision.riskLevel in riskRank) ||
    audit.decision.humanApprovalRequired !== true ||
    audit.decision.mergeReady !== false ||
    audit.decision.projectTestsRequired !== true ||
    !Array.isArray(audit.decision.findings) ||
    !Array.isArray(audit.decision.candidateIds)
  ) {
    fail("comparison-decision-boundary", `${label} does not preserve the deterministic decision boundary.`);
  }
  if (
    !isRecord(audit.policy) ||
    !Array.isArray(audit.policy.changedFiles) ||
    audit.policy.changedFiles.length !== audit.policy.changedFileCount ||
    audit.policy.arbitraryCommandAccepted !== false
  ) {
    fail("comparison-policy-boundary", `${label} does not preserve the deterministic policy boundary.`);
  }
  if (
    !isRecord(audit.boundaries) ||
    audit.boundaries.networkRequired !== false ||
    audit.boundaries.browserExecutionEnabled !== false ||
    audit.boundaries.repositoryUploadEnabled !== false ||
    audit.boundaries.automaticSourceApplyEnabled !== false ||
    audit.boundaries.learnedRuntimeAuthorityAdded !== false
  ) {
    fail("comparison-authority-boundary", `${label} expands authority beyond a local Runrail review.`);
  }
}

function findingSummary(finding) {
  return {
    id: finding.id,
    severity: finding.severity,
    title: finding.title,
    fileCount: finding.fileCount
  };
}

function auditSummary(audit) {
  return {
    runId: audit.runId,
    checksumSha256: audit.checksumSha256,
    baseCommit: audit.source.baseCommit,
    candidateCommit: audit.source.candidateCommit,
    preflightPassed: audit.preflightPassed,
    verifierPassed: audit.verifier.passed,
    riskLevel: audit.decision.riskLevel,
    recommendation: audit.decision.recommendation,
    findingCounts: { ...audit.decision.findingCounts },
    changedFileCount: audit.policy.changedFileCount,
    diffLineCount: audit.policy.diffLineCount,
    reviewContractSha256: audit.schemaVersion >= 4 ? audit.reviewContract.contractSha256 : null,
    reviewContractSource: audit.schemaVersion >= 4 ? audit.reviewContract.source : "legacy-unreported"
  };
}

function compareFindings(before, after) {
  const beforeById = new Map(before.decision.findings.map((finding) => [finding.id, finding]));
  const afterById = new Map(after.decision.findings.map((finding) => [finding.id, finding]));
  const added = [];
  const resolved = [];
  const unchanged = [];
  const changed = [];
  const ids = [...new Set([...beforeById.keys(), ...afterById.keys()])].sort();
  for (const id of ids) {
    const previous = beforeById.get(id);
    const current = afterById.get(id);
    if (!previous) added.push(findingSummary(current));
    else if (!current) resolved.push(findingSummary(previous));
    else if (stableRunrailComparisonJson(previous) === stableRunrailComparisonJson(current)) {
      unchanged.push(findingSummary(current));
    } else {
      changed.push({
        id,
        before: findingSummary(previous),
        after: findingSummary(current)
      });
    }
  }
  return { added, resolved, changed, unchanged };
}

function compareFiles(before, after) {
  const beforeByPath = new Map(before.policy.changedFiles.map((row) => [row.path, row.status]));
  const afterByPath = new Map(after.policy.changedFiles.map((row) => [row.path, row.status]));
  const added = [];
  const removed = [];
  const statusChanged = [];
  const unchanged = [];
  const paths = [...new Set([...beforeByPath.keys(), ...afterByPath.keys()])].sort();
  for (const relativePath of paths) {
    const previousStatus = beforeByPath.get(relativePath);
    const currentStatus = afterByPath.get(relativePath);
    if (!previousStatus) added.push({ status: currentStatus, path: relativePath });
    else if (!currentStatus) removed.push({ status: previousStatus, path: relativePath });
    else if (previousStatus !== currentStatus) {
      statusChanged.push({ path: relativePath, beforeStatus: previousStatus, afterStatus: currentStatus });
    } else {
      unchanged.push({ status: currentStatus, path: relativePath });
    }
  }
  return { added, removed, statusChanged, unchanged };
}

function relationship(before, after) {
  if (before.source.baseCommit === after.source.baseCommit) return "same-base";
  if (before.source.candidateCommit === after.source.baseCommit) return "linear-successor";
  fail(
    "audit-comparison-not-related",
    "The revised audit must use the same base commit or start from the baseline candidate commit."
  );
}

function reviewDirection(before, after, findings, files) {
  if (
    (before.schemaVersion >= 4) !== (after.schemaVersion >= 4) ||
    (
      before.schemaVersion >= 4 &&
      after.schemaVersion >= 4 &&
      before.reviewContract.contractSha256 !== after.reviewContract.contractSha256
    )
  ) {
    return "policy-basis-changed";
  }
  const delta = riskRank[after.decision.riskLevel] - riskRank[before.decision.riskLevel];
  if (delta < 0) return "decreased";
  if (delta > 0) return "increased";
  const evidenceChanged =
    findings.added.length > 0 ||
    findings.resolved.length > 0 ||
    findings.changed.length > 0 ||
    files.added.length > 0 ||
    files.removed.length > 0 ||
    files.statusChanged.length > 0 ||
    before.policy.diffLineCount !== after.policy.diffLineCount ||
    before.verifier.passed !== after.verifier.passed;
  return evidenceChanged ? "changed" : "unchanged";
}

export function compareRunrailReviewAudits(before, after) {
  assertComparableAudit(before, "The baseline audit");
  assertComparableAudit(after, "The revised audit");
  if (before.checksumSha256 === after.checksumSha256 || before.runId === after.runId) {
    fail("audit-comparison-identical", "The baseline and revised audits must be different Runrail review artifacts.");
  }
  if (before.source.candidateCommit === after.source.candidateCommit) {
    fail("audit-comparison-identical-candidate", "The revised audit must identify a different candidate commit.");
  }

  const relationshipType = relationship(before, after);
  const findings = compareFindings(before, after);
  const files = compareFiles(before, after);
  const direction = reviewDirection(before, after, findings, files);
  const body = {
    schemaVersion: 1,
    artifactType: "runrail-review-comparison",
    product: "Runrail Local v1",
    comparisonId: `RRC-${before.checksumSha256.slice(0, 12).toUpperCase()}-${after.checksumSha256.slice(0, 12).toUpperCase()}`,
    relationship: {
      type: relationshipType,
      originAuthenticated: false
    },
    before: auditSummary(before),
    after: auditSummary(after),
    reviewPriority: {
      before: before.decision.riskLevel,
      after: after.decision.riskLevel,
      rankDelta: riskRank[after.decision.riskLevel] - riskRank[before.decision.riskLevel],
      direction
    },
    evidenceDelta: {
      changedFileCount: after.policy.changedFileCount - before.policy.changedFileCount,
      diffLineCount: after.policy.diffLineCount - before.policy.diffLineCount,
      findingCount: after.decision.findings.length - before.decision.findings.length
    },
    reviewContractDelta: {
      comparablePolicyBasis:
        before.schemaVersion >= 4 &&
        after.schemaVersion >= 4 &&
        before.reviewContract.contractSha256 === after.reviewContract.contractSha256,
      beforeSha256: before.schemaVersion >= 4 ? before.reviewContract.contractSha256 : null,
      afterSha256: after.schemaVersion >= 4 ? after.reviewContract.contractSha256 : null,
      beforeSource: before.schemaVersion >= 4 ? before.reviewContract.source : "legacy-unreported",
      afterSource: after.schemaVersion >= 4 ? after.reviewContract.source : "legacy-unreported"
    },
    findings,
    files,
    nextReview: {
      recommendation: after.decision.recommendation,
      candidateIds: [...after.decision.candidateIds],
      humanApprovalRequired: true,
      projectTestsRequired: true,
      mergeReady: false
    },
    boundaries: {
      localComparisonOnly: true,
      networkRequired: false,
      repositoryUploadEnabled: false,
      browserExecutionEnabled: false,
      projectTestsExecuted: false,
      semanticCorrectnessEvaluated: false,
      securityReviewPerformed: false,
      automaticSourceApplyEnabled: false,
      mergeReady: false,
      humanApprovalRequired: true,
      originAuthenticated: false,
      learnedRuntimeAuthorityAdded: false
    }
  };
  return body;
}

function escapeHtml(value) {
  return String(value)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
}

function escapeMarkdown(value) {
  return String(value)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll("`", "&#96;");
}

function directionLabel(direction) {
  return {
    decreased: "Review priority decreased",
    increased: "Review priority increased",
    changed: "Review evidence changed",
    unchanged: "Review evidence pattern unchanged",
    "policy-basis-changed": "Review contract changed"
  }[direction];
}

function markdownFindingRows(rows) {
  return rows.length > 0
    ? rows.map((finding) => `- **${finding.severity.toUpperCase()} - ${escapeMarkdown(finding.title)}** (\`${escapeMarkdown(finding.id)}\`)`).join("\n")
    : "- None.";
}

export function renderRunrailReviewComparisonMarkdown(comparison) {
  return `# Runrail revision comparison

Comparison: ${comparison.comparisonId}

Relationship: ${comparison.relationship.type}

Outcome: ${directionLabel(comparison.reviewPriority.direction)}

Baseline review priority: ${comparison.reviewPriority.before}

Revised review priority: ${comparison.reviewPriority.after}

## Resolved deterministic findings

${markdownFindingRows(comparison.findings.resolved)}

## Added deterministic findings

${markdownFindingRows(comparison.findings.added)}

## Evidence delta

- Changed-file count delta: ${comparison.evidenceDelta.changedFileCount}
- Diff-line count delta: ${comparison.evidenceDelta.diffLineCount}
- Finding-count delta: ${comparison.evidenceDelta.findingCount}
- Comparable review-contract basis: ${comparison.reviewContractDelta.comparablePolicyBasis ? "yes" : "no"}

## Decision boundary

- Human approval required: yes
- Project tests required: yes
- Merge ready: no
- Repository upload: disabled
- Automatic source apply: disabled
- Origin authenticated: no

This comparison matches the embedded content checksum, deterministic policy, and baseline review-contract evaluation of each local audit before comparing their declared evidence. A changed contract basis prevents a decreased-priority claim. It does not authenticate either audit's creator, run project tests, determine semantic correctness, perform a security review, apply source changes, or approve a merge.
`;
}

function htmlFindingRows(rows, emptyLabel) {
  if (rows.length === 0) return `<li class="empty">${escapeHtml(emptyLabel)}</li>`;
  return rows
    .map(
      (finding) =>
        `<li><span class="severity severity-${escapeHtml(finding.severity)}">${escapeHtml(finding.severity)}</span><div><strong>${escapeHtml(finding.title)}</strong><small>${escapeHtml(finding.id)} · ${finding.fileCount} file${finding.fileCount === 1 ? "" : "s"}</small></div></li>`
    )
    .join("");
}

export function renderRunrailReviewComparisonHtml(comparison) {
  const checksum = comparison.checksumSha256 || "not-recorded";
  return `<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src 'none'; media-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'">
  <title>Runrail revision comparison</title>
  <style>
    :root{color-scheme:light;--ink:#111923;--muted:#65717c;--line:#d8dde1;--paper:#fff;--wash:#f4f6f7;--green:#0c7453;--amber:#805500;--red:#9f2d23}*{box-sizing:border-box}body{margin:0;background:var(--wash);color:var(--ink);font:15px/1.55 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}main{width:min(1120px,calc(100% - 32px));margin:0 auto;padding:42px 0 68px}header{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:24px;align-items:end;padding-bottom:28px;border-bottom:1px solid var(--line)}.eyebrow{margin:0;color:var(--green);font-size:11px;font-weight:900;text-transform:uppercase}h1{margin:6px 0 7px;font-size:clamp(32px,5vw,54px);line-height:1.02}p{margin:0;color:var(--muted)}.direction{min-width:190px;padding:15px;border:1px solid currentColor;text-align:center;font-weight:900;text-transform:uppercase}.direction-decreased{color:var(--green)}.direction-increased{color:var(--red)}.direction-changed,.direction-policy-basis-changed{color:var(--amber)}.direction-unchanged{color:#365166}.metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));margin:0;background:var(--paper);border-bottom:1px solid var(--line)}.metrics div{min-height:100px;padding:18px;border-right:1px solid var(--line)}.metrics div:last-child{border-right:0}dt{color:var(--muted);font-size:12px}dd{margin:7px 0 0;font-weight:800}.layout{display:grid;grid-template-columns:1fr 1fr;gap:34px;padding-top:34px}.panel{min-width:0}.panel h2{margin:0 0 12px;font-size:19px}.panel ul{margin:0;padding:0;list-style:none;border-top:1px solid var(--line)}.panel li{display:grid;grid-template-columns:62px minmax(0,1fr);gap:12px;padding:15px 0;border-bottom:1px solid var(--line)}.panel li.empty{display:block;color:var(--muted)}.severity{font-size:10px;font-weight:900;text-transform:uppercase}.severity-high{color:var(--red)}.severity-medium{color:var(--amber)}.severity-info{color:var(--green)}.panel strong,.panel small{display:block}.panel small{margin-top:3px;color:var(--muted)}.boundary{grid-column:1/-1;display:grid;grid-template-columns:minmax(0,1fr) minmax(280px,.55fr);gap:28px;margin-top:12px;padding:24px;border:1px solid var(--line);background:var(--paper)}.boundary h2{margin:4px 0 8px}.boundary dl{margin:0}.boundary dl div{display:flex;justify-content:space-between;gap:16px;padding:8px 0;border-top:1px solid var(--line)}.checksum{margin-top:28px;font:10px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace;overflow-wrap:anywhere}@media(max-width:720px){header,.layout,.boundary{grid-template-columns:1fr}.direction{text-align:left}.metrics{grid-template-columns:1fr 1fr}.metrics div:nth-child(2){border-right:0}.metrics div:nth-child(-n+2){border-bottom:1px solid var(--line)}}
  </style>
</head>
<body>
<main>
  <header>
    <div><p class="eyebrow">Runrail local revision review</p><h1>${escapeHtml(directionLabel(comparison.reviewPriority.direction))}</h1><p>${escapeHtml(comparison.comparisonId)} · ${escapeHtml(comparison.relationship.type)} evidence relationship</p></div>
    <div class="direction direction-${escapeHtml(comparison.reviewPriority.direction)}">${escapeHtml(comparison.reviewPriority.before)} → ${escapeHtml(comparison.reviewPriority.after)}</div>
  </header>
  <dl class="metrics">
    <div><dt>Resolved findings</dt><dd>${comparison.findings.resolved.length}</dd></div>
    <div><dt>Added findings</dt><dd>${comparison.findings.added.length}</dd></div>
    <div><dt>Changed files delta</dt><dd>${comparison.evidenceDelta.changedFileCount}</dd></div>
    <div><dt>Project tests</dt><dd>Not run</dd></div>
  </dl>
  <div class="layout">
    <section class="panel"><h2>Resolved deterministic findings</h2><ul>${htmlFindingRows(comparison.findings.resolved, "No deterministic findings were resolved.")}</ul></section>
    <section class="panel"><h2>Added deterministic findings</h2><ul>${htmlFindingRows(comparison.findings.added, "No deterministic findings were added.")}</ul></section>
    <section class="boundary">
      <div><p class="eyebrow">Decision boundary</p><h2>Human approval is still required.</h2><p>${comparison.reviewContractDelta.comparablePolicyBasis ? "The baseline review-contract basis is comparable." : "The review-contract basis changed, so no decreased-priority claim is made."} This local comparison does not authenticate origin, run project tests, determine semantic correctness, perform a security review, apply source changes, or approve a merge.</p></div>
      <dl><div><dt>Repository upload</dt><dd>Disabled</dd></div><div><dt>Automatic apply</dt><dd>Disabled</dd></div><div><dt>Merge ready</dt><dd>No</dd></div><div><dt>Origin authenticated</dt><dd>No</dd></div></dl>
    </section>
  </div>
  <p class="checksum">Comparison content checksum: ${escapeHtml(checksum)}</p>
</main>
</body>
</html>
`;
}
