import type { Writable } from "node:stream";
import { sha256Canonical } from "../../../schema/src/canonical.js";
import {
  classify,
  type ClassifiedRun,
  type CoverageEvent,
  type HarnessEvent,
  type KernelStores,
} from "../../../core/src/classify/classifier.js";
import { matchCanonicalGlob } from "../../../core/src/match/scope.js";
import { capabilityProfileRef } from "../../../core/src/sdk/capability.js";
import type { ExecutionContext } from "../../../schema/src/records/context.js";
import type { Finding } from "../../../schema/src/records/finding.js";
import {
  runUniversal,
  type AdapterSurface,
  type UniversalRunPlan,
} from "../../../universal/src/runner.js";
import { loadConfig } from "../config.js";
import { computeExitStatus, type ExitStatus } from "../exit-status.js";
import {
  buildMachineResult,
  emitMachineResult,
  type HookMetadata,
  writeMachineLog,
} from "../machine-output.js";
import type { VerifyOptions } from "../parse-args.js";
import {
  buildCapabilityProfile,
  canonicalStoreRoot,
  compareBaseline,
  coverageEventForContractAuthority,
  createVerificationStores,
  createRunId,
  deriveRunConditions,
  formatLoadError,
  loadSurfaces,
  resolveDetectors,
  type IoContext,
} from "./verify.js";

export const BROADER_VERIFY_COMMAND = "invariantum verify --scope=universal";
export const DEFAULT_HOOK_BUDGET_MS = 250;
const UNIVERSAL_SCOPE = "universal";
let nextHookContextId = 0;

type ImpactUnknownReason = "missing-inputs" | "unmatched-path" | "empty-surfaces";

type ImpactAnalysis =
  | {
      known: true;
      surfaces: AdapterSurface[];
    }
  | {
      known: false;
      reason: ImpactUnknownReason;
    };

