import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
import { extname, join, resolve } from "node:path";
import { diagnose, formatWrn } from "@wrnexus/syntax";
import { buildRouter, findRouteConflicts } from "@wrnexus/router";
import { loadAppConfig, validateAppConfig } from "@wrnexus/styles";
import { createPluginRunner, discoverPlugins } from "@wrnexus/plugin";

export interface DoctorCheck {
  name: string;
  ok: boolean;
  detail: string;
  level?: "error" | "warning";
}

export interface DoctorRepair {
  name: string;
  changed: boolean;
  detail: string;
}

function parseVersion(value: string): [number, number, number] {
  const match = /^(\d+)\.(\d+)\.(\d+)/.exec(value.replace(/^[^\d]*/, ""));
  return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : [0, 0, 0];
}

function versionAtLeast(value: string, minimum: string): boolean {
  const left = parseVersion(value);
  const right = parseVersion(minimum);
  for (let i = 0; i < 3; i++) {
    if (left[i] !== right[i]) return left[i]! > right[i]!;
  }
  return true;
}

function walk(dir: string, extension: string): string[] {
  if (!existsSync(dir)) return [];
  const files: string[] = [];
  for (const entry of readdirSync(dir)) {
    if (["node_modules", "dist", ".git", ".wrnexus"].includes(entry)) continue;
    const path = join(dir, entry);
    const stat = statSync(path);
    if (stat.isDirectory()) files.push(...walk(path, extension));
    else if (stat.isFile() && extname(path) === extension) files.push(path);
  }
  return files;
}

function frameworkRanges(pkg: Record<string, unknown>): Map<string, string[]> {
  const ranges = new Map<string, string[]>();
  for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
    const deps = pkg[field] as Record<string, string> | undefined;
    for (const [name, range] of Object.entries(deps ?? {})) {
      if (!name.startsWith("@wrnexus/")) continue;
      const values = ranges.get(range) ?? [];
      values.push(name);
      ranges.set(range, values);
    }
  }
  return ranges;
}

export function repairProject(appRoot: string): DoctorRepair[] {
  const root = resolve(appRoot);
  const repairs: DoctorRepair[] = [];
  const pages = join(root, "app", "pages");
  if (!existsSync(pages)) {
    mkdirSync(pages, { recursive: true });
    repairs.push({ name: "app/pages", changed: true, detail: "created app/pages" });
  }

  const configNames = ["wrnexus.config.ts", "wrnexus.config.mjs", "wrnexus.config.js"];
  if (!configNames.some((name) => existsSync(join(root, name)))) {
    writeFileSync(join(root, "wrnexus.config.ts"), "export default {};\n", "utf8");
    repairs.push({ name: "configuration", changed: true, detail: "created wrnexus.config.ts" });
  }

  const pkgPath = join(root, "package.json");
  if (existsSync(pkgPath)) {
    try {
      const source = readFileSync(pkgPath, "utf8");
      const pkg = JSON.parse(source) as Record<string, unknown>;
      let changed = false;
      const marker = (pkg.wrnexus as Record<string, unknown> | undefined) ?? {};
      if (!marker.version || !versionAtLeast(String(marker.version), "0.8.0")) {
        marker.version = "0.8.0";
        pkg.wrnexus = marker;
        changed = true;
      }
      if (changed) {
        writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
        repairs.push({
          name: "package.json",
          changed: true,
          detail: "recorded 0.8.0 migration marker",
        });
      }
    } catch {
      repairs.push({ name: "package.json", changed: false, detail: "skipped invalid JSON" });
    }
  }

  let formatted = 0;
  for (const file of walk(join(root, "app"), ".wrn")) {
    const source = readFileSync(file, "utf8");
    if (diagnose(source).some((item) => item.severity === "error")) continue;
    const output = formatWrn(source, { tabSize: 2, printWidth: 100, multilineAttributes: true });
    if (output !== source) {
      writeFileSync(file, output, "utf8");
      formatted++;
    }
  }
  if (formatted) {
    repairs.push({ name: "WRN formatting", changed: true, detail: `formatted ${formatted} files` });
  }
  return repairs;
}

