export const RUNRAIL_REVIEW_CONTRACT_PATH = ".runrail/review-contract.json";
export const RUNRAIL_REVIEW_CONTRACT_PROFILE = "runrail-review-contract-v1";

const MAX_RULES = 32;
const MAX_PREFIXES_PER_RULE = 16;
const MAX_COMPANION_GROUPS = 4;
const MAX_PREFIXES_PER_COMPANION_GROUP = 8;
const MAX_CONTRACT_CHANGED_FILES = 100;
const MAX_CONTRACT_DIFF_LINES = 5_000;

const severityValues = new Set(["medium", "high"]);
const reviewLaneValues = new Set([
  "api",
  "data",
  "delivery",
  "dependency",
  "documentation",
  "policy",
  "product",
  "quality",
  "security"
]);
const evidenceValues = new Set([
  "api-compatibility-review",
  "data-migration-review",
  "delivery-review",
  "dependency-review",
  "documentation-review",
  "policy-owner-review",
  "product-owner-review",
  "project-tests",
  "rollback-review",
  "security-review"
]);
const dependencyBasenames = new Set([
  "cargo.lock",
  "cargo.toml",
  "composer.json",
  "composer.lock",
  "deno.json",
  "deno.lock",
  "gemfile",
  "gemfile.lock",
  "go.mod",
  "go.sum",
  "gradle.lockfile",
  "package-lock.json",
  "package.json",
  "pom.xml",
  "pnpm-lock.yaml",
  "poetry.lock",
  "pyproject.toml",
  "requirements.in",
  "requirements.txt",
  "uv.lock",
  "yarn.lock"
]);
const securityPathParts = new Set([
  "auth",
  "authentication",
  "authorization",
  "crypto",
  "iam",
  "permissions",
  "policy",
  "security",
  "secrets"
]);
const testPathParts = new Set(["test", "tests", "__tests__", "spec", "specs"]);
const deploymentPathParts = new Set([
  ".github",
  ".gitlab",
  "deploy",
  "deployment",
  "infra",
  "infrastructure",
  "terraform"
]);
const migrationPathParts = new Set(["migration", "migrations", "schema"]);
const sourceExtensions = new Set([
  ".c",
  ".cpp",
  ".go",
  ".java",
  ".cjs",
  ".cts",
  ".js",
  ".jsx",
  ".mjs",
  ".mts",
  ".py",
  ".rb",
  ".rs",
  ".sh",
  ".ts",
  ".tsx"
]);
const documentationExtensions = new Set([".md", ".mdx", ".rst", ".txt"]);

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

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

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

function assertExactKeys(value, expected, label) {
  if (!isRecord(value)) fail("review-contract-invalid", `${label} must be an object.`);
  const actual = Object.keys(value).sort();
  const required = [...expected].sort();
  if (actual.length !== required.length || actual.some((key, index) => key !== required[index])) {
    fail("review-contract-invalid", `${label} has an invalid field set.`);
  }
}

function compareText(left, right) {
  return left < right ? -1 : left > right ? 1 : 0;
}

function sortedUniqueStrings(values, allowed, label, maximum) {
  if (!Array.isArray(values) || values.length < 1 || values.length > maximum) {
    fail("review-contract-invalid", `${label} must contain between 1 and ${maximum} entries.`);
  }
  const normalized = values.map((value) => {
    if (typeof value !== "string" || !allowed.has(value)) {
      fail("review-contract-invalid", `${label} contains an unsupported value.`);
    }
    return value;
  });
  if (new Set(normalized).size !== normalized.length) {
    fail("review-contract-invalid", `${label} must not contain duplicate entries.`);
  }
  return normalized.sort(compareText);
}

function safePrefix(value) {
  if (
    typeof value !== "string" ||
    value.length < 1 ||
    value.length > 160 ||
    value !== value.normalize("NFC") ||
    value.startsWith("/") ||
    value.includes("\\") ||
    /[\0-\x1f\x7f\u202a-\u202e\u2066-\u2069]/u.test(value)
  ) {
    return "";
  }
  const normalized = value.endsWith("/") ? value.slice(0, -1) : value;
  const parts = normalized.split("/");
  if (
    normalized.length < 1 ||
    parts.includes("") ||
    parts.includes(".") ||
    parts.includes("..") ||
    parts.some((part) => part.endsWith(".") || part.endsWith(" ") || part.includes(":"))
  ) {
    return "";
  }
  return normalized;
}