function normalizeChangedPath(path: string): string {
  return path.replace(/\\/g, "/").replace(/^\.\//, "");
}

function surfaceDeclaresInputs(surface: AdapterSurface): boolean {
  return surface.inputs !== undefined && surface.inputs.length > 0;
}

function changedPathMatchesSurface(changedPath: string, surface: AdapterSurface): boolean {
  const inputs = surface.inputs;
  if (inputs === undefined || inputs.length === 0) {
    return false;
  }

  return inputs.some((glob) => matchCanonicalGlob(glob, changedPath));
}

export function analyzeImpact(
  surfaces: AdapterSurface[],
  changedPaths: string[],
  dependencyMap: Record<string, string[]> | undefined,
): ImpactAnalysis {
  if (surfaces.length === 0) {
    return { known: false, reason: "empty-surfaces" };
  }

  if (surfaces.some((surface) => !surfaceDeclaresInputs(surface))) {
    return { known: false, reason: "missing-inputs" };
  }

  const normalizedChanged = changedPaths.map(normalizeChangedPath);
  const impactedIds = new Set<string>();
  for (const changedPath of normalizedChanged) {
    const directMatches = surfaces.filter((surface) =>
      changedPathMatchesSurface(changedPath, surface),
    );
    const mappedIds = Object.entries(dependencyMap ?? {})
      .filter(([glob]) => matchCanonicalGlob(glob, changedPath))
      .flatMap(([, surfaceIds]) => surfaceIds);
    const mappedSurfaces = mappedIds.map((surfaceId) => surfaces.find((surface) => surface.id === surfaceId));
    if (mappedSurfaces.some((surface) => surface === undefined)) {
      return { known: false, reason: "unmatched-path" };
    }
    if (directMatches.length === 0 && mappedSurfaces.length === 0) {
      return { known: false, reason: "unmatched-path" };
    }
    for (const surface of [...directMatches, ...mappedSurfaces]) {
      if (surface !== undefined) impactedIds.add(surface.id);
    }
  }

  const impacted = surfaces.filter((surface) => impactedIds.has(surface.id));

  return { known: true, surfaces: impacted };
}

function emptyClassifiedRun(): ClassifiedRun {
  return {
    findings: [],
    hiddenFindings: [],
    coverageOutcomes: [],
    harnessOutcomes: [],
  };
}

function cliExecutionContext(seed: string): ExecutionContext {
  return {
    kind: "cli",
    surfaceId: "invariantum-cli",
    adapterId: "invariantum-cli",
    environment: {},
    seed,
  };
}

function witnessRefId(parts: unknown): string {
  return sha256Canonical(parts);
}

function coverageEventForImpactUnknown(
  seed: string,
  reason: ImpactUnknownReason,
  capabilities: ReturnType<typeof buildCapabilityProfile>,
): CoverageEvent {
  return {
    scope: {
      id: "hook-impact",
      detectorId: "invariantum-cli",
      surfaceId: "invariantum-cli",
    },
    context: cliExecutionContext(seed),
    reason: "unreachable-target",
    witnessRefs: [
      {
        id: witnessRefId({
          kind: "hook-impact-unknown",
          reason,
          seed,
        }),
      },
    ],
    capabilityProfileRef: capabilityProfileRef(capabilities, []),
  };
}

function classifyCoverageEvents(
  events: CoverageEvent[],
  stores: KernelStores,
  runId: string,
): ClassifiedRun {
  if (events.length === 0) {
    return emptyClassifiedRun();
  }

  return classify({
    detectorOutcomes: [],
    harnessEvents: [],
    coverageEvents: events,
    stores,
    runId,
  });
}

function formatExecutionContext(context: Finding["context"]): string {
  if (context.kind === "browser") {
    return `browser:${context.cell.id}`;
  }

  const surfaceId = "surfaceId" in context ? context.surfaceId : "-";
  return `${context.kind}:${surfaceId}`;
}

function reproductionRef(evidence: unknown): string {
  if (typeof evidence !== "object" || evidence === null || !("reproduction" in evidence)) {
    return "-";
  }

  const reproduction = evidence.reproduction;
  if (typeof reproduction !== "object" || reproduction === null) {
    return "-";
  }

  const record = reproduction as Record<string, unknown>;
  const parts = ["fixtureId", "seed", "surfaceId", "detectorId"]
    .map((key) => {
      const value = record[key];
      if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
        return `${key}=${String(value)}`;
      }
      return undefined;
    })
    .filter((part): part is string => part !== undefined);

  return parts.length > 0 ? parts.join(",") : JSON.stringify(reproduction);
}

export function formatCompactFindingLine(finding: Finding): string {
  const contract =
    finding.contractIds.length > 0 ? finding.contractIds.join(",") : "-";
  const artifact =
    finding.artifacts.length > 0
      ? finding.artifacts.map((entry) => entry.relativePath).join(",")
      : "-";

  return [
    `finding:`,
    `id=${finding.id}`,
    `class=${finding.class}`,
    `target=${finding.target.canonical}`,
    `summary=${finding.summary}`,
    `context=${formatExecutionContext(finding.context)}`,
    `contract=${contract}`,
    `artifact=${artifact}`,
    `reproduction=${reproductionRef(finding.evidence)}`,
    `required-command=${BROADER_VERIFY_COMMAND}`,
  ].join(" ");
}

function hookBudgetMs(config: {
  hook?: { budgetMs?: number | undefined } | undefined;
  timeouts?: { phases: Record<string, number> } | undefined;
}): number {
  return config.hook?.budgetMs ?? config.timeouts?.phases.hook ?? DEFAULT_HOOK_BUDGET_MS;
}

function hookTimeoutRun(
  seed: string,
  stores: KernelStores,
  runId: string,
  budgetMs: number,
): ClassifiedRun {
  const event: HarnessEvent = {
    phase: "infrastructure",
    scope: {
      id: witnessRefId({ kind: "hook-budget-timeout", seed, runId }),
      detectorId: "invariantum-cli",
      surfaceId: "invariantum-cli",
    },
    plannedContext: cliExecutionContext(seed),
    cause: {
      code: "hook-budget-exceeded",
      message: `hook budget exceeded: phase=hook budgetMs=${String(budgetMs)}`,
      retryable: true,
    },
    artifactRefs: [],
  };
  return classify({ detectorOutcomes: [], harnessEvents: [event], coverageEvents: [], stores, runId });
}

