import { canonicalize, sha256Canonical } from "../../../schema/src/canonical.js";
import type { ContractEnvelope } from "../../../schema/src/decisions/contract.js";
import type { ReviewDecision } from "../../../schema/src/decisions/decision.js";
import type { CreationSource } from "../../../schema/src/records/envelope.js";
import type { ExecutionContext } from "../../../schema/src/records/context.js";
import type { CoverageOutcome } from "../../../schema/src/records/coverage.js";
import type { Finding } from "../../../schema/src/records/finding.js";
import {
  COVERAGE_OUTCOME_VERSION,
} from "../../../schema/src/records/coverage.js";
import {
  HARNESS_OUTCOME_VERSION,
  type HarnessOutcome,
} from "../../../schema/src/records/harness.js";
import { FINDING_VERSION } from "../../../schema/src/records/finding.js";
import type { ContractStore } from "../contracts/store.js";
import {
  hasBlockingAuthority,
  type DecisionQuerySubject,
  type DecisionStore,
} from "../decisions/store.js";
import { evidenceFingerprint, extractDetectorMajor } from "../identity/fingerprint.js";
import { findingId } from "../identity/finding-id.js";
import { matchScope } from "../match/scope.js";
import {
  deriveCertainty,
  type ProvenancedEvidence,
} from "./certainty.js";

export type LaneEligibility = "blocking" | "blocking-eligible" | "advisory";

export type DetectorOutcome = {
  detector: { id: string; version: string };
  class: string;
  severity: Finding["severity"];
  target: { kind: string; canonical: string };
  context: ExecutionContext;
  summary: string;
  evidence: ProvenancedEvidence[];
  artifacts: Finding["artifacts"];
  laneEligibility: LaneEligibility;
  proofConditionMet?: boolean;
  scope: CoverageOutcome["scope"];
  violation: unknown;
  contractOrConfig?: unknown;
  contextDimensions?: Record<string, string>;
};

export type HarnessEvent = {
  phase: HarnessOutcome["phase"];
  scope: CoverageOutcome["scope"];
  plannedContext?: ExecutionContext;
  cause: HarnessOutcome["cause"];
  artifactRefs: Finding["artifacts"];
};

export type CoverageEvent = {
  scope: CoverageOutcome["scope"];
  context: ExecutionContext;
  reason: CoverageOutcome["reason"];
  witnessRefs: CoverageOutcome["witnessRefs"];
  capabilityProfileRef?: CoverageOutcome["capabilityProfileRef"];
  interaction?: CoverageOutcome["interaction"];
};

export type KernelStores = {
  decisions: DecisionStore;
  contracts: ContractStore;
  authoritativeContracts: ReadonlySet<string>;
  now: string;
};

export type ClassifyInput = {
  detectorOutcomes: DetectorOutcome[];
  harnessEvents: HarnessEvent[];
  coverageEvents: CoverageEvent[];
  stores: KernelStores;
  runId: string;
};

export type ClassifiedRun = {
  findings: Finding[];
  hiddenFindings: Finding[];
  coverageOutcomes: CoverageOutcome[];
  harnessOutcomes: HarnessOutcome[];
};

export type ClassifyEvidenceInput = Omit<
  DetectorOutcome,
  never
> & {
  harnessEvents: HarnessEvent[];
  coverageEvents: CoverageEvent[];
  stores: KernelStores;
  runId: string;
};

const LANE_ORDER: Record<Finding["lane"], number> = {
  blocking: 0,
  advisory: 1,
  accepted: 2,
  resolved: 3,
};

const SEVERITY_ORDER: Record<Finding["severity"], number> = {
  critical: 0,
  high: 1,
  medium: 2,
  low: 3,
  info: 4,
};

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 compareFindings(left: Finding, right: Finding): number {
  const laneDelta = LANE_ORDER[left.lane] - LANE_ORDER[right.lane];
  if (laneDelta !== 0) {
    return laneDelta;
  }

  const severityDelta = SEVERITY_ORDER[left.severity] - SEVERITY_ORDER[right.severity];
  if (severityDelta !== 0) {
    return severityDelta;
  }

  const classDelta = compareUnicodeScalars(left.class, right.class);
  if (classDelta !== 0) {
    return classDelta;
  }

  const targetDelta = compareUnicodeScalars(left.target.canonical, right.target.canonical);
  if (targetDelta !== 0) {
    return targetDelta;
  }

  const contextDelta = compareUnicodeScalars(
    canonicalize(left.context),
    canonicalize(right.context),
  );
  if (contextDelta !== 0) {
    return contextDelta;
  }

  return compareUnicodeScalars(left.id, right.id);
}

