import type { ClassifiedRun } from "../classify/classifier.js";
import type { Finding } from "../../../schema/src/records/finding.js";
import type { CoverageOutcome } from "../../../schema/src/records/coverage.js";
import {
  coverageDecisionSubject,
  type DecisionStore,
} from "../decisions/store.js";
import {
  hasAuthoritativeContract,
  parseBaseline,
  type Baseline,
  type BaselineDefectEntry,
} from "./baseline.js";

export type RatchetClassification =
  | "addition"
  | "recurrence"
  | "material-change"
  | "resolution"
  | "coverage-loss"
  | "detector-migration"
  | "expired-applicability"
  | "unscanned-prior-defect";

export type RatchetEffectiveLane = "blocking" | "advisory" | "coverage-incomplete";

export type RatchetStatus = "pass" | "fail" | "coverage-incomplete";

export type RatchetEntry = {
  classification: RatchetClassification;
  findingId: string;
  evidenceFingerprint: string;
  scopeId: string;
  effectiveLane: RatchetEffectiveLane;
  priorEvidenceFingerprint?: string;
};

export type RatchetResult = {
  status: RatchetStatus;
  entries: RatchetEntry[];
  blockingCount: number;
  advisoryCount: number;
  unresolvedPriorDefectCount: number;
};

export type RatchetOptions = {
  decisions?: DecisionStore;
  now?: string;
};

function coverageKey(
  scopeId: string,
  detectorId: string,
  capabilityProfileId: string,
  reason: CoverageOutcome["reason"],
): string {
  return `${scopeId}\0${detectorId}\0${capabilityProfileId}\0${reason}`;
}

function coverageSurfaceKey(scopeId: string, detectorId: string): string {
  return `${scopeId}\0${detectorId}`;
}

function acceptedCoverageGap(
  baseline: Baseline,
  outcome: CoverageOutcome,
  options: RatchetOptions,
): boolean {
  const profileId = outcome.capabilityProfileRef?.id;
  if (profileId === undefined || options.decisions === undefined) {
    return false;
  }

  const subject = coverageDecisionSubject(outcome, options.now ?? "");
  if (subject === undefined) {
    return false;
  }

  const owned = baseline.ownedCoverageEntries.some(
    (entry) =>
      entry.scopeId === outcome.scope.id &&
      entry.detectorId === (outcome.scope.detectorId ?? "") &&
      entry.capabilityProfileRef?.id === profileId &&
      entry.reason === outcome.reason &&
      entry.status === "missing",
  );
  if (!owned) {
    return false;
  }

  const decision = options.decisions.activeDecisionFor(subject);
  return decision?.verdict === "not_defect";
}

function compareUnicodeScalars(left: string, right: string): number {
  let leftIndex = 0;
  let rightIndex = 0;

  while (leftIndex < left.length && rightIndex < right.length) {
    const leftCode = left.codePointAt(leftIndex);
    const rightCode = right.codePointAt(rightIndex);
    if (leftCode === undefined || rightCode === undefined) {
      break;
    }
    if (leftCode !== rightCode) {
      return leftCode < rightCode ? -1 : 1;
    }
    leftIndex += leftCode > 0xffff ? 2 : 1;
    rightIndex += rightCode > 0xffff ? 2 : 1;
  }

  return left.length - right.length;
}

function sortEntries(entries: RatchetEntry[]): RatchetEntry[] {
  return [...entries].sort((left, right) => {
    const classificationDelta = compareUnicodeScalars(
      left.classification,
      right.classification,
    );
    if (classificationDelta !== 0) {
      return classificationDelta;
    }
    const scopeDelta = compareUnicodeScalars(left.scopeId, right.scopeId);
    if (scopeDelta !== 0) {
      return scopeDelta;
    }
    return compareUnicodeScalars(left.findingId, right.findingId);
  });
}

