import { sha256Canonical } from "../../../schema/src/canonical.js";
import type { CapabilityProfile } from "../../../core/src/sdk/capability.js";
import {
  witnessCalibration,
  witnessNonVacuity,
  witnessPerturbation,
  witnessPositiveControl,
  witnessPrecondition,
  type CoverageGap,
  type CoverageReason,
  type DetectorRef,
  type WitnessKind,
  type WitnessResult,
} from "../../../universal/src/oracle/witnesses.js";
import type { OmittedCombination } from "../adapters/combinatorial/types.js";

export const REQUIRED_BLOCKING_WITNESS_KINDS = [
  "non-vacuity",
  "perturbation",
  "positive-control",
  "calibration",
  "precondition",
] as const satisfies readonly WitnessKind[];

export type LedgerWitnessPlanItem =
  | {
      kind: "non-vacuity";
      selection: { matched: number; expectedEmpty: boolean };
    }
  | {
      kind: "perturbation";
      probe: { applied: boolean; observedAtBoundary: boolean };
    }
  | {
      kind: "positive-control";
      check: { ownedItemObserved: boolean };
    }
  | {
      kind: "calibration";
      knownBad: { fired: boolean; profileFeature: string };
    }
  | {
      kind: "precondition";
      preconditions: { name: string; proven: boolean }[];
    };

export type LedgerWitnessPlan = LedgerWitnessPlanItem[];

export type CoverageLedgerRowInput = {
  journeyBranch: string;
  status: "inferred" | "confirmed";
  outcomeOracle: string;
  invariantCoverage: readonly string[];
  dimensions: Record<string, string>;
  lastEvidence: unknown;
  selectionCount: number;
  exercisedBranchCount: number;
  reachablePerturbations: number;
  unsupportedCapabilities: readonly string[];
  unprovenPreconditions: readonly string[];
};

export type CoverageLedgerRow = CoverageLedgerRowInput & {
  id: string;
};

export type CoverageLedgerQuery = {
  journeyBranch?: string;
  status?: "inferred" | "confirmed";
  outcomeOracle?: string;
};

export type CoverageLedger = {
  rows: CoverageLedgerRow[];
  blockingOracles: Map<string, BlockingOracleRegistration>;
};

export type BlockingOracleRegistration = {
  oracleId: string;
  detector: DetectorRef;
  witnesses: LedgerWitnessPlan;
};

export type ScopeCoverageMetrics = {
  selectionCount: number;
  exercisedBranchCount: number;
  reachablePerturbations: number;
  unsupportedCapabilities: readonly string[];
  unprovenPreconditions: readonly string[];
};

export type ScopeCoverageInput = {
  metrics: ScopeCoverageMetrics;
  expectedEmpty?: boolean;
};

export type ScopeCoverageResult = {
  certified: boolean;
  coverageIncomplete: boolean;
  reason?: CoverageReason;
  witnessRefs: Array<{ id: string }>;
  gaps: CoverageGap[];
};

export type BlockingOracleEligibility = {
  blockingEligible: boolean;
  witnessRefs: Array<{ id: string }>;
  gaps: CoverageGap[];
};

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

function assertNonNegativeInteger(value: number, field: string): void {
  if (!Number.isInteger(value) || value < 0) {
    throw new Error(`${field} must be a non-negative integer`);
  }
}

function assertStatus(value: string): asserts value is "inferred" | "confirmed" {
  if (value !== "inferred" && value !== "confirmed") {
    throw new Error("status must be inferred or confirmed");
  }
}

function witnessRefId(kind: WitnessKind, evidence: unknown): string {
  return sha256Canonical({
    kind: "witness-ref",
    witnessKind: kind,
    evidence,
  });
}

function witnessRefsFromResult(result: WitnessResult): Array<{ id: string }> {
  if (result.ok) {
    return [{ id: witnessRefId(result.witness.kind, result.witness.evidence) }];
  }
  return [{ id: witnessRefId(result.gap.witnessKind, result.gap) }];
}

function evaluateWitnessItem(
  item: LedgerWitnessPlanItem,
  detector: DetectorRef,
  capabilities: CapabilityProfile,
): WitnessResult {
  switch (item.kind) {
    case "non-vacuity":
      return witnessNonVacuity(item.selection);
    case "perturbation":
      return witnessPerturbation(item.probe);
    case "positive-control":
      return witnessPositiveControl(item.check);
    case "calibration":
      return witnessCalibration(detector, capabilities, item.knownBad);
    case "precondition":
      return witnessPrecondition(item.preconditions);
    default: {
      const exhaustive: never = item;
      throw new Error(`unsupported witness plan item: ${String(exhaustive)}`);
    }
  }
}