function sortFindings(findings: Finding[]): Finding[] {
  return [...findings].sort(compareFindings);
}

function creationSource(runId: string): CreationSource {
  return {
    runId,
    component: "classifier",
    componentId: "kernel-classifier",
  };
}

function assertNonEmptyString(value: string, field: string): void {
  if (value.trim().length === 0) {
    throw new Error(`${field} must be a non-empty string`);
  }
}

function validateDetectorOutcome(outcome: DetectorOutcome): void {
  assertNonEmptyString(outcome.detector.id, "detector.id");
  assertNonEmptyString(outcome.detector.version, "detector.version");
  assertNonEmptyString(outcome.class, "class");
  assertNonEmptyString(outcome.summary, "summary");
  assertNonEmptyString(outcome.scope.id, "scope.id");
  if (outcome.evidence.length === 0) {
    throw new Error("detector outcome requires at least one provenanced evidence item");
  }
}

function validateHarnessEvent(event: HarnessEvent): void {
  assertNonEmptyString(event.scope.id, "harness scope.id");
  assertNonEmptyString(event.cause.code, "harness cause.code");
  assertNonEmptyString(event.cause.message, "harness cause.message");
}

function validateCoverageEvent(event: CoverageEvent): void {
  assertNonEmptyString(event.scope.id, "coverage scope.id");
}

function buildEvidencePayload(evidence: ProvenancedEvidence[]): unknown {
  return evidence.map((item) => ({
    truthSource: item.truthSource,
    payload: item.payload,
    ...(item.visionOnly === true ? { visionOnly: true } : {}),
  }));
}

function computeFingerprint(outcome: DetectorOutcome): string {
  return evidenceFingerprint({
    detectorMajor: extractDetectorMajor(outcome.detector.version),
    normalizedTarget: outcome.target,
    violation: outcome.violation,
    contractOrConfig: outcome.contractOrConfig ?? null,
    contextDimensions: outcome.contextDimensions ?? {},
  });
}

function buildDecisionSubject(
  outcome: DetectorOutcome,
  finding: Pick<Finding, "id" | "evidenceFingerprint">,
  stores: KernelStores,
): DecisionQuerySubject {
  return {
    findingId: finding.id,
    evidenceFingerprint: finding.evidenceFingerprint,
    detector: outcome.detector,
    target: outcome.target.canonical,
    contextKind: outcome.context.kind,
    dimensions: outcome.contextDimensions ?? {},
    contractVersion: null,
    now: stores.now,
  };
}

function findAuthoritativeContract(
  outcome: DetectorOutcome,
  stores: KernelStores,
): ContractEnvelope | undefined {
  for (const contract of stores.contracts.confirmedContracts()) {
    if (!stores.contracts.isActive(contract.id)) {
      continue;
    }
    if (!stores.authoritativeContracts.has(`${contract.id}@${contract.version}`)) {
      continue;
    }

    const subject = {
      detector: outcome.detector,
      target: outcome.target.canonical,
      contextKind: outcome.context.kind,
      dimensions: outcome.contextDimensions ?? {},
      contractVersion: contract.applicableScope.contractVersion,
      now: stores.now,
    };
    const match = matchScope(contract.applicableScope, subject);
    if (match.matched) {
      return contract;
    }
  }

  return undefined;
}

function hasUniversalBasis(evidence: ProvenancedEvidence[]): boolean {
  return evidence.some((item) => item.truthSource === "universal");
}

function proofConditionMet(outcome: DetectorOutcome): boolean {
  if (outcome.laneEligibility === "blocking") {
    return true;
  }
  if (outcome.laneEligibility === "blocking-eligible") {
    return outcome.proofConditionMet === true;
  }
  return false;
}