function buildScannedScopeIds(baseline: Baseline, run: ClassifiedRun): Set<string> {
  const scanned = new Set<string>();
  const scopeByFindingId = new Map(
    baseline.defectEntries.map((entry) => [entry.findingId, entry.scopeId]),
  );
  for (const accepted of baseline.acceptedFingerprints) {
    scopeByFindingId.set(accepted.findingId, accepted.scopeId);
  }

  for (const outcome of run.coverageOutcomes) {
    scanned.add(outcome.scope.id);
  }
  for (const finding of [...run.findings, ...run.hiddenFindings]) {
    const baselineScopeId = scopeByFindingId.get(finding.id);
    if (baselineScopeId !== undefined) {
      scanned.add(baselineScopeId);
    }
    const coverageScopeId = scopeIdForFinding(finding, run);
    if (coverageScopeId !== undefined) {
      scanned.add(coverageScopeId);
    }
  }
  return scanned;
}

function scopeIdForFinding(finding: Finding, run: ClassifiedRun): string | undefined {
  for (const outcome of run.coverageOutcomes) {
    if (outcome.scope.detectorId === finding.detector.id) {
      return outcome.scope.id;
    }
  }
  return undefined;
}

function indexFindingsById(run: ClassifiedRun): Map<string, Finding> {
  const findings = new Map<string, Finding>();
  for (const finding of [...run.findings, ...run.hiddenFindings]) {
    findings.set(finding.id, finding);
  }
  return findings;
}

function isAcceptedFingerprint(
  baseline: Baseline,
  finding: Finding,
  scopeId: string,
): boolean {
  return baseline.acceptedFingerprints.some(
    (accepted) =>
      accepted.findingId === finding.id &&
      accepted.evidenceFingerprint === finding.evidenceFingerprint &&
      accepted.scopeId === scopeId,
  );
}

function resolveEffectiveLane(
  baseline: Baseline,
  defectEntry: BaselineDefectEntry | undefined,
  finding: Finding | undefined,
): RatchetEffectiveLane {
  if (finding === undefined) {
    return "advisory";
  }

  if (finding.lane === "blocking" && finding.certainty === "proven") {
    return "blocking";
  }

  if (defectEntry?.confirmed === true) {
    if (
      defectEntry.promotion !== undefined &&
      hasAuthoritativeContract(baseline, defectEntry.promotion)
    ) {
      return "blocking";
    }

    if (finding.lane === "blocking" && finding.certainty === "confirmed-contract") {
      return "coverage-incomplete";
    }

    return "advisory";
  }

  if (finding.lane === "blocking") {
    if (finding.certainty === "confirmed-contract") {
      const contractId = finding.contractIds[0];
      if (contractId === undefined) {
        return "coverage-incomplete";
      }
      const authoritative = baseline.activeContracts.some(
        (contract) => contract.id === contractId && contract.authoritative,
      );
      return authoritative ? "blocking" : "coverage-incomplete";
    }
    return "coverage-incomplete";
  }

  return "advisory";
}

function classifyBaselineDefect(
  baseline: Baseline,
  defectEntry: BaselineDefectEntry,
  findingById: Map<string, Finding>,
  scannedScopeIds: Set<string>,
): RatchetEntry {
  const finding = findingById.get(defectEntry.findingId);

  if (finding !== undefined) {
    if (finding.evidenceFingerprint === defectEntry.evidenceFingerprint) {
      return {
        classification: "recurrence",
        findingId: defectEntry.findingId,
        evidenceFingerprint: finding.evidenceFingerprint,
        scopeId: defectEntry.scopeId,
        effectiveLane: resolveEffectiveLane(baseline, defectEntry, finding),
      };
    }

    return {
      classification: "material-change",
      findingId: defectEntry.findingId,
      evidenceFingerprint: finding.evidenceFingerprint,
      scopeId: defectEntry.scopeId,
      effectiveLane: resolveEffectiveLane(baseline, defectEntry, finding),
      priorEvidenceFingerprint: defectEntry.evidenceFingerprint,
    };
  }

  if (!scannedScopeIds.has(defectEntry.scopeId)) {
    return {
      classification: "unscanned-prior-defect",
      findingId: defectEntry.findingId,
      evidenceFingerprint: defectEntry.evidenceFingerprint,
      scopeId: defectEntry.scopeId,
      effectiveLane: "coverage-incomplete",
    };
  }

  return {
    classification: "resolution",
    findingId: defectEntry.findingId,
    evidenceFingerprint: defectEntry.evidenceFingerprint,
    scopeId: defectEntry.scopeId,
    effectiveLane: "advisory",
  };
}