function incompleteResult(
  reason: CoverageReason,
  gap: CoverageGap,
  witnessRefs: Array<{ id: string }>,
): ScopeCoverageResult {
  return {
    certified: false,
    coverageIncomplete: true,
    reason,
    witnessRefs,
    gaps: [gap],
  };
}

function completeResult(witnessRefs: Array<{ id: string }>): ScopeCoverageResult {
  return {
    certified: true,
    coverageIncomplete: false,
    witnessRefs,
    gaps: [],
  };
}

function validateCoverageRowInput(input: CoverageLedgerRowInput): void {
  assertNonEmptyString(input.journeyBranch, "journeyBranch");
  assertStatus(input.status);
  assertNonEmptyString(input.outcomeOracle, "outcomeOracle");
  if (!Array.isArray(input.invariantCoverage)) {
    throw new Error("invariantCoverage must be an array");
  }
  for (const invariant of input.invariantCoverage) {
    if (typeof invariant !== "string") {
      throw new Error("invariantCoverage[] must contain strings");
    }
    assertNonEmptyString(invariant, "invariantCoverage[]");
  }
  if (typeof input.dimensions !== "object" || Array.isArray(input.dimensions)) {
    throw new Error("dimensions must be a record");
  }
  for (const [key, value] of Object.entries(input.dimensions)) {
    assertNonEmptyString(key, "dimensions key");
    if (typeof value !== "string") {
      throw new Error(`dimensions.${key} must be a string`);
    }
  }
  assertNonNegativeInteger(input.selectionCount, "selectionCount");
  assertNonNegativeInteger(input.exercisedBranchCount, "exercisedBranchCount");
  assertNonNegativeInteger(input.reachablePerturbations, "reachablePerturbations");
  if (!Array.isArray(input.unsupportedCapabilities)) {
    throw new Error("unsupportedCapabilities must be an array");
  }
  for (const capability of input.unsupportedCapabilities) {
    if (typeof capability !== "string") {
      throw new Error("unsupportedCapabilities[] must contain strings");
    }
    assertNonEmptyString(capability, "unsupportedCapabilities[]");
  }
  if (!Array.isArray(input.unprovenPreconditions)) {
    throw new Error("unprovenPreconditions must be an array");
  }
  for (const precondition of input.unprovenPreconditions) {
    if (typeof precondition !== "string") {
      throw new Error("unprovenPreconditions[] must contain strings");
    }
    assertNonEmptyString(precondition, "unprovenPreconditions[]");
  }
}

function rowId(input: CoverageLedgerRowInput): string {
  return sha256Canonical({
    kind: "feature-coverage-ledger-row",
    journeyBranch: input.journeyBranch,
    status: input.status,
    outcomeOracle: input.outcomeOracle,
    dimensions: input.dimensions,
    lastEvidence: input.lastEvidence,
  });
}

function assertCompleteWitnessPlan(plan: LedgerWitnessPlan): void {
  const kinds = new Set(plan.map((item) => item.kind));
  for (const required of REQUIRED_BLOCKING_WITNESS_KINDS) {
    if (!kinds.has(required)) {
      throw new Error(`blocking oracle witness plan missing required kind: ${required}`);
    }
  }
}

export function createCoverageLedger(): CoverageLedger {
  return {
    rows: [],
    blockingOracles: new Map(),
  };
}

export function appendCoverageRow(
  ledger: CoverageLedger,
  input: CoverageLedgerRowInput,
): CoverageLedgerRow {
  validateCoverageRowInput(input);
  const row: CoverageLedgerRow = {
    ...input,
    invariantCoverage: [...input.invariantCoverage],
    dimensions: { ...input.dimensions },
    unsupportedCapabilities: [...input.unsupportedCapabilities],
    unprovenPreconditions: [...input.unprovenPreconditions],
    id: rowId(input),
  };
  ledger.rows.push(row);
  return row;
}

export function queryCoverageRows(
  ledger: CoverageLedger,
  query: CoverageLedgerQuery = {},
): CoverageLedgerRow[] {
  return ledger.rows.filter((row) => {
    if (query.journeyBranch !== undefined && row.journeyBranch !== query.journeyBranch) {
      return false;
    }
    if (query.status !== undefined && row.status !== query.status) {
      return false;
    }
    if (query.outcomeOracle !== undefined && row.outcomeOracle !== query.outcomeOracle) {
      return false;
    }
    return true;
  });
}

export function registerBlockingOracle(
  ledger: CoverageLedger,
  registration: BlockingOracleRegistration,
): void {
  assertNonEmptyString(registration.oracleId, "oracleId");
  assertNonEmptyString(registration.detector.id, "detector.id");
  assertNonEmptyString(registration.detector.version, "detector.version");
  assertCompleteWitnessPlan(registration.witnesses);
  ledger.blockingOracles.set(registration.oracleId, {
    oracleId: registration.oracleId,
    detector: { ...registration.detector },
    witnesses: [...registration.witnesses],
  });
}