function resolveLane(
  outcome: DetectorOutcome,
  decision: ReviewDecision | undefined,
  authoritativeContract: ContractEnvelope | undefined,
): Finding["lane"] {
  if (decision?.verdict === "not_defect") {
    return "accepted";
  }

  if (hasUniversalBasis(outcome.evidence) && proofConditionMet(outcome)) {
    return "blocking";
  }

  if (
    authoritativeContract !== undefined &&
    decision !== undefined &&
    hasBlockingAuthority(decision) &&
    decision.promotion?.contractId === authoritativeContract.id &&
    decision.promotion.contractVersion === authoritativeContract.version
  ) {
    return "blocking";
  }

  return "advisory";
}

const HARNESS_CELL_DIMENSIONS = [
  "routeId",
  "url",
  "role",
  "locale",
  "viewport",
] as const;

function assertAttributableHarnessContext(event: HarnessEvent): void {
  const context = event.plannedContext;
  if (context === undefined || context.kind !== "browser") {
    return;
  }
  const missing = HARNESS_CELL_DIMENSIONS.filter(
    (dimension) => context.cell[dimension] === undefined,
  );
  if (missing.length > 0) {
    throw new Error(
      `HAR-004: harness browser context for scope ${event.scope.id} is missing matrix cell dimensions (${HARNESS_CELL_DIMENSIONS.join(", ")}); absent: ${missing.join(", ")}`,
    );
  }
}

function buildHarnessOutcome(
  event: HarnessEvent,
  runId: string,
): HarnessOutcome {
  assertAttributableHarnessContext(event);

  const id = sha256Canonical({
    kind: "harness-outcome",
    phase: event.phase,
    scope: event.scope,
    cause: event.cause,
    runId,
  });

  return {
    schemaVersion: HARNESS_OUTCOME_VERSION,
    id,
    creationSource: creationSource(runId),
    status: "harness-failure",
    phase: event.phase,
    scope: event.scope,
    plannedContext: event.plannedContext,
    cause: event.cause,
    artifactRefs: event.artifactRefs,
    runId,
  };
}

function buildCoverageOutcome(
  event: CoverageEvent,
  runId: string,
): CoverageOutcome {
  const id = sha256Canonical({
    kind: "coverage-outcome",
    scope: event.scope,
    context: event.context,
    reason: event.reason,
    capabilityProfileRef: event.capabilityProfileRef ?? null,
    runId,
  });

  return {
    schemaVersion: COVERAGE_OUTCOME_VERSION,
    id,
    creationSource: creationSource(runId),
    status: "coverage-incomplete",
    scope: event.scope,
    context: event.context,
    reason: event.reason,
    witnessRefs: event.witnessRefs,
    ...(event.capabilityProfileRef === undefined ? {} : { capabilityProfileRef: event.capabilityProfileRef }),
    ...(event.interaction === undefined ? {} : { interaction: event.interaction }),
    runId,
  };
}

function isScopeSuppressed(
  scopeId: string,
  suppressedScopeIds: ReadonlySet<string>,
): boolean {
  return suppressedScopeIds.has(scopeId);
}

function buildFinding(
  outcome: DetectorOutcome,
  stores: KernelStores,
  runId: string,
): Finding {
  const fingerprint = computeFingerprint(outcome);
  const id = findingId({
    detector: outcome.detector,
    target: outcome.target,
    context: outcome.context,
  });
  const authoritativeContract = findAuthoritativeContract(outcome, stores);
  const decision = stores.decisions.activeDecisionFor(
    buildDecisionSubject(outcome, { id, evidenceFingerprint: fingerprint }, stores),
  );
  const lane = resolveLane(outcome, decision, authoritativeContract);
  const resolvedCertainty = deriveCertainty(outcome.evidence, {
    hasActiveContract:
      authoritativeContract !== undefined &&
      lane === "blocking" &&
      !hasUniversalBasis(outcome.evidence),
  });

  const contractIds =
    authoritativeContract === undefined ? [] : [authoritativeContract.id];

  return {
    schemaVersion: FINDING_VERSION,
    id,
    creationSource: creationSource(runId),
    evidenceFingerprint: fingerprint,
    detector: outcome.detector,
    class: outcome.class,
    lane,
    certainty: resolvedCertainty,
    severity: outcome.severity,
    target: outcome.target,
    context: outcome.context,
    summary: outcome.summary,
    evidence: buildEvidencePayload(outcome.evidence),
    artifacts: outcome.artifacts,
    contractIds,
    firstSeenRunId: runId,
    lastSeenRunId: runId,
  };
}

