import type { Browser, BrowserContext } from "playwright";
import { sha256Canonical } from "../../schema/src/canonical.js";
import { matrixCellRef } from "../../schema/src/records/context.js";
import type { MatrixCell } from "../../schema/src/records/context.js";
import type { CoverageOutcome } from "../../schema/src/records/coverage.js";
import type { ExecutionContext } from "../../schema/src/records/context.js";
import {
  classify,
  type ClassifiedRun,
  type CoverageEvent,
  type DetectorOutcome,
  type HarnessEvent,
  type KernelStores,
} from "../../core/src/classify/classifier.js";
import {
  parseCapabilityProfile,
  type CapabilityProfile,
} from "../../core/src/sdk/capability.js";
import {
  asReadonlyKernelStores,
  type Detector,
  type DetectorContext,
  type DetectorOutcome as SdkDetectorOutcome,
} from "../../core/src/sdk/detector.js";
import type { CoverageGap } from "../../universal/src/oracle/witnesses.js";
import type { MatrixPlanResult } from "./matrix.js";
import {
  runCell,
  type CaptureOptions,
  type CellEvidence,
  type CellResult,
  type HarnessEventInput,
} from "./cell-runner.js";

export type AuthProof = {
  role: string;
  evidence: unknown;
};

export interface AuthAdapter {
  readonly id: string;
  setup(context: BrowserContext, cell: MatrixCell): Promise<AuthProof>;
}

export type AuthAdapterModuleRef = {
  module: string;
  export: string;
};

const authAdapterExportSchema = {
  validate(exported: unknown, ref: AuthAdapterModuleRef): AuthAdapter {
    if (exported === null || typeof exported !== "object") {
      throw new AuthAdapterLoadError(ref, "export must be an object");
    }

    const candidate = exported as Record<string, unknown>;
    const id = candidate.id;
    if (typeof id !== "string" || id.trim().length === 0) {
      throw new AuthAdapterLoadError(ref, "id must be a non-empty string");
    }

    if (typeof candidate.setup !== "function") {
      throw new AuthAdapterLoadError(ref, "setup must be a function");
    }

    return exported as AuthAdapter;
  },
};

function validateAuthAdapterExport(
  exported: unknown,
  ref: AuthAdapterModuleRef,
): AuthAdapter {
  return authAdapterExportSchema.validate(exported, ref);
}

export class AuthAdapterLoadError extends Error {
  readonly module: string;
  readonly exportName: string;
  readonly reason: string;
  readonly cause?: unknown;

  constructor(ref: AuthAdapterModuleRef, reason: string, cause?: unknown) {
    super(`Failed to load auth adapter ${ref.export} from ${ref.module}: ${reason}`);
    this.name = "AuthAdapterLoadError";
    this.module = ref.module;
    this.exportName = ref.export;
    this.reason = reason;
    if (cause !== undefined) {
      this.cause = cause;
    }
  }
}

export class AuthProofVerificationError extends Error {
  readonly proof: AuthProof;
  readonly cell: MatrixCell;

  constructor(message: string, proof: AuthProof, cell: MatrixCell) {
    super(message);
    this.name = "AuthProofVerificationError";
    this.proof = proof;
    this.cell = cell;
  }
}

export async function loadAuthAdapters(
  refs: readonly AuthAdapterModuleRef[],
): Promise<AuthAdapter[]> {
  const adapters: AuthAdapter[] = [];

  for (const ref of refs) {
    let importedModule: Record<string, unknown>;
    try {
      importedModule = (await import(ref.module)) as Record<string, unknown>;
    } catch (error) {
      throw new AuthAdapterLoadError(ref, "module import failed", error);
    }

    const exported = importedModule[ref.export];
    if (exported === undefined) {
      throw new AuthAdapterLoadError(ref, `export "${ref.export}" is missing from module`);
    }

    adapters.push(validateAuthAdapterExport(exported, ref));
  }

  return adapters;
}

export function verifyAuthProof(proof: AuthProof, cell: MatrixCell): void {
  if (proof.role.trim().length === 0) {
    throw new AuthProofVerificationError(
      "auth proof role must be a non-empty string",
      proof,
      cell,
    );
  }

  if (proof.role !== cell.role) {
    throw new AuthProofVerificationError(
      `auth proof role "${proof.role}" does not match cell role "${cell.role}"`,
      proof,
      cell,
    );
  }
}

export type RunMatrixInput = {
  plan: MatrixPlanResult;
  baseUrl: string;
  adapters: Record<string, AuthAdapter>;
  detectors: Detector<CellEvidence, unknown>[];
  stores: KernelStores;
  runId: string;
  seed: string;
  clockStart: string;
  browser: Browser;
  capture: CaptureOptions;
  timeouts: { navigateMs: number; settleMs: number };
  capabilities?: CapabilityProfile;
  onCellEvidence?: (evidence: CellEvidence) => void;
};

type CellScope = CoverageOutcome["scope"];

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 assertNonEmptyString(value: string, field: string): void {
  if (value.trim().length === 0) {
    throw new Error(`${field} must be a non-empty string`);
  }
}