async function runWithinHookBudget(
  plan: UniversalRunPlan,
  seed: string,
  budgetMs: number,
): Promise<ClassifiedRun> {
  const deadline = Date.now() + budgetMs;
  let combined = emptyClassifiedRun();

  for (const surface of plan.surfaces) {
    const remainingMs = deadline - Date.now();
    if (remainingMs <= 0) {
      return mergeClassifiedRuns(
        combined,
        hookTimeoutRun(seed, plan.stores, plan.runId, budgetMs),
      );
    }

    const result = await runSurfaceWithinHookBudget(
      { ...plan, surfaces: [surface] },
      seed,
      budgetMs,
      remainingMs,
    );
    combined = mergeClassifiedRuns(combined, result);
    if (result.harnessOutcomes.some((outcome) => outcome.cause.code === "hook-budget-exceeded")) {
      return combined;
    }
  }

  return combined;
}

function mergeClassifiedRuns(left: ClassifiedRun, right: ClassifiedRun): ClassifiedRun {
  return {
    findings: [...left.findings, ...right.findings],
    hiddenFindings: [...left.hiddenFindings, ...right.hiddenFindings],
    coverageOutcomes: [...left.coverageOutcomes, ...right.coverageOutcomes],
    harnessOutcomes: [...left.harnessOutcomes, ...right.harnessOutcomes],
  };
}

async function runSurfaceWithinHookBudget(
  plan: UniversalRunPlan,
  seed: string,
  budgetMs: number,
  remainingMs: number,
): Promise<ClassifiedRun> {
  let timer: ReturnType<typeof setTimeout> | undefined;
  const timedRun = runUniversal(plan);
  const timeout = new Promise<ClassifiedRun>((resolve) => {
    timer = setTimeout(() => {
      resolve(hookTimeoutRun(seed, plan.stores, plan.runId, budgetMs));
    }, remainingMs);
  });
  try {
    return await Promise.race([timedRun, timeout]);
  } finally {
    if (timer !== undefined) clearTimeout(timer);
  }
}

function buildHookMetadata(
  options: VerifyOptions,
  impact: ImpactAnalysis,
  selectedSurfaceIds: string[],
): HookMetadata {
  return {
    mode: "advisory",
    impactKnown: impact.known,
    requiredCommand: impact.known ? null : BROADER_VERIFY_COMMAND,
    changedPaths: [...options.changedPaths],
    selectedSurfaceIds,
  };
}

function printHookSummary(
  stderr: Writable,
  result: ReturnType<typeof buildMachineResult>,
  impact: ImpactAnalysis,
  verbose: boolean,
): void {
  if (!impact.known) {
    writeMachineLog(`impact-unknown: run ${BROADER_VERIFY_COMMAND}`, stderr);
    writeMachineLog(`required-command: ${BROADER_VERIFY_COMMAND}`, stderr);
  }

  if (verbose) {
    writeMachineLog(`runId: ${result.runId}`, stderr);
    writeMachineLog(`seed: ${result.environment.seed}`, stderr);
    writeMachineLog(
      `scopes: requested=${result.scopes.requested.join(",") || "(default)"} resolved=${result.scopes.resolved.join(",")}`,
      stderr,
    );
  }

  for (const finding of result.findings) {
    writeMachineLog(formatCompactFindingLine(finding), stderr);
  }

  writeMachineLog(
    `findings: blocking=${String(result.counts.byLane.blocking)} advisory=${String(result.counts.byLane.advisory)}`,
    stderr,
  );
  writeMachineLog(
    `outcomes: coverage=${String(result.coverageOutcomes.length)} harness=${String(result.harnessOutcomes.length)}`,
    stderr,
  );
  writeMachineLog(`exit: ${String(result.exitStatus)}`, stderr);
}

