import { execFile as execFileCallback } from "node:child_process";
import { access, readFile } from "node:fs/promises";
import { dirname, isAbsolute, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { promisify } from "node:util";
import { loadContractStore } from "../../../core/src/contracts/store.js";
import { loadDecisionStore } from "../../../core/src/decisions/store.js";
import { loadConfig, type InvariantumConfig } from "../config.js";
import type { DoctorOptions } from "../parse-args.js";
import { buildCapabilityProfile, loadSurface, type IoContext } from "./verify.js";

const DOCTOR_RESULT_VERSION = "doctor-result/v1" as const;
const execFile = promisify(execFileCallback);

type CheckStatus = "failed" | "not-configured" | "not-required" | "passed" | "skipped";
type Check = { id: string; status: CheckStatus; detail: string };

function check(id: string, status: CheckStatus, detail: string): Check {
  return { id, status, detail };
}

function adapterSpecifier(moduleRef: string, cwd: string): string {
  if (moduleRef.startsWith("node:") || moduleRef.startsWith("@") || (!moduleRef.startsWith(".") && !moduleRef.startsWith("/"))) {
    return moduleRef;
  }
  return pathToFileURL(isAbsolute(moduleRef) ? moduleRef : resolve(cwd, moduleRef)).href;
}

async function checkAdapters(config: InvariantumConfig, options: DoctorOptions, io: IoContext): Promise<Check> {
  try {
    for (const surface of config.surfaces) {
      await loadSurface(surface, options.configPath, io.cwd);
    }
    for (const adapter of config.discovery?.adapters ?? []) {
      const module = await import(adapterSpecifier(adapter.module, io.cwd)) as Record<string, unknown>;
      if (typeof module.discover !== "function") throw new Error(`discovery adapter ${adapter.id} must export discover()`);
    }
    for (const adapter of config.credentials?.adapters ?? []) {
      if (io.env[adapter.reference.replace(/^\$\{(.+)\}$/, "$1")] === undefined) {
        throw new Error(`credential adapter ${adapter.id} requires ${adapter.reference}`);
      }
    }
    return check("adapter-availability", "passed", "all configured adapters are available");
  } catch (error) {
    return check("adapter-availability", "failed", error instanceof Error ? error.message : String(error));
  }
}

async function checkStoreIntegrity(config: InvariantumConfig, io: IoContext): Promise<Check> {
  if (config.acceptedFindings?.store === undefined) {
    return check("store-integrity", "not-configured", "no canonical decision store configured");
  }
  try {
    const root = dirname(resolve(io.cwd, config.acceptedFindings.store));
    await Promise.all([loadDecisionStore(root), loadContractStore(root)]);
    return check("store-integrity", "passed", "canonical decision and contract stores are valid");
  } catch (error) {
    return check("store-integrity", "failed", error instanceof Error ? error.message : String(error));
  }
}

async function checkBrowserRuntime(config: InvariantumConfig): Promise<Check> {
  const dimension = config.matrix?.dimensions.browser;
  if (dimension === undefined || dimension.length === 0) {
    return check("browser-runtime", "not-required", "no browser matrix dimension is configured");
  }
  const browsers = dimension.filter((entry): entry is string => typeof entry === "string");
  if (browsers.length === 0) {
    return check("browser-runtime", "failed", "browser matrix dimension contains no string engine names");
  }
  try {
    const playwright = await import("playwright");
    const engines = { chromium: playwright.chromium, firefox: playwright.firefox, webkit: playwright.webkit };
    const results = await Promise.all([...browsers].sort().map(async (browser) => {
      if (!(browser in engines)) return { browser, available: false, detail: "unsupported Playwright engine" };
      const engine = engines[browser as keyof typeof engines];
      try {
        const executable = engine.executablePath();
        await access(executable);
        const instance = await engine.launch({ headless: true });
        await instance.close();
        return { browser, available: true, detail: `available at ${executable}` };
      } catch (error) {
        return { browser, available: false, detail: error instanceof Error ? error.message : String(error) };
      }
    }));
    const detail = results.map((result) => `${result.browser}: ${result.detail}`).join("; ");
    return check("browser-runtime", results.every((result) => result.available) ? "passed" : "failed", detail);
  } catch (error) {
    return check("browser-runtime", "failed", `browser runtime unavailable: ${error instanceof Error ? error.message : String(error)}`);
  }
}

async function checkNodeRuntime(): Promise<Check> {
  try {
    const packageJson = JSON.parse(await readFile(new URL("../../package.json", import.meta.url), "utf8")) as { engines?: { node?: unknown } };
    const required = packageJson.engines?.node;
    if (typeof required !== "string") throw new Error("packages/cli package.json must declare node engine as >=<major>");
    const match = /^>=(\d+)$/.exec(required);
    if (match === null) throw new Error("packages/cli package.json must declare node engine as >=<major>");
    const actual = Number.parseInt(process.versions.node.split(".", 1)[0] ?? "", 10);
    const minimum = Number.parseInt(match[1] ?? "", 10);
    if (!Number.isSafeInteger(actual) || !Number.isSafeInteger(minimum)) throw new Error("invalid Node runtime version");
    return actual >= minimum
      ? check("node-runtime", "passed", `Node ${process.version} satisfies ${required}`)
      : check("node-runtime", "failed", `Node ${process.version} does not satisfy ${required}`);
  } catch (error) {
    return check("node-runtime", "failed", error instanceof Error ? error.message : String(error));
  }
}

async function checkPnpmRuntime(io: IoContext): Promise<Check> {
  try {
    const { stdout } = await execFile("pnpm", ["--version"], { env: io.env });
    const version = stdout.trim();
    if (version.length === 0) throw new Error("pnpm returned no version");
    return check("pnpm-runtime", "passed", `pnpm ${version} is available`);
  } catch (error) {
    return check("pnpm-runtime", "failed", `pnpm unavailable: ${error instanceof Error ? error.message : String(error)}`);
  }
}

function checkCapabilityProfile(config: InvariantumConfig): Check {
  try {
    const profile = buildCapabilityProfile(config);
    return check("capability-profile", "passed", `capability profile contains ${String(Object.keys(profile.features).length)} features`);
  } catch (error) {
    return check("capability-profile", "failed", error instanceof Error ? error.message : String(error));
  }
}

function emit(checks: Check[], io: IoContext, verbose: boolean): void {
  const ordered = [...checks].sort((left, right) => left.id.localeCompare(right.id));
  io.stdout.write(`${JSON.stringify({ schemaVersion: DOCTOR_RESULT_VERSION, checks: ordered })}\n`);
  if (verbose) {
    for (const item of ordered) io.stderr.write(`doctor ${item.id}: ${item.status}: ${item.detail}\n`);
  }
}

export async function runDoctor(options: DoctorOptions, io: IoContext): Promise<0 | 2 | 3> {
  const configResult = loadConfig(options.configPath, io.env);
  if (!configResult.ok) {
    emit([
      check("config-schema", "failed", `${configResult.error.message} Recovery: ${configResult.error.recoveryInstructions}`),
      check("store-integrity", "skipped", "config schema is invalid"),
      check("browser-runtime", "skipped", "config schema is invalid"),
      check("node-runtime", "skipped", "config schema is invalid"),
      check("pnpm-runtime", "skipped", "config schema is invalid"),
      check("adapter-availability", "skipped", "config schema is invalid"),
      check("capability-profile", "skipped", "config schema is invalid"),
    ], io, options.verbose);
    return 2;
  }

  const config = configResult.config;
  const [store, browser, node, pnpm, adapters] = await Promise.all([
    checkStoreIntegrity(config, io),
    checkBrowserRuntime(config),
    checkNodeRuntime(),
    checkPnpmRuntime(io),
    checkAdapters(config, options, io),
  ]);
  const capabilities = checkCapabilityProfile(config);
  const checks = [check("config-schema", "passed", "config schema is valid"), store, browser, node, pnpm, adapters, capabilities];
  emit(checks, io, options.verbose);

  if (checks.some((item) => ["store-integrity", "capability-profile"].includes(item.id) && item.status === "failed")) return 2;
  if (checks.some((item) => item.status === "failed")) return 3;
  return 0;
}
