import fs from "node:fs";
import { execFileSync } from "node:child_process";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";

export const playwrightDependencyCommand = "npm install";
export const playwrightDependencySummary =
  "Install repository dependencies before running browser smokes: npm install provides the declared playwright package; the Code Queue image may also provide the same package globally.";

const chromiumExecutableEnvKeys = Object.freeze([
  "HWLAB_PLAYWRIGHT_CHROMIUM",
  "PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH",
  "CHROMIUM_PATH"
]);

const chromiumExecutableCandidates = Object.freeze([
  "/snap/bin/chromium",
  "/usr/bin/chromium-browser",
  "/usr/bin/chromium",
  "/usr/bin/google-chrome",
  "/usr/bin/google-chrome-stable"
]);

export async function importPlaywright() {
  try {
    return normalizePlaywrightModule(await import("playwright"));
  } catch (localError) {
    const globalRoot = npmGlobalRoot();
    if (globalRoot) {
      try {
        return normalizePlaywrightModule(await import(pathToFileURL(path.join(globalRoot, "playwright/index.js")).href));
      } catch {
        // Keep the local resolution error so remediation points at this repository's package.json.
      }
    }
    throw localError;
  }
}

export async function launchChromium(chromium, options = {}) {
  const plan = chromiumLaunchPlan();
  const launchOptions = {
    ...options,
    headless: options.headless ?? true,
    args: ["--no-sandbox", ...(options.args ?? [])],
    ...(plan.executablePath ? { executablePath: plan.executablePath } : {})
  };
  try {
    const browser = await chromium.launch(launchOptions);
    browser.hwlabLaunchPlan = { ...plan, failureCode: null };
    return browser;
  } catch (error) {
    error.hwlabBrowserLaunch = {
      ...plan,
      failureCode: classifyBrowserLaunchFailure(error),
      remediation: browserLaunchRemediation(plan)
    };
    throw error;
  }
}

export async function browserDoctor() {
  let playwright;
  try {
    playwright = await importPlaywright();
  } catch (error) {
    return {
      status: "fail",
      failureCode: "playwright-import-failed",
      playwrightPackageSource: "missing",
      platform: browserRuntimePlatform(),
      browser: chromiumLaunchPlan(),
      remediation: [playwrightDependencyCommand],
      error: error instanceof Error ? error.message : String(error)
    };
  }

  const managedBrowser = playwrightManagedBrowserPlan(playwright.chromium);
  const installCheck = managedBrowser.cacheStatus === "missing" ? playwrightInstallDryRun() : null;
  const browser = await launchChromium(playwright.chromium).catch((error) => ({ error }));
  if (browser?.error) {
    const plan = browser.error.hwlabBrowserLaunch ?? chromiumLaunchPlan();
    return {
      status: "fail",
      failureCode: plan.failureCode ?? classifyBrowserLaunchFailure(browser.error),
      playwrightPackageSource: "playwright",
      platform: browserRuntimePlatform(),
      managedBrowser,
      installCheck,
      browser: plan,
      remediation: browserLaunchRemediation(plan),
      error: browser.error instanceof Error ? browser.error.message : String(browser.error)
    };
  }

  try {
    const page = await browser.newPage();
    await page.goto("data:text/html,<title>hwlab-browser-doctor</title>", { waitUntil: "domcontentloaded", timeout: 5000 });
    await page.close();
  } finally {
    await browser.close();
  }

  return {
    status: "pass",
    failureCode: null,
    playwrightPackageSource: "playwright",
    platform: browserRuntimePlatform(),
    managedBrowser,
    installCheck,
    browser: browser.hwlabLaunchPlan ?? chromiumLaunchPlan(),
    remediation: browserLaunchRemediation(browser.hwlabLaunchPlan ?? chromiumLaunchPlan(), { managedBrowser, installCheck })
  };
}

export function playwrightManagedBrowserPlan(chromium) {
  const executablePath = typeof chromium?.executablePath === "function" ? chromium.executablePath() : "";
  const exists = executablePath ? fs.existsSync(executablePath) : false;
  return {
    browserSource: "playwright-cache",
    executablePath,
    exists,
    cacheStatus: exists ? "available" : "missing"
  };
}

