import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";

import { CHECK_PLAN_VERSION, checkProfiles, listCheckTasks } from "./check-plan.mjs";

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");

export async function runCheckRunner(argv, io = process) {
  const options = parseCheckRunnerArgs(argv);
  if (options.help) {
    writeJson(io.stdout, helpPayload());
    return { exitCode: 0 };
  }

  const selected = selectTasks(options);
  const payload = {
    ok: true,
    command: "check-runner",
    version: CHECK_PLAN_VERSION,
    profile: options.profile,
    groups: options.groups,
    ids: options.ids,
    taskCount: selected.length,
    tasks: selected.map(serializeTask)
  };

  if (options.list || options.dryRun) {
    writeJson(io.stdout, { ...payload, mode: options.list ? "list" : "dry-run" });
    return { exitCode: 0 };
  }

  writeJson(io.stdout, { ...payload, mode: "run-start", tasks: undefined });
  const startedAt = Date.now();
  for (let index = 0; index < selected.length; index += 1) {
    const task = selected[index];
    const resolved = resolveTaskCommand(task);
    writeJson(io.stdout, {
      ok: true,
      event: "task-start",
      index: index + 1,
      total: selected.length,
      task: serializeTask(task),
      resolvedCommand: resolved.command
    });
    const exitCode = await runTask(resolved);
    if (exitCode !== 0) {
      writeJson(io.stdout, {
        ok: false,
        event: "task-failed",
        task: serializeTask(task),
        exitCode,
        elapsedMs: Date.now() - startedAt
      });
      return { exitCode };
    }
  }

  writeJson(io.stdout, {
    ok: true,
    event: "run-complete",
    profile: options.profile,
    taskCount: selected.length,
    elapsedMs: Date.now() - startedAt
  });
  return { exitCode: 0 };
}

export function parseCheckRunnerArgs(argv) {
  const options = { profile: "check", groups: [], ids: [], list: false, dryRun: false, help: false };
  for (let index = 0; index < argv.length; index += 1) {
    const arg = argv[index];
    const readValue = (name) => {
      const inline = arg.match(new RegExp(`^${name}=(.+)$`, "u"));
      if (inline) return inline[1];
      const value = argv[index + 1];
      if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`);
      index += 1;
      return value;
    };
    if (arg === "--help" || arg === "-h") options.help = true;
    else if (arg === "--profile" || arg.startsWith("--profile=")) options.profile = readValue("--profile");
    else if (arg === "--group" || arg.startsWith("--group=")) options.groups.push(...readValue("--group").split(",").filter(Boolean));
    else if (arg === "--id" || arg.startsWith("--id=")) options.ids.push(...readValue("--id").split(",").filter(Boolean));
    else if (arg === "--list") options.list = true;
    else if (arg === "--dry-run") options.dryRun = true;
    else throw new Error(`unknown check-runner argument: ${arg}`);
  }
  if (!checkProfiles[options.profile]) throw new Error(`unknown check profile: ${options.profile}`);
  return options;
}

export function selectTasks(options) {
  const groups = new Set(options.groups);
  const ids = new Set(options.ids);
  const profileTasks = listCheckTasks(options.profile);
  const availableGroups = new Set(profileTasks.map((task) => task.group));
  const availableIds = new Set(profileTasks.map((task) => task.id));
  for (const group of groups) {
    if (!availableGroups.has(group)) throw new Error(`unknown check group for profile ${options.profile}: ${group}`);
  }
  for (const id of ids) {
    if (!availableIds.has(id)) throw new Error(`unknown check task id for profile ${options.profile}: ${id}`);
  }
  const selected = profileTasks.filter((task) => {
    if (groups.size > 0 && !groups.has(task.group)) return false;
    if (ids.size > 0 && !ids.has(task.id)) return false;
    return true;
  });
  if (selected.length === 0) throw new Error(`check-runner selected no tasks for profile ${options.profile}`);
  return selected;
}

export function resolveTaskCommand(task) {
  const [bin, ...args] = task.command;
  if (!bin) throw new Error(`task has empty command: ${task.id}`);
  const cwd = task.cwd ? path.resolve(repoRoot, task.cwd) : repoRoot;
  return {
    task,
    command: bin === "bun" ? resolveBunCommand(cwd) : bin,
    args,
    cwd
  };
}

function runTask(task) {
  return new Promise((resolve) => {
    const child = spawn(task.command, task.args, {
      cwd: task.cwd,
      env: process.env,
      stdio: ["ignore", "inherit", "inherit"]
    });
    child.on("error", (error) => {
      process.stderr.write(JSON.stringify({ ok: false, event: "task-spawn-error", task: serializeTask(task.task), error: error.message }, null, 2) + "\n");
      resolve(127);
    });
    child.on("exit", (code, signal) => {
      if (signal) {
        process.stderr.write(JSON.stringify({ ok: false, event: "task-signal", task: serializeTask(task.task), signal }, null, 2) + "\n");
        resolve(128);
        return;
      }
      resolve(code ?? 0);
    });
  });
}

function resolveBunCommand(cwd) {
  for (const candidate of bunCandidates(cwd)) {
    if (!candidate) continue;
    if (!candidate.includes(path.sep)) return candidate;
    if (existsSync(candidate)) return candidate;
  }
  return "bun";
}

function bunCandidates(cwd) {
  const exe = process.platform === "win32" ? "bun.exe" : "bun";
  return [
    process.env.HWLAB_BUN_COMMAND,
    process.env.HWLAB_BUN_BINARY,
    process.env.BUN_BINARY,
    path.join(cwd, "node_modules", ".bin", exe),
    path.join(repoRoot, "node_modules", ".bin", exe),
    path.join(os.homedir(), ".bun", "bin", exe),
    "/root/.bun/bin/bun",
    "/usr/local/bin/bun",
    "bun"
  ].filter(Boolean);
}

function serializeTask(task) {
  return { id: task.id, group: task.group, cwd: task.cwd ?? ".", command: task.command };
}

function writeJson(stream, value) {
  stream.write(JSON.stringify(value, null, 2) + "\n");
}

function helpPayload() {
  return {
    ok: true,
    command: "check-runner",
    usage: [
      "node scripts/check-runner.mjs --profile check",
      "node scripts/check-runner.mjs --profile validate",
      "node scripts/check-runner.mjs --profile check --group cloud-api",
      "node scripts/check-runner.mjs --profile check --list",
      "node scripts/check-runner.mjs --profile check --dry-run"
    ],
    profiles: Object.keys(checkProfiles),
    groups: [...new Set(Object.values(checkProfiles).flat().map((task) => task.group))].sort()
  };
}