function normalizedPrefixes(values, label, maximum) {
  if (!Array.isArray(values) || values.length < 1 || values.length > maximum) {
    fail("review-contract-invalid", `${label} must contain between 1 and ${maximum} path prefixes.`);
  }
  const prefixes = values.map((value) => {
    const prefix = safePrefix(value);
    if (!prefix) fail("review-contract-invalid", `${label} contains an unsafe path prefix.`);
    return prefix;
  });
  if (new Set(prefixes).size !== prefixes.length) {
    fail("review-contract-invalid", `${label} must not contain duplicate path prefixes.`);
  }
  return prefixes.sort(compareText);
}

function normalizedCompanionGroups(value, label) {
  if (!Array.isArray(value) || value.length > MAX_COMPANION_GROUPS) {
    fail("review-contract-invalid", `${label} must contain at most ${MAX_COMPANION_GROUPS} groups.`);
  }
  const groups = value.map((group, index) =>
    normalizedPrefixes(group, `${label}[${index}]`, MAX_PREFIXES_PER_COMPANION_GROUP)
  );
  const identities = groups.map((group) => JSON.stringify(group));
  if (new Set(identities).size !== identities.length) {
    fail("review-contract-invalid", `${label} must not contain duplicate groups.`);
  }
  return groups.sort((left, right) => compareText(JSON.stringify(left), JSON.stringify(right)));
}

function normalizeRule(rule) {
  assertExactKeys(
    rule,
    ["id", "pathPrefixes", "severity", "reviewLane", "requiredEvidence", "companionPathGroups"],
    "Review contract rule"
  );
  if (typeof rule.id !== "string" || !/^[a-z][a-z0-9-]{2,47}$/.test(rule.id)) {
    fail("review-contract-invalid", "Review contract rule IDs must be bounded lowercase identifiers.");
  }
  if (!severityValues.has(rule.severity)) {
    fail("review-contract-invalid", "Review contract rule severity must be medium or high.");
  }
  if (!reviewLaneValues.has(rule.reviewLane)) {
    fail("review-contract-invalid", "Review contract rule lane is unsupported.");
  }
  return {
    id: rule.id,
    pathPrefixes: normalizedPrefixes(rule.pathPrefixes, `Rule ${rule.id} pathPrefixes`, MAX_PREFIXES_PER_RULE),
    severity: rule.severity,
    reviewLane: rule.reviewLane,
    requiredEvidence: sortedUniqueStrings(
      rule.requiredEvidence,
      evidenceValues,
      `Rule ${rule.id} requiredEvidence`,
      evidenceValues.size
    ),
    companionPathGroups: normalizedCompanionGroups(
      rule.companionPathGroups,
      `Rule ${rule.id} companionPathGroups`
    )
  };
}

export function createDefaultRunrailReviewContract() {
  return {
    schemaVersion: 1,
    artifactType: "runrail-review-contract",
    budgets: {
      maxChangedFiles: MAX_CONTRACT_CHANGED_FILES,
      maxDiffLines: MAX_CONTRACT_DIFF_LINES
    },
    rules: []
  };
}

export function normalizeRunrailReviewContract(value) {
  assertExactKeys(value, ["schemaVersion", "artifactType", "budgets", "rules"], "Review contract");
  if (value.schemaVersion !== 1 || value.artifactType !== "runrail-review-contract") {
    fail("review-contract-invalid", "The review contract identity is invalid.");
  }
  assertExactKeys(value.budgets, ["maxChangedFiles", "maxDiffLines"], "Review contract budgets");
  if (
    !Number.isSafeInteger(value.budgets.maxChangedFiles) ||
    value.budgets.maxChangedFiles < 1 ||
    value.budgets.maxChangedFiles > MAX_CONTRACT_CHANGED_FILES ||
    !Number.isSafeInteger(value.budgets.maxDiffLines) ||
    value.budgets.maxDiffLines < 1 ||
    value.budgets.maxDiffLines > MAX_CONTRACT_DIFF_LINES
  ) {
    fail("review-contract-invalid", "Review contract budgets must tighten the built-in review limits.");
  }
  if (!Array.isArray(value.rules) || value.rules.length > MAX_RULES) {
    fail("review-contract-invalid", `Review contract rules must contain at most ${MAX_RULES} entries.`);
  }
  const rules = value.rules.map(normalizeRule).sort((left, right) => compareText(left.id, right.id));
  if (new Set(rules.map((rule) => rule.id)).size !== rules.length) {
    fail("review-contract-invalid", "Review contract rule IDs must be unique.");
  }
  return {
    schemaVersion: 1,
    artifactType: "runrail-review-contract",
    budgets: {
      maxChangedFiles: value.budgets.maxChangedFiles,
      maxDiffLines: value.budgets.maxDiffLines
    },
    rules
  };
}