export function evaluateBlockingOracleEligibility(
  registration: BlockingOracleRegistration,
  capabilities: CapabilityProfile,
): BlockingOracleEligibility {
  assertCompleteWitnessPlan(registration.witnesses);
  const witnessRefs: Array<{ id: string }> = [];
  const gaps: CoverageGap[] = [];

  for (const item of registration.witnesses) {
    const result = evaluateWitnessItem(item, registration.detector, capabilities);
    witnessRefs.push(...witnessRefsFromResult(result));
    if (!result.ok) {
      gaps.push(result.gap);
      return {
        blockingEligible: false,
        witnessRefs,
        gaps,
      };
    }
  }

  return {
    blockingEligible: true,
    witnessRefs,
    gaps,
  };
}

export function assessWitnessPlanCoverage(
  witnesses: LedgerWitnessPlan,
  detector: DetectorRef,
  capabilities: CapabilityProfile,
): ScopeCoverageResult {
  const witnessRefs: Array<{ id: string }> = [];

  for (const item of witnesses) {
    const result = evaluateWitnessItem(item, detector, capabilities);
    witnessRefs.push(...witnessRefsFromResult(result));
    if (!result.ok) {
      return incompleteResult(result.gap.reason, result.gap, witnessRefs);
    }
  }

  return completeResult(witnessRefs);
}

export function assessScopeCoverage(input: ScopeCoverageInput): ScopeCoverageResult {
  const { metrics, expectedEmpty = false } = input;
  assertNonNegativeInteger(metrics.selectionCount, "selectionCount");
  assertNonNegativeInteger(metrics.exercisedBranchCount, "exercisedBranchCount");
  assertNonNegativeInteger(metrics.reachablePerturbations, "reachablePerturbations");

  if (metrics.selectionCount === 0 && !expectedEmpty) {
    const result = witnessNonVacuity({ matched: 0, expectedEmpty: false });
    if (!result.ok) {
      return incompleteResult(
        result.gap.reason,
        result.gap,
        witnessRefsFromResult(result),
      );
    }
  }

  if (metrics.exercisedBranchCount === 0 && !expectedEmpty) {
    const result = witnessNonVacuity({ matched: 0, expectedEmpty: false });
    if (!result.ok) {
      return incompleteResult(
        result.gap.reason,
        result.gap,
        witnessRefsFromResult(result),
      );
    }
  }

  if (metrics.unprovenPreconditions.length > 0) {
    const result = witnessPrecondition(
      metrics.unprovenPreconditions.map((name) => ({ name, proven: false })),
    );
    if (!result.ok) {
      return incompleteResult(
        result.gap.reason,
        result.gap,
        witnessRefsFromResult(result),
      );
    }
  }

  if (metrics.reachablePerturbations === 0) {
    const result = witnessPerturbation({
      applied: false,
      observedAtBoundary: false,
    });
    if (!result.ok) {
      return incompleteResult(
        result.gap.reason,
        result.gap,
        witnessRefsFromResult(result),
      );
    }
  }

  if (metrics.unsupportedCapabilities.length > 0) {
    const capability = metrics.unsupportedCapabilities[0];
    if (capability === undefined) {
      throw new Error("unsupportedCapabilities must not contain undefined entries");
    }
    const result = witnessCalibration(
      { id: "feature-coverage-ledger", version: "1.0.0" },
      { platform: "linux", features: { [capability]: "unavailable" } },
      { fired: false, profileFeature: capability },
    );
    if (!result.ok) {
      return incompleteResult(
        result.gap.reason,
        result.gap,
        witnessRefsFromResult(result),
      );
    }
  }

  return completeResult([]);
}

export function appendOmittedCombinationRows(
  ledger: CoverageLedger,
  input: {
    journeyBranch: string;
    status: "inferred" | "confirmed";
    outcomeOracle: string;
    omittedCombinations: readonly OmittedCombination[];
    invariantCoverage: readonly string[];
    lastEvidence: unknown;
    selectionCount: number;
  },
): CoverageLedgerRow[] {
  assertNonNegativeInteger(input.selectionCount, "selectionCount");
  const selectionCount = input.selectionCount;

  const rows: CoverageLedgerRow[] = [];
  for (const omitted of input.omittedCombinations) {
    rows.push(
      appendCoverageRow(ledger, {
        journeyBranch: input.journeyBranch,
        status: input.status,
        outcomeOracle: input.outcomeOracle,
        invariantCoverage: input.invariantCoverage,
        dimensions: { ...omitted.dimensions },
        lastEvidence: input.lastEvidence,
        selectionCount,
        exercisedBranchCount: 0,
        reachablePerturbations: 0,
        unsupportedCapabilities: [],
        unprovenPreconditions: [],
      }),
    );
  }
  return rows;
}