export function playwrightInstallDryRun() {
  const command = ["npx", "playwright", "install", "--dry-run", "chromium"];
  try {
    const output = execFileSync(command[0], command.slice(1), {
      encoding: "utf8",
      stdio: ["ignore", "pipe", "pipe"],
      timeout: 15000
    }).trim();
    return {
      status: "dry-run-available",
      command: command.join(" "),
      note: "Dry-run reports the Playwright download plan only; this doctor uses system Chromium fallback when the managed cache is missing.",
      output: boundedText(output)
    };
  } catch (error) {
    const output = `${error?.stdout ?? ""}${error?.stderr ?? ""}`.trim();
    const message = error instanceof Error ? error.message : String(error);
    return {
      status: /does not support|unsupported platform|unsupported/iu.test(`${output}\n${message}`) ? "unsupported" : "unknown",
      command: command.join(" "),
      output: boundedText(output || message),
      exitCode: typeof error?.status === "number" ? error.status : null
    };
  }
}

export function browserRuntimePlatform() {
  return {
    platform: process.platform,
    arch: process.arch,
    osRelease: readOsReleaseSummary(),
    glibcVersionRuntime: process.report?.getReport?.().header?.glibcVersionRuntime ?? null
  };
}

export function chromiumLaunchPlan(env = process.env) {
  const candidatePaths = chromiumExecutableCandidates.filter((candidate) => fs.existsSync(candidate));
  for (const key of chromiumExecutableEnvKeys) {
    const value = env[key];
    if (value && fs.existsSync(value)) {
      return {
        browserSource: "env",
        executablePath: value,
        executablePathEnvKey: key,
        fallbackUsed: key !== "HWLAB_PLAYWRIGHT_CHROMIUM",
        candidatePaths
      };
    }
  }
  for (const candidate of chromiumExecutableCandidates) {
    if (fs.existsSync(candidate)) {
      return {
        browserSource: "system-chromium",
        executablePath: candidate,
        executablePathEnvKey: null,
        fallbackUsed: true,
        candidatePaths
      };
    }
  }
  return {
    browserSource: "playwright-cache",
    executablePath: "",
    executablePathEnvKey: null,
    fallbackUsed: false,
    candidatePaths
  };
}

export function classifyBrowserLaunchFailure(error) {
  const message = error instanceof Error ? error.message : String(error);
  if (/does not support chromium on/iu.test(message)) return "browser-install-unsupported-platform";
  if (/Executable doesn't exist|download new browsers|playwright install/iu.test(message)) return "browser-cache-missing";
  if (/spawn|ENOENT|permission denied|EACCES/iu.test(message)) return "browser-executable-unavailable";
  return "browser-launch-failed";
}

export function browserLaunchRemediation(plan = chromiumLaunchPlan(), context = {}) {
  const remediation = [];
  if (plan.executablePath) {
    remediation.push(`using ${plan.executablePath}`);
    if (context.managedBrowser?.cacheStatus === "missing") {
      remediation.push("Playwright-managed browser cache is missing; the repo launcher selected system Chromium fallback.");
    }
    if (context.installCheck?.status) {
      remediation.push(`Playwright install dry-run status=${context.installCheck.status}; keep using the repo launcher/system Chromium fallback when managed browser install is unavailable or unnecessary.`);
    }
    return remediation;
  }
  remediation.push(
    "Set HWLAB_PLAYWRIGHT_CHROMIUM=/snap/bin/chromium when system Chromium exists.",
    context.installCheck?.status === "unsupported"
      ? "Playwright install is unsupported on this platform; install a system Chromium package or use a runner image that already contains Chromium."
      : "Install Playwright browsers with npx playwright install when no system Chromium is available."
  );
  return remediation;
}

function normalizePlaywrightModule(module) {
  if (module?.chromium) return module;
  if (module?.default?.chromium) return module.default;
  return module;
}

function npmGlobalRoot() {
  try {
    return execFileSync("npm", ["root", "-g"], {
      encoding: "utf8",
      stdio: ["ignore", "pipe", "ignore"],
      timeout: 5000
    }).trim();
  } catch {
    return "";
  }
}

function readOsReleaseSummary() {
  try {
    const fields = Object.fromEntries(fs.readFileSync("/etc/os-release", "utf8")
      .split(/\r?\n/u)
      .map((line) => line.match(/^([A-Z_]+)=(.*)$/u))
      .filter(Boolean)
      .map((match) => [match[1], match[2].replace(/^"|"$/gu, "")]));
    return {
      name: fields.NAME ?? os.type(),
      versionId: fields.VERSION_ID ?? "",
      prettyName: fields.PRETTY_NAME ?? "",
      codename: fields.VERSION_CODENAME ?? fields.UBUNTU_CODENAME ?? ""
    };
  } catch {
    return { name: os.type(), versionId: os.release(), prettyName: os.platform(), codename: "" };
  }
}

function boundedText(value, maxChars = 2000) {
  const text = String(value ?? "");
  return text.length > maxChars ? `${text.slice(0, maxChars)}...<truncated ${text.length - maxChars} chars>` : text;
}