export function inspectProject(appRoot: string): DoctorCheck[] {
  const root = resolve(appRoot);
  const checks: DoctorCheck[] = [];
  const pkgPath = join(root, "package.json");
  const bunVersion = typeof Bun !== "undefined" ? String(Bun.version) : "";
  checks.push({
    name: "Bun runtime",
    ok: !!bunVersion && versionAtLeast(bunVersion, "1.3.0"),
    detail: bunVersion ? `v${bunVersion} (minimum 1.3.0)` : "Bun 1.3.0 or newer is required",
  });
  checks.push({
    name: "package.json",
    ok: existsSync(pkgPath),
    detail: existsSync(pkgPath) ? pkgPath : "Run this command from a WRNexus project root",
  });

  if (existsSync(pkgPath)) {
    try {
      const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>;
      const ranges = frameworkRanges(pkg);
      const invalid = [...ranges.keys()].filter((range) => !/[~^]?\d+\.\d+\.\d+/.test(range));
      checks.push({
        name: "framework package versions",
        ok: invalid.length === 0,
        detail: invalid.length
          ? `invalid ranges: ${invalid.join(", ")}`
          : `${[...ranges.values()].reduce((count, names) => count + names.length, 0)} independently versioned package(s)`,
      });
      const marker = (pkg.wrnexus as { version?: string } | undefined)?.version;
      checks.push({
        name: "update marker",
        ok: !marker || versionAtLeast(marker, "0.4.0"),
        detail:
          marker && versionAtLeast(marker, "0.4.0")
            ? `project last migrated to ${marker}`
            : marker
              ? `project is on ${marker}; run \`wrnexus update 0.4.0\``
              : "missing; run `wrnexus update`",
        level: "warning",
      });
    } catch {
      checks.push({ name: "package JSON", ok: false, detail: "package.json is invalid JSON" });
    }
  }

  const app = join(root, "app");
  checks.push({
    name: "app/pages",
    ok: existsSync(join(app, "pages")),
    detail: existsSync(join(app, "pages")) ? "page directory found" : "Create app/pages",
  });
  const config = ["wrnexus.config.ts", "wrnexus.config.mjs", "wrnexus.config.js"].find((name) =>
    existsSync(join(root, name)),
  );
  checks.push({
    name: "configuration",
    ok: !!config,
    detail: config ?? "No wrnexus.config file; framework defaults will be used",
    level: "warning",
  });

  const wrnFiles = walk(app, ".wrn");
  let syntaxErrors = 0;
  let syntaxWarnings = 0;
  for (const file of wrnFiles) {
    const diagnostics = diagnose(readFileSync(file, "utf8"), { file, accessibility: true });
    syntaxErrors += diagnostics.filter((item) => item.severity === "error").length;
    syntaxWarnings += diagnostics.filter((item) => item.severity !== "error").length;
  }
  checks.push({
    name: "WRN language",
    ok: syntaxErrors === 0,
    detail: `${wrnFiles.length} files, ${syntaxErrors} errors, ${syntaxWarnings} warnings`,
  });

  if (existsSync(join(app, "pages"))) {
    try {
      const router = buildRouter(app);
      const conflicts = [
        ...findRouteConflicts(router.pages),
        ...findRouteConflicts(router.api),
        ...findRouteConflicts(router.realtime),
      ];
      checks.push({
        name: "route manifest",
        ok: conflicts.length === 0,
        detail: conflicts.length
          ? conflicts.map((conflict) => conflict.raw).join(", ")
          : `${router.pages.length} pages, ${router.api.length} API, ${router.realtime.length} realtime`,
      });
    } catch (error) {
      checks.push({
        name: "route manifest",
        ok: false,
        detail: error instanceof Error ? error.message : String(error),
      });
    }
  }

  const mobilePkg = join(root, "mobile", "package.json");
  if (existsSync(mobilePkg)) {
    try {
      const mobile = JSON.parse(readFileSync(mobilePkg, "utf8")) as { wrnexus?: { mode?: string } };
      checks.push({
        name: "mobile project",
        ok: mobile.wrnexus?.mode === "webview" || mobile.wrnexus?.mode === "native",
        detail: `mode: ${mobile.wrnexus?.mode ?? "missing"}`,
      });
    } catch {
      checks.push({
        name: "mobile project",
        ok: false,
        detail: "mobile/package.json is invalid JSON",
      });
    }
  }
  return checks;
}