function assertPositiveFiniteNumber(value: number, field: string): void {
  if (!Number.isFinite(value) || value <= 0) {
    throw new Error(`${field} must be a positive finite number`);
  }
}

function validateRunMatrixInput(input: RunMatrixInput): CapabilityProfile {
  assertNonEmptyString(input.baseUrl, "baseUrl");
  assertNonEmptyString(input.runId, "runId");
  assertNonEmptyString(input.seed, "seed");
  assertNonEmptyString(input.clockStart, "clockStart");
  assertPositiveFiniteNumber(input.timeouts.navigateMs, "timeouts.navigateMs");
  assertPositiveFiniteNumber(input.timeouts.settleMs, "timeouts.settleMs");

  const clockStartMs = Date.parse(input.clockStart);
  if (Number.isNaN(clockStartMs)) {
    throw new Error("clockStart must be an ISO-8601 timestamp");
  }

  if (input.plan.cells.length === 0 && input.plan.coverageGaps.length === 0) {
    throw new Error("plan must contain at least one cell or coverage gap");
  }

  for (const detector of input.detectors) {
    assertNonEmptyString(detector.id, "detector.id");
    assertNonEmptyString(detector.version, "detector.version");
  }

  for (const [role, adapter] of Object.entries(input.adapters)) {
    assertNonEmptyString(role, "adapters key");
    assertNonEmptyString(adapter.id, `adapters.${role}.id`);
  }

  return parseCapabilityProfile(
    input.capabilities ?? {
      platform: "linux",
      features: {},
    },
  );
}

function authScopeId(role: string): string {
  return sha256Canonical({
    kind: "browser-auth-scope",
    role,
  });
}

function browserExecutionContext(cell: MatrixCell): ExecutionContext {
  return {
    kind: "browser",
    cell: matrixCellRef(cell),
  };
}

function detectorScope(cell: MatrixCell, detectorId: string): CellScope {
  return {
    id: cell.id,
    detectorId,
    surfaceId: cell.id,
  };
}

function coverageGapScope(gap: CoverageGap, detectorId: string): CellScope {
  return {
    id: sha256Canonical({
      kind: "matrix-coverage-gap-scope",
      detectorId,
      gap,
    }),
    detectorId,
  };
}

function coverageGapContext(gap: CoverageGap): ExecutionContext {
  return {
    kind: "browser",
    cell: {
      id: sha256Canonical({
        kind: "matrix-coverage-gap-cell",
        gap,
      }),
    },
  };
}

function witnessRefForGap(gap: CoverageGap): CoverageEvent["witnessRefs"] {
  return [
    {
      id: sha256Canonical({
        kind: "matrix-coverage-gap-witness",
        gap,
      }),
    },
  ];
}

function resolveAdapter(
  cell: MatrixCell,
  adapters: Record<string, AuthAdapter>,
): AuthAdapter | undefined {
  return adapters[cell.role];
}

function harnessEventFromCellResult(input: {
  outcome: HarnessEventInput;
  cell: MatrixCell;
  adapterId?: string;
  authScope?: string;
}): HarnessEvent {
  const scopeId = input.authScope ?? input.cell.id;
  const plannedContext = input.outcome.plannedContext ?? browserExecutionContext(input.cell);

  return {
    phase: input.outcome.phase,
    scope: {
      id: scopeId,
      ...(input.adapterId === undefined ? {} : { surfaceId: input.adapterId }),
    },
    plannedContext,
    cause: input.outcome.cause,
    artifactRefs: input.outcome.artifactRefs,
  };
}

function harnessEventFromAuthProofFailure(input: {
  cell: MatrixCell;
  adapterId: string;
  error: unknown;
  authScope: string;
}): HarnessEvent {
  const message =
    input.error instanceof Error ? input.error.message : String(input.error);

  return {
    phase: "setup",
    scope: {
      id: input.authScope,
      surfaceId: input.adapterId,
    },
    plannedContext: browserExecutionContext(input.cell),
    cause: {
      code: "auth-proof-verification-failed",
      message,
      retryable: true,
    },
    artifactRefs: [],
  };
}

function harnessEventFromDetectorFailure(input: {
  cell: MatrixCell;
  detectorId: string;
  error: unknown;
}): HarnessEvent {
  const message = input.error instanceof Error ? input.error.message : String(input.error);

  return {
    phase: "detector",
    scope: detectorScope(input.cell, input.detectorId),
    plannedContext: browserExecutionContext(input.cell),
    cause: {
      code: "detector-evaluation-failed",
      message,
      retryable: true,
    },
    artifactRefs: [],
  };
}

