import { createServer, type Server } from "node:http";
import { readFile } from "node:fs/promises";
import { extname, join, resolve } from "node:path";
import type { AddressInfo } from "node:net";
import { chromium, type Browser } from "playwright";
import { createReadonlyStores } from "../../cli/src/commands/verify.js";
import type { CapabilityProfile } from "../../core/src/sdk/capability.js";
import {
  renderedDetectorByRuleId,
  runRenderedMatrix,
  type RenderedMatrixPlan,
  type RenderedRuleId,
} from "../../ui-detectors/src/rendered/index.js";
import {
  geometryDetectorByRuleId,
  runGeometryMatrix,
  type GeometryRuleId,
} from "../../ui-detectors/src/geometry/index.js";
import {
  layoutDetectorByRuleId,
  runLayoutMatrix,
  type LayoutRuleId,
} from "../../ui-detectors/src/layout/index.js";
import {
  interactionDetectorByRuleId,
  runInteractionMatrix,
  type InteractionRuleId,
} from "../../ui-detectors/src/interaction/index.js";
import {
  consistencyDetectorByRuleId,
  runConsistencyMatrix,
  type ConsistencyRuleId,
} from "../../ui-detectors/src/consistency/index.js";
import {
  relationsDetectorByRuleId,
  runRelationsMatrix,
  type RelationsRuleId,
} from "../../ui-detectors/src/relations/index.js";
import {
  runSweepsMatrix,
  sweepsDetectorByRuleId,
  type SweepsRuleId,
} from "../../ui-detectors/src/sweeps/index.js";
import {
  a11yDetectorByRuleId,
  runA11yMatrix,
  type A11yRuleId,
} from "../../ui-detectors/src/a11y/index.js";
import {
  assetsDetectorByRuleId,
  runAssetsMatrix,
  type AssetsRuleId,
} from "../../ui-detectors/src/assets/index.js";
import type { SweepsMatrixPlan } from "../../ui-detectors/src/sweeps/matrix.js";
import type { CaptureOptions } from "../../playwright/src/cell-runner.js";
import type { Finding } from "../../schema/src/records/finding.js";
import type { KernelStores } from "../../core/src/classify/classifier.js";
import { readManifest, scoreFindings, type CorpusScore } from "./harness.js";

const MIME_TYPES: Record<string, string> = {
  ".html": "text/html; charset=utf-8",
  ".js": "text/javascript; charset=utf-8",
  ".mjs": "text/javascript; charset=utf-8",
  ".css": "text/css; charset=utf-8",
  ".json": "application/json; charset=utf-8",
  ".svg": "image/svg+xml",
};

const DEFAULT_TIMEOUTS = { navigateMs: 10_000, settleMs: 3_000 };
const DEFAULT_CAPTURE: CaptureOptions = {
  console: true,
  network: true,
  domSnapshot: false,
  screenshot: false,
  trace: false,
  performance: false,
};
const DEFAULT_CAPABILITIES: CapabilityProfile = { platform: "linux", features: {} };

function normalizeFixtureFinding(finding: Finding, baseUrl: string): Finding {
  if (!finding.target.canonical.startsWith(baseUrl)) {
    return finding;
  }

  return {
    ...finding,
    target: {
      ...finding.target,
      canonical: finding.target.canonical.slice(baseUrl.length) || "/",
    },
  };
}

export type RunCorpusUiFixtureInput = {
  publicDir: string;
  plan: RenderedMatrixPlan;
  ruleIds: RenderedRuleId[];
  seed: string;
  runId: string;
  clockStart: string;
  timeouts?: { navigateMs: number; settleMs: number };
  capture?: CaptureOptions;
  capabilities?: CapabilityProfile;
};

export type CorpusMatrixSelector =
  | { matrix: "rendered"; ruleIds: RenderedRuleId[] }
  | { matrix: "geometry"; ruleIds: GeometryRuleId[] }
  | { matrix: "layout"; ruleIds: LayoutRuleId[] }
  | { matrix: "interaction"; ruleIds: InteractionRuleId[] }
  | { matrix: "consistency"; ruleIds: ConsistencyRuleId[] }
  | { matrix: "relations"; ruleIds: RelationsRuleId[] }
  | { matrix: "sweeps"; ruleIds: SweepsRuleId[] }
  | { matrix: "a11y"; ruleIds: A11yRuleId[] }
  | { matrix: "assets"; ruleIds: AssetsRuleId[] };