export async function runDoctor(
  appRoot: string,
  options: { fix?: boolean } = {},
): Promise<boolean> {
  const root = resolve(appRoot);
  const repairs = options.fix ? repairProject(root) : [];
  const checks = inspectProject(root);
  try {
    const config = await loadAppConfig(root);
    const issues = validateAppConfig(config);
    checks.push({
      name: "resolved configuration",
      ok: issues.every((issue) => issue.severity !== "error"),
      detail: issues.length
        ? issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")
        : "valid",
      level: issues.some((issue) => issue.severity === "error") ? "error" : "warning",
    });

    const discovered = await discoverPlugins(root, config.plugins, {
      includeDevDependencies: true,
      strict: true,
      warn: (message) =>
        checks.push({ name: "package plugin discovery", ok: false, detail: message }),
    });
    const runner = createPluginRunner(discovered, {
      root,
      mode: "development",
      command: "dev",
      metadata: new Map(),
      warn: () => {},
    });
    await runner.configure(config as Record<string, unknown>);
    await runner.configResolved(config as Readonly<Record<string, unknown>>);
    const contributions = await runner.contributions();
    checks.push({
      name: "package plugins",
      ok: true,
      detail: `${runner.plugins.length} plugins, ${contributions.clientRuntimes.length} runtimes, ${contributions.assets.length} assets, ${contributions.routes.length} routes, ${contributions.migrations.length} migrations`,
    });

    const missingContributions = [
      ...contributions.componentDirs.map((path) => ({ kind: "component directory", path })),
      ...contributions.clientRuntimes
        .filter((runtime) => runtime.entry)
        .map((runtime) => ({ kind: `runtime ${runtime.id}`, path: runtime.entry! })),
      ...contributions.assets
        .filter((asset) => asset.entry)
        .map((asset) => ({ kind: `asset ${asset.id}`, path: asset.entry! })),
      ...contributions.routes.map((route) => ({
        kind: `${route.kind} route ${route.path}`,
        path: route.entry,
      })),
      ...contributions.middleware.map((path) => ({ kind: "middleware", path })),
      ...contributions.migrations
        .filter((migration) => migration.entry)
        .map((migration) => ({ kind: `migration ${migration.id}`, path: migration.entry! })),
    ].filter((entry) => !existsSync(entry.path));
    checks.push({
      name: "package contribution files",
      ok: missingContributions.length === 0,
      detail: missingContributions.length
        ? missingContributions.map((entry) => `${entry.kind}: ${entry.path}`).join("; ")
        : "all discovered contribution files exist",
    });

    const legacyCaptchaAssets = [
      "public/assets/wrnexus/captcha.js",
      "public/__wrnexus/captcha.js",
    ].filter((path) => existsSync(join(root, path)));
    checks.push({
      name: "legacy CAPTCHA runtime copies",
      ok: legacyCaptchaAssets.length === 0,
      detail: legacyCaptchaAssets.length
        ? `remove with wrnexus update 0.4.0: ${legacyCaptchaAssets.join(", ")}`
        : "none; CAPTCHA runtime is package-managed",
      level: "warning",
    });
  } catch (error) {
    checks.push({
      name: "resolved configuration",
      ok: false,
      detail: error instanceof Error ? error.message : String(error),
    });
  }

  console.log("WRNexus doctor\n");
  for (const repair of repairs) console.log(`  ↻ ${repair.name}: ${repair.detail}`);
  if (repairs.length) console.log("");
  for (const check of checks) {
    const optional = check.level === "warning";
    console.log(`  ${check.ok ? "✓" : optional ? "⚠" : "✗"} ${check.name}: ${check.detail}`);
  }
  console.log("\n  Security dependencies: run `bun audit`");
  console.log("  Complete verification: run `bun run check`");
  return checks.every((check) => check.ok || check.level === "warning");
}