export function parseRunrailReviewContract(text) {
  if (typeof text !== "string" || text.length < 2 || new TextEncoder().encode(text).byteLength > 64 * 1024) {
    fail("review-contract-size-boundary", "The baseline review contract must be between 2 bytes and 64 KiB.");
  }
  let value;
  try {
    value = JSON.parse(text);
  } catch {
    fail("review-contract-invalid", "The baseline review contract must contain valid JSON.");
  }
  return normalizeRunrailReviewContract(value);
}

function normalizedParts(relativePath) {
  return String(relativePath || "")
    .replaceAll("\\", "/")
    .toLowerCase()
    .split("/")
    .filter(Boolean);
}

function basename(relativePath) {
  return normalizedParts(relativePath).at(-1) || "";
}

function extension(relativePath) {
  const name = basename(relativePath);
  const dot = name.lastIndexOf(".");
  return dot > 0 ? name.slice(dot) : "";
}

function hasPart(relativePath, parts) {
  return normalizedParts(relativePath).some((part) => parts.has(part));
}

function prefixMatches(relativePath, prefix) {
  return relativePath === prefix || relativePath.startsWith(`${prefix}/`);
}

function isDependencyPath(relativePath) {
  const name = basename(relativePath);
  return dependencyBasenames.has(name) || /^requirements(?:[-_.][a-z0-9-]+)?\.(?:in|txt)$/i.test(name);
}

function isTestPath(relativePath) {
  const parts = normalizedParts(relativePath);
  const name = parts.at(-1) || "";
  return parts.some((part) => testPathParts.has(part)) ||
    /(?:^test[_-]|[_-]test\.|\.test\.|\.spec\.|[_-]spec\.)/i.test(name);
}

function isSecurityPath(relativePath) {
  const parts = normalizedParts(relativePath);
  const name = parts.at(-1) || "";
  const stem = name.split(".")[0];
  return parts.some((part) => securityPathParts.has(part)) ||
    /^(?:auth|authentication|authorization|crypto|iam|permission|permissions|policy|security|secret|secrets)(?:[-_].*)?$/i.test(stem);
}

function isDeploymentPath(relativePath) {
  const name = basename(relativePath);
  return hasPart(relativePath, deploymentPathParts) ||
    /^(?:dockerfile|docker-compose(?:\.[a-z0-9-]+)?\.ya?ml|compose(?:\.[a-z0-9-]+)?\.ya?ml)$/i.test(name);
}

function isMigrationPath(relativePath) {
  const name = basename(relativePath);
  return hasPart(relativePath, migrationPathParts) || /^(?:schema|migration)[._-]/i.test(name);
}

function builtInObligations(paths) {
  const evidence = new Set(["project-tests"]);
  const lanes = new Set(["quality"]);
  if (paths.some(isSecurityPath)) {
    evidence.add("security-review");
    lanes.add("security");
  }
  if (paths.some(isDependencyPath)) {
    evidence.add("dependency-review");
    lanes.add("dependency");
  }
  if (paths.some(isDeploymentPath)) {
    evidence.add("delivery-review");
    evidence.add("rollback-review");
    lanes.add("delivery");
  }
  if (paths.some(isMigrationPath)) {
    evidence.add("data-migration-review");
    evidence.add("rollback-review");
    lanes.add("data");
  }
  if (paths.every((relativePath) => documentationExtensions.has(extension(relativePath)))) {
    evidence.delete("project-tests");
    evidence.add("documentation-review");
    lanes.delete("quality");
    lanes.add("documentation");
  } else if (!paths.some((relativePath) => sourceExtensions.has(extension(relativePath)))) {
    evidence.add("product-owner-review");
    lanes.add("product");
  }
  return { evidence, lanes };
}