function classifyCoverageLosses(
  baseline: Baseline,
  run: ClassifiedRun,
  scannedScopeIds: Set<string>,
  options: RatchetOptions,
): RatchetEntry[] {
  const entries: RatchetEntry[] = [];
  const coverageByKey = new Map(
    baseline.ownedCoverageEntries.flatMap((entry) =>
      entry.capabilityProfileRef === undefined
        ? []
        : entry.reason === undefined
          ? []
          : [[
              coverageKey(
                entry.scopeId,
                entry.detectorId,
                entry.capabilityProfileRef.id,
                entry.reason,
              ),
              entry,
            ] as const],
    ),
  );
  const coverageBySurface = new Map<string, CoverageOutcome[]>();
  for (const outcome of run.coverageOutcomes) {
    const surfaceKey = coverageSurfaceKey(
      outcome.scope.id,
      outcome.scope.detectorId ?? "",
    );
    const existing = coverageBySurface.get(surfaceKey) ?? [];
    existing.push(outcome);
    coverageBySurface.set(surfaceKey, existing);

    if (acceptedCoverageGap(baseline, outcome, options)) {
      continue;
    }

    const profileId = outcome.capabilityProfileRef?.id;
    const owned =
      profileId === undefined
        ? undefined
        : coverageByKey.get(
            coverageKey(
              outcome.scope.id,
              outcome.scope.detectorId ?? "",
              profileId,
              outcome.reason,
            ),
          );
    if (owned?.status === "retired") {
      continue;
    }

    entries.push({
      classification: "coverage-loss",
      findingId: `coverage:${outcome.scope.detectorId ?? ""}:${outcome.reason}:${outcome.scope.id}`,
      evidenceFingerprint: outcome.id,
      scopeId: outcome.scope.id,
      effectiveLane: "coverage-incomplete",
    });
  }

  for (const owned of baseline.ownedCoverageEntries) {
    if (owned.status !== "covered") {
      continue;
    }

    const currentOutcomes = coverageBySurface.get(
      coverageSurfaceKey(owned.scopeId, owned.detectorId),
    );
    if (currentOutcomes !== undefined && currentOutcomes.length > 0) {
      continue;
    }

    if (!scannedScopeIds.has(owned.scopeId)) {
      entries.push({
        classification: "coverage-loss",
        findingId: `coverage:${owned.scopeId}`,
        evidenceFingerprint: `missing-scan:${owned.scopeId}`,
        scopeId: owned.scopeId,
        effectiveLane: "coverage-incomplete",
      });
    }
  }

  return entries;
}

function classifyDetectorMigrations(
  baseline: Baseline,
  run: ClassifiedRun,
): RatchetEntry[] {
  const entries: RatchetEntry[] = [];
  const baselineVersions = new Map(
    baseline.detectorVersions.map((detector) => [detector.id, detector.version]),
  );

  for (const finding of run.findings) {
    const baselineVersion = baselineVersions.get(finding.detector.id);
    if (
      baselineVersion !== undefined &&
      baselineVersion !== finding.detector.version
    ) {
      entries.push({
        classification: "detector-migration",
        findingId: finding.id,
        evidenceFingerprint: finding.evidenceFingerprint,
        scopeId: scopeIdForFinding(finding, run) ?? `detector:${finding.detector.id}`,
        effectiveLane: resolveEffectiveLane(baseline, undefined, finding),
      });
    }
  }

  return entries;
}