export type CorpusMatrixName = CorpusMatrixSelector["matrix"];

export type RunCorpusMatrixFixtureInput = CorpusMatrixSelector & {
  publicDir: string;
  // Superset of every family's plan: identical `cells`/`coverageGaps` plus the
  // sweeps-only `enabledDimensions`.
  plan: SweepsMatrixPlan;
  seed: string;
  runId: string;
  clockStart: string;
  timeouts?: { navigateMs: number; settleMs: number };
  capture?: CaptureOptions;
  capabilities?: CapabilityProfile;
};

function startStaticServer(root: string): Promise<{ server: Server; baseUrl: string }> {
  const absoluteRoot = resolve(root);

  return new Promise((resolveServer, reject) => {
    const server = createServer((req, res) => {
      void (async () => {
        const requestPath = (req.url ?? "/").split("?")[0] ?? "/";
        const relativePath = requestPath === "/" ? "/index.html" : requestPath;
        const filePath = join(absoluteRoot, relativePath);
        if (!filePath.startsWith(absoluteRoot)) {
          res.writeHead(403).end();
          return;
        }
        try {
          const body = await readFile(filePath);
          const mediaType = MIME_TYPES[extname(filePath)] ?? "application/octet-stream";
          res.writeHead(200, { "content-type": mediaType }).end(body);
        } catch (error) {
          if ((error as NodeJS.ErrnoException).code === "ENOENT") {
            res.writeHead(404).end();
            return;
          }
          res.writeHead(500).end(String(error));
        }
      })();
    });

    server.once("error", reject);
    server.listen(0, "127.0.0.1", () => {
      const address = server.address() as AddressInfo;
      resolveServer({ server, baseUrl: `http://127.0.0.1:${String(address.port)}` });
    });
  });
}

function stopServer(server: Server): Promise<void> {
  return new Promise((resolveStop, reject) => {
    server.close((error) => {
      if (error) {
        reject(error);
        return;
      }
      resolveStop();
    });
  });
}

const CAPTURE_WITH_SCREENSHOT: CaptureOptions = { ...DEFAULT_CAPTURE, screenshot: true };

async function evaluateMatrix(
  baseUrl: string,
  input: RunCorpusMatrixFixtureInput,
  stores: KernelStores,
): Promise<Finding[]> {
  let browser: Browser | undefined;

  try {
    browser = await chromium.launch();
    const common = {
      plan: input.plan,
      baseUrl,
      stores,
      runId: input.runId,
      seed: input.seed,
      clockStart: input.clockStart,
      browser,
      capture: input.capture ?? CAPTURE_WITH_SCREENSHOT,
      timeouts: input.timeouts ?? DEFAULT_TIMEOUTS,
      capabilities: input.capabilities ?? DEFAULT_CAPABILITIES,
    };

    const run = await (() => {
      switch (input.matrix) {
        case "rendered":
          return runRenderedMatrix({
            ...common,
            detectors: input.ruleIds.map((ruleId) => renderedDetectorByRuleId[ruleId]),
          });
        case "geometry":
          return runGeometryMatrix({
            ...common,
            detectors: input.ruleIds.map((ruleId) => geometryDetectorByRuleId[ruleId]),
          });
        case "layout":
          return runLayoutMatrix({
            ...common,
            detectors: input.ruleIds.map((ruleId) => layoutDetectorByRuleId[ruleId]),
          });
        case "interaction":
          return runInteractionMatrix({
            ...common,
            detectors: input.ruleIds.map((ruleId) => interactionDetectorByRuleId[ruleId]),
          });
        case "consistency":
          return runConsistencyMatrix({
            ...common,
            detectors: input.ruleIds.map((ruleId) => consistencyDetectorByRuleId[ruleId]),
          });
        case "relations":
          return runRelationsMatrix({
            ...common,
            detectors: input.ruleIds.map((ruleId) => relationsDetectorByRuleId[ruleId]),
          });
        case "sweeps":
          return runSweepsMatrix({
            ...common,
            detectors: input.ruleIds.map((ruleId) => sweepsDetectorByRuleId[ruleId]),
          });
        case "a11y":
          return runA11yMatrix({
            ...common,
            detectors: input.ruleIds.map((ruleId) => a11yDetectorByRuleId[ruleId]),
          });
        case "assets":
          return runAssetsMatrix({
            ...common,
            detectors: input.ruleIds.map((ruleId) => assetsDetectorByRuleId[ruleId]),
          });
        default: {
          const exhaustive: never = input;
          throw new Error(`unsupported corpus matrix: ${JSON.stringify(exhaustive)}`);
        }
      }
    })();
    return run.findings.map((finding) => normalizeFixtureFinding(finding, baseUrl));
  } finally {
    await browser?.close();
  }
}