export function queryClassifiedFindings(
  run: ClassifiedRun,
  options: { audit?: boolean } = {},
): Finding[] {
  if (options.audit === true) {
    return sortFindings([...run.findings, ...run.hiddenFindings]);
  }
  return run.findings;
}

export function classify(input: ClassifyInput): ClassifiedRun {
  assertNonEmptyString(input.runId, "runId");

  for (const outcome of input.detectorOutcomes) {
    validateDetectorOutcome(outcome);
  }
  for (const event of input.harnessEvents) {
    validateHarnessEvent(event);
  }
  for (const event of input.coverageEvents) {
    validateCoverageEvent(event);
  }

  const suppressedScopeIds = new Set(
    input.harnessEvents.map((event) => event.scope.id),
  );

  const harnessOutcomes = input.harnessEvents.map((event) =>
    buildHarnessOutcome(event, input.runId),
  );
  const coverageOutcomes = input.coverageEvents.map((event) =>
    buildCoverageOutcome(event, input.runId),
  );

  const visibleFindings: Finding[] = [];
  const hiddenFindings: Finding[] = [];
  // DATA-021 identity is detector, target and context, so two outcomes carrying
  // the same identity describe one finding observed more than once — a resource
  // that fails twice on a page, an element reported by two checks of one rule.
  const byIdentity = new Map<string, Finding>();

  for (const outcome of input.detectorOutcomes) {
    if (isScopeSuppressed(outcome.scope.id, suppressedScopeIds)) {
      continue;
    }

    const finding = buildFinding(outcome, input.stores, input.runId);
    const merged = byIdentity.get(finding.id);
    if (merged !== undefined) {
      const mergedEvidence: unknown = merged.evidence;
      const extraEvidence: unknown = finding.evidence;
      if (Array.isArray(mergedEvidence) && Array.isArray(extraEvidence)) {
        merged.evidence = [...(mergedEvidence as unknown[]), ...(extraEvidence as unknown[])];
      }
      continue;
    }
    byIdentity.set(finding.id, finding);
    if (finding.lane === "accepted") {
      hiddenFindings.push(finding);
      continue;
    }
    visibleFindings.push(finding);
  }

  return {
    findings: sortFindings(visibleFindings),
    hiddenFindings: sortFindings(hiddenFindings),
    coverageOutcomes,
    harnessOutcomes,
  };
}

export function classifyFromEvidence(
  inputs: ClassifyEvidenceInput[],
): ClassifiedRun {
  if (inputs.length === 0) {
    throw new Error("classifyFromEvidence requires at least one evidence input");
  }

  const first = inputs[0];
  if (first === undefined) {
    throw new Error("classifyFromEvidence requires at least one evidence input");
  }

  const detectorOutcomes: DetectorOutcome[] = inputs.map((input) => {
    const outcome: DetectorOutcome = {
      detector: input.detector,
      class: input.class,
      severity: input.severity,
      target: input.target,
      context: input.context,
      summary: input.summary,
      evidence: input.evidence,
      artifacts: input.artifacts,
      laneEligibility: input.laneEligibility,
      scope: input.scope,
      violation: input.violation,
    };
    if (input.proofConditionMet !== undefined) {
      outcome.proofConditionMet = input.proofConditionMet;
    }
    if (input.contractOrConfig !== undefined) {
      outcome.contractOrConfig = input.contractOrConfig;
    }
    if (input.contextDimensions !== undefined) {
      outcome.contextDimensions = input.contextDimensions;
    }
    return outcome;
  });

  return classify({
    detectorOutcomes,
    harnessEvents: first.harnessEvents,
    coverageEvents: first.coverageEvents,
    stores: first.stores,
    runId: first.runId,
  });
}