function classifyExpiredApplicability(baseline: Baseline): RatchetEntry[] {
  const entries: RatchetEntry[] = [];

  for (const defect of baseline.defectEntries) {
    if (defect.promotion === undefined) {
      continue;
    }

    const contract = baseline.activeContracts.find(
      (candidate) =>
        candidate.id === defect.promotion?.contractId &&
        candidate.version === defect.promotion.contractVersion,
    );

    if (contract !== undefined && !contract.authoritative) {
      entries.push({
        classification: "expired-applicability",
        findingId: defect.findingId,
        evidenceFingerprint: defect.evidenceFingerprint,
        scopeId: defect.scopeId,
        effectiveLane: "coverage-incomplete",
      });
    }
  }

  return entries;
}

function classifyAdditions(
  baseline: Baseline,
  run: ClassifiedRun,
  baselineDefectIds: Set<string>,
): RatchetEntry[] {
  const entries: RatchetEntry[] = [];

  for (const finding of run.findings) {
    if (baselineDefectIds.has(finding.id)) {
      continue;
    }

    const scopeId =
      scopeIdForFinding(finding, run) ?? `finding:${finding.id}`;

    if (isAcceptedFingerprint(baseline, finding, scopeId)) {
      continue;
    }

    entries.push({
      classification: "addition",
      findingId: finding.id,
      evidenceFingerprint: finding.evidenceFingerprint,
      scopeId,
      effectiveLane: resolveEffectiveLane(baseline, undefined, finding),
    });
  }

  return entries;
}

function computeStatus(entries: RatchetEntry[]): RatchetStatus {
  if (entries.some((entry) => entry.effectiveLane === "blocking")) {
    return "fail";
  }

  if (
    entries.some(
      (entry) =>
        entry.effectiveLane === "coverage-incomplete" ||
        entry.classification === "unscanned-prior-defect" ||
        entry.classification === "coverage-loss",
    )
  ) {
    return "coverage-incomplete";
  }

  return "pass";
}

function countByLane(entries: RatchetEntry[]): {
  blockingCount: number;
  advisoryCount: number;
  unresolvedPriorDefectCount: number;
} {
  let blockingCount = 0;
  let advisoryCount = 0;
  let unresolvedPriorDefectCount = 0;

  for (const entry of entries) {
    if (entry.effectiveLane === "blocking") {
      blockingCount += 1;
    }
    if (entry.effectiveLane === "advisory") {
      advisoryCount += 1;
    }
    if (entry.classification === "unscanned-prior-defect") {
      unresolvedPriorDefectCount += 1;
    }
  }

  return { blockingCount, advisoryCount, unresolvedPriorDefectCount };
}

export function ratchet(
  baselineInput: Baseline,
  run: ClassifiedRun,
  options: RatchetOptions = {},
): RatchetResult {
  const baseline = parseBaseline(baselineInput);
  const findingById = indexFindingsById(run);
  const scannedScopeIds = buildScannedScopeIds(baseline, run);
  const baselineDefectIds = new Set(
    baseline.defectEntries.map((entry) => entry.findingId),
  );

  const entries: RatchetEntry[] = [];

  for (const defectEntry of baseline.defectEntries) {
    entries.push(
      classifyBaselineDefect(baseline, defectEntry, findingById, scannedScopeIds),
    );
  }

  entries.push(...classifyAdditions(baseline, run, baselineDefectIds));
  entries.push(...classifyCoverageLosses(baseline, run, scannedScopeIds, options));
  entries.push(...classifyDetectorMigrations(baseline, run));
  entries.push(...classifyExpiredApplicability(baseline));

  const sortedEntries = sortEntries(entries);
  const status = computeStatus(sortedEntries);
  const counts = countByLane(sortedEntries);

  return {
    status,
    entries: sortedEntries,
    ...counts,
  };
}