export function evaluateRunrailReviewContract({
  contract,
  changedFiles,
  diffLineCount,
  source = "built-in",
  contractSha256,
  sourceBlobSha256 = null,
  candidateContractChanged = false
}) {
  const normalized = normalizeRunrailReviewContract(contract);
  if (!Array.isArray(changedFiles) || changedFiles.length < 1 || changedFiles.length > MAX_CONTRACT_CHANGED_FILES) {
    fail("review-contract-evaluation-invalid", "Changed files must stay within the review contract boundary.");
  }
  if (!Number.isSafeInteger(diffLineCount) || diffLineCount < 1 || diffLineCount > MAX_CONTRACT_DIFF_LINES) {
    fail("review-contract-evaluation-invalid", "Diff lines must stay within the review contract boundary.");
  }
  if (!new Set(["built-in", "baseline-file"]).has(source)) {
    fail("review-contract-evaluation-invalid", "The review contract source is invalid.");
  }
  if (typeof contractSha256 !== "string" || !/^[a-f0-9]{64}$/.test(contractSha256)) {
    fail("review-contract-evaluation-invalid", "The normalized review contract checksum is invalid.");
  }
  if (
    (source === "built-in" && sourceBlobSha256 !== null) ||
    (source === "baseline-file" && (typeof sourceBlobSha256 !== "string" || !/^[a-f0-9]{64}$/.test(sourceBlobSha256)))
  ) {
    fail("review-contract-evaluation-invalid", "The review contract source checksum is invalid.");
  }
  if (typeof candidateContractChanged !== "boolean") {
    fail("review-contract-evaluation-invalid", "The candidate contract-change flag is invalid.");
  }
  const paths = changedFiles.map((row) => String(row?.path || ""));
  const builtIn = builtInObligations(paths);
  const requiredEvidence = new Set(builtIn.evidence);
  const requiredReviewLanes = new Set(builtIn.lanes);
  const customMatchedPaths = new Set();
  const matches = [];
  let missingCompanionGroupCount = 0;

  for (const rule of normalized.rules) {
    const matchedPaths = paths.filter((relativePath) =>
      rule.pathPrefixes.some((prefix) => prefixMatches(relativePath, prefix))
    );
    if (matchedPaths.length < 1) continue;
    for (const relativePath of matchedPaths) customMatchedPaths.add(relativePath);
    const missingGroups = rule.companionPathGroups.filter((group) =>
      !paths.some((relativePath) => group.some((prefix) => prefixMatches(relativePath, prefix)))
    ).length;
    missingCompanionGroupCount += missingGroups;
    requiredReviewLanes.add(rule.reviewLane);
    for (const evidence of rule.requiredEvidence) requiredEvidence.add(evidence);
    matches.push({
      ruleId: rule.id,
      severity: rule.severity,
      reviewLane: rule.reviewLane,
      requiredEvidence: [...rule.requiredEvidence],
      matchedFileCount: matchedPaths.length,
      requiredCompanionGroupCount: rule.companionPathGroups.length,
      missingCompanionGroupCount: missingGroups
    });
  }

  if (candidateContractChanged) {
    requiredEvidence.add("policy-owner-review");
    requiredReviewLanes.add("policy");
  }
  const changedFileBudgetExceeded = changedFiles.length > normalized.budgets.maxChangedFiles;
  const diffLineBudgetExceeded = diffLineCount > normalized.budgets.maxDiffLines;
  const passed =
    !candidateContractChanged &&
    !changedFileBudgetExceeded &&
    !diffLineBudgetExceeded &&
    missingCompanionGroupCount === 0;

  return {
    profile: RUNRAIL_REVIEW_CONTRACT_PROFILE,
    contractPath: RUNRAIL_REVIEW_CONTRACT_PATH,
    source,
    baselineBound: true,
    candidateCouldWeakenBaseline: false,
    contractSha256,
    sourceBlobSha256,
    contract: normalized,
    candidateContractChanged,
    changedFileBudgetExceeded,
    diffLineBudgetExceeded,
    missingCompanionGroupCount,
    matchedRuleCount: matches.length,
    matchedFileCount: customMatchedPaths.size,
    unmatchedChangedFileCount: paths.length - customMatchedPaths.size,
    requiredReviewLanes: [...requiredReviewLanes].sort(compareText),
    requiredEvidence: [...requiredEvidence].sort(compareText),
    matches,
    passed
  };
}