export async function runHook(options: VerifyOptions, io: IoContext): Promise<ExitStatus> {
  const startedAt = process.hrtime.bigint();
  const configResult = loadConfig(options.configPath, io.env);
  if (!configResult.ok) {
    writeMachineLog(`config error: ${configResult.error.message}`, io.stderr);
    writeMachineLog(configResult.error.recoveryInstructions, io.stderr);
    return 2;
  }

  const config = configResult.config;
  const root = canonicalStoreRoot(config, io.cwd);
  const requestedScopes =
    options.scopes.length === 0 ? [UNIVERSAL_SCOPE] : [...options.scopes];
  const defaultScopeApplied = options.scopes.length === 0;
  const clockStart = new Date().toISOString();
  const seed = options.seed ?? config.seed ?? "invariantum-default-seed";
  const runId = createRunId({
    seed,
    configPath: options.configPath,
    scopes: requestedScopes,
  });

  let stores: KernelStores;
  let authorityErrors: string[];
  try {
    ({ stores, authorityErrors } = await createVerificationStores(root, clockStart));
  } catch (error) {
    writeMachineLog(`blocking state error: ${formatLoadError(error)}`, io.stderr);
    writeMachineLog("Repair or restore the canonical decision and contract stores before rerunning invariantum.", io.stderr);
    return 2;
  }
  const capabilities = buildCapabilityProfile(config);
  let run = emptyClassifiedRun();
  if (authorityErrors.length > 0) {
    run = classifyCoverageEvents(
      authorityErrors.map((error) => coverageEventForContractAuthority(error, seed, capabilities)),
      stores,
      runId,
    );
  }
  let impact: ImpactAnalysis = { known: false, reason: "empty-surfaces" };
  let selectedSurfaceIds: string[] = [];

  try {
    const isolationKey = `${runId}-${String(nextHookContextId++)}`;
    const surfaces = await loadSurfaces(config, options.configPath, io.cwd, isolationKey);
    impact = analyzeImpact(surfaces, options.changedPaths, config.hook?.dependencyMap);

    if (impact.known) {
      selectedSurfaceIds = impact.surfaces.map((surface) => surface.id);
      const detectors = resolveDetectors(config);
      const plan: UniversalRunPlan = {
        detectors,
        surfaces: impact.surfaces,
        context: {
          seed,
          clockStart,
          capabilities,
        },
        stores,
        runId,
      };
      run = mergeClassifiedRuns(
        run,
        await runWithinHookBudget(plan, seed, hookBudgetMs(config)),
      );
    } else {
      const coverageRun = classifyCoverageEvents(
        [coverageEventForImpactUnknown(seed, impact.reason, capabilities)],
        stores,
        runId,
      );
      run = mergeClassifiedRuns(run, {
        findings: coverageRun.findings,
        hiddenFindings: coverageRun.hiddenFindings,
        coverageOutcomes: coverageRun.coverageOutcomes,
        harnessOutcomes: coverageRun.harnessOutcomes,
      });
    }
  } catch (error) {
    writeMachineLog(`hook failed: ${formatLoadError(error)}`, io.stderr);
    return 2;
  }

  let baselineComparison: Awaited<ReturnType<typeof compareBaseline>>;
  try {
    baselineComparison = await compareBaseline(root, run, stores, false);
  } catch (error) {
    writeMachineLog(`baseline state error: ${formatLoadError(error)}`, io.stderr);
    writeMachineLog("Repair or restore the canonical baseline before rerunning invariantum.", io.stderr);
    return 2;
  }
  const conditions = deriveRunConditions(run, {
    coverageIncomplete:
      !impact.known ||
      baselineComparison.missing ||
      baselineComparison.comparison.status === "coverage-incomplete",
  });
  const machineResult = buildMachineResult({
    runId,
    requestedScopes,
    defaultScopeApplied,
    run,
    conditions,
    timings: {
      totalMs: Number(process.hrtime.bigint() - startedAt) / 1_000_000,
    },
    environment: {
      platform: process.platform,
      node: process.version,
      seed,
    },
    hook: buildHookMetadata(options, impact, selectedSurfaceIds),
  });

  if (options.outputJson) {
    emitMachineResult(machineResult, io.stdout);
  }

  printHookSummary(io.stderr, machineResult, impact, options.verbose);

  return computeExitStatus(conditions);
}