function renderedMatrixInput(input: RunCorpusUiFixtureInput): RunCorpusMatrixFixtureInput {
  return { ...input, matrix: "rendered" };
}

async function readonlyStores(clockStart: string): Promise<KernelStores> {
  const { stores } = await createReadonlyStores(clockStart);
  return stores;
}

/**
 * Executes rendered UI detectors against a corpus fixture's static pages via
 * the real `runRenderedMatrix` product pipeline (the same pipeline
 * `packages/ui-detectors/test/rendered.test.ts` exercises), rather than the
 * CLI subprocess used by `runCorpusFixture`. UI scopes are not yet wired into
 * `verify --scope=ui` (M1 preview), so this is the only path that can prove
 * real rendered-detector positives/negatives end to end today.
 */
export async function runCorpusUiFixture(input: RunCorpusUiFixtureInput): Promise<Finding[]> {
  const { server, baseUrl } = await startStaticServer(input.publicDir);

  try {
    return await evaluateMatrix(
      baseUrl,
      renderedMatrixInput(input),
      await readonlyStores(input.clockStart),
    );
  } finally {
    await stopServer(server);
  }
}

/**
 * Runs any UI detector family against a corpus fixture's static pages through
 * that family's real product matrix runner, so a fixture can prove positives
 * and negatives for every registered detector class rather than only the
 * rendered and geometry families.
 */
export async function runCorpusMatrixFixture(
  input: RunCorpusMatrixFixtureInput,
  stores: KernelStores,
): Promise<Finding[]> {
  const { server, baseUrl } = await startStaticServer(input.publicDir);

  try {
    return await evaluateMatrix(baseUrl, input, stores);
  } finally {
    await stopServer(server);
  }
}

/**
 * Runs the same fixture input twice against one shared static server so a
 * determinism comparison exercises only the detection pipeline. Each
 * `runCorpusUiFixture` call binds a fresh ephemeral port; comparing findings
 * across two separately started servers would fail on the server's own
 * per-run port rather than on detector output, since several rendered
 * detectors (e.g. UI-011's console-error locator) legitimately record the
 * page's live navigated URL as evidence.
 */
export async function runCorpusUiFixtureTwice(
  input: RunCorpusUiFixtureInput,
): Promise<[Finding[], Finding[]]> {
  const { server, baseUrl } = await startStaticServer(input.publicDir);

  try {
    const matrixInput = renderedMatrixInput(input);
    const first = await evaluateMatrix(baseUrl, matrixInput, await readonlyStores(input.clockStart));
    const second = await evaluateMatrix(baseUrl, matrixInput, await readonlyStores(input.clockStart));
    return [first, second];
  } finally {
    await stopServer(server);
  }
}

export async function scoreCorpusUiFixture(
  fixtureDir: string,
  input: RunCorpusUiFixtureInput,
): Promise<CorpusScore> {
  const manifest = await readManifest(fixtureDir);
  const findings = await runCorpusUiFixture(input);
  return scoreFindings(manifest, findings);
}