function toClassifierOutcome(
  outcome: SdkDetectorOutcome,
  cell: MatrixCell,
): DetectorOutcome {
  const mapped: DetectorOutcome = {
    detector: outcome.detector,
    class: outcome.class,
    severity: outcome.severity,
    target: outcome.target,
    context: browserExecutionContext(cell),
    summary: outcome.summary,
    evidence: outcome.evidence,
    artifacts: outcome.artifacts,
    laneEligibility: outcome.laneEligibility,
    scope: detectorScope(cell, outcome.detector.id),
    violation: outcome.violation,
  };

  if (outcome.proofConditionMet !== undefined) {
    mapped.proofConditionMet = outcome.proofConditionMet;
  }
  if (outcome.contractOrConfig !== undefined) {
    mapped.contractOrConfig = outcome.contractOrConfig;
  }
  if (outcome.contextDimensions !== undefined) {
    mapped.contextDimensions = outcome.contextDimensions;
  }

  return mapped;
}

function coverageEventsFromPlanGaps(
  gaps: CoverageGap[],
  detectors: Detector<CellEvidence, unknown>[],
): CoverageEvent[] {
  const events: CoverageEvent[] = [];

  for (const gap of gaps) {
    for (const detector of detectors) {
      events.push({
        scope: coverageGapScope(gap, detector.id),
        context: coverageGapContext(gap),
        reason: gap.reason,
        witnessRefs: witnessRefForGap(gap),
      });
    }
  }

  return events;
}

function createDetectorContext(
  input: RunMatrixInput,
  capabilities: CapabilityProfile,
  clock: () => string,
): DetectorContext {
  return {
    clock,
    seed: input.seed,
    capabilities,
    readStores: asReadonlyKernelStores(input.stores),
  };
}

function isEvidenceResult(
  result: CellResult,
): result is Extract<CellResult, { kind: "evidence" }> {
  return result.kind === "evidence";
}

export async function runMatrix(input: RunMatrixInput): Promise<ClassifiedRun> {
  const capabilities = validateRunMatrixInput(input);

  let clockTick = 0;
  const clockStartMs = Date.parse(input.clockStart);
  const clock = (): string => new Date(clockStartMs + clockTick++).toISOString();
  const detectorContext = createDetectorContext(input, capabilities, clock);

  const sortedCells = [...input.plan.cells].sort((left, right) =>
    compareUnicodeScalars(left.id, right.id),
  );
  const sortedDetectors = [...input.detectors].sort((left, right) => {
    const idDelta = compareUnicodeScalars(left.id, right.id);
    if (idDelta !== 0) {
      return idDelta;
    }
    return compareUnicodeScalars(left.version, right.version);
  });

  const detectorOutcomes: DetectorOutcome[] = [];
  const harnessEvents: HarnessEvent[] = [];
  const coverageEvents = coverageEventsFromPlanGaps(
    input.plan.coverageGaps,
    sortedDetectors,
  );

  const failedAuthRoles = new Set<string>();

  for (const cell of sortedCells) {
    const adapter = resolveAdapter(cell, input.adapters);
    const authScope = authScopeId(cell.role);

    if (adapter !== undefined && failedAuthRoles.has(cell.role)) {
      continue;
    }

    const result = await runCell({
      cell,
      baseUrl: input.baseUrl,
      browser: input.browser,
      seed: input.seed,
      clockStart: input.clockStart,
      capture: input.capture,
      timeouts: input.timeouts,
      ...(adapter === undefined ? {} : { authAdapter: adapter }),
    });

    if (result.kind === "harness") {
      if (adapter !== undefined) {
        if (!failedAuthRoles.has(cell.role)) {
          failedAuthRoles.add(cell.role);
          harnessEvents.push(
            harnessEventFromCellResult({
              outcome: result.outcome,
              cell,
              adapterId: adapter.id,
              authScope,
            }),
          );
        }
      } else {
        harnessEvents.push(
          harnessEventFromCellResult({
            outcome: result.outcome,
            cell,
          }),
        );
      }
      continue;
    }

    if (!isEvidenceResult(result)) {
      continue;
    }

    input.onCellEvidence?.(result.evidence);

    if (adapter !== undefined) {
      if (result.authProof === undefined) {
        if (!failedAuthRoles.has(cell.role)) {
          failedAuthRoles.add(cell.role);
          harnessEvents.push(
            harnessEventFromAuthProofFailure({
              cell,
              adapterId: adapter.id,
              error: new Error("auth adapter did not return proof"),
              authScope,
            }),
          );
        }
        continue;
      }

      try {
        verifyAuthProof(result.authProof, cell);
      } catch (error) {
        if (!failedAuthRoles.has(cell.role)) {
          failedAuthRoles.add(cell.role);
          harnessEvents.push(
            harnessEventFromAuthProofFailure({
              cell,
              adapterId: adapter.id,
              error,
              authScope,
            }),
          );
        }
        continue;
      }
    }

    for (const detector of sortedDetectors) {
      try {
        const outcomes = await detector.evaluate(result.evidence, detectorContext);
        for (const outcome of outcomes) {
          detectorOutcomes.push(toClassifierOutcome(outcome, cell));
        }
      } catch (error) {
        harnessEvents.push(
          harnessEventFromDetectorFailure({
            cell,
            detectorId: detector.id,
            error,
          }),
        );
      }
    }
  }

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