import { statSync } from "node:fs";
import { execFileSync } from "node:child_process";
import { delimiter, isAbsolute, join } from "node:path";

export type PlatformName = typeof process.platform;
type PathEnv = Record<string, string | undefined>;

export type CommandSpec = {
  command: string;
  args: string[];
};

export function platformCommand(
  command: string,
  args: readonly string[],
  platform: PlatformName = process.platform,
): CommandSpec {
  if (platform !== "win32") return { command, args: [...args] };
  return { command: "cmd.exe", args: ["/d", "/s", "/c", command, ...args] };
}

export function npmCommand(platform: PlatformName = process.platform): string {
  return platform === "win32" ? "npm.cmd" : "npm";
}

export function npmSpawnCommand(
  args: readonly string[],
  platform: PlatformName = process.platform,
): CommandSpec {
  return platformCommand(npmCommand(platform), args, platform);
}

function pathEntries(envPath: string | undefined): string[] {
  if (envPath === undefined || envPath.length === 0) return [];
  return envPath.split(delimiter).filter((entry) => entry.length > 0);
}

function windowsExecutableExtensions(envPathExt: string | undefined): string[] {
  const defaults = [".COM", ".EXE", ".BAT", ".CMD"];
  if (envPathExt === undefined || envPathExt.trim().length === 0) return defaults;
  const parsed = envPathExt
    .split(";")
    .map((entry) => entry.trim())
    .filter((entry) => entry.length > 0);
  return parsed.length > 0 ? parsed : defaults;
}

function isExecutableCandidate(path: string, platform: PlatformName): boolean {
  try {
    const stat = statSync(path);
    if (!stat.isFile()) return false;
    if (platform === "win32" || process.platform === "win32") return true;
    return (stat.mode & 0o111) !== 0;
  } catch {
    return false;
  }
}

function executableCandidates(bin: string, platform: PlatformName, env: PathEnv): string[] {
  if (isAbsolute(bin)) return [bin];
  const entries = pathEntries(env["PATH"] ?? env["Path"] ?? env["path"]);
  if (platform !== "win32") return entries.map((entry) => join(entry, bin));

  const hasExtension = /\.[^\\/]+$/.test(bin);
  const extensions = hasExtension ? [""] : windowsExecutableExtensions(env["PATHEXT"]);
  const candidates: string[] = [];
  for (const entry of entries) {
    for (const extension of extensions) {
      candidates.push(join(entry, `${bin}${extension.toLowerCase()}`));
      candidates.push(join(entry, `${bin}${extension.toUpperCase()}`));
    }
  }
  return candidates;
}

export function resolveExecutableOnPath(
  bin: string,
  platform: PlatformName = process.platform,
  env: PathEnv = process.env,
): string | null {
  if (bin.trim().length === 0) return null;
  for (const candidate of executableCandidates(bin, platform, env)) {
    if (isExecutableCandidate(candidate, platform)) return candidate;
  }
  return null;
}

export function openUrlCommand(
  targetUrl: string,
  platform: PlatformName = process.platform,
): CommandSpec | null {
  switch (platform) {
    case "darwin":
      return { command: "open", args: [targetUrl] };
    case "linux":
    case "freebsd":
    case "openbsd":
    case "netbsd":
      return { command: "xdg-open", args: [targetUrl] };
    case "win32":
      return { command: "cmd.exe", args: ["/d", "/s", "/c", "start", "", targetUrl] };
    case "aix":
    case "android":
    case "cygwin":
    case "haiku":
    case "sunos":
      return null;
  }

  return null;
}

function parseWindowsNetstatPids(output: string, targetPort: number): number[] {
  const pids = new Set<number>();
  const targetToken = `:${targetPort}`;
  for (const line of output.split(/\r?\n/)) {
    const parts = line.trim().split(/\s+/);
    if (parts.length < 5) continue;
    const localAddress = parts[1] ?? "";
    const pidStr = parts[4] ?? "";
    if (!localAddress.endsWith(targetToken)) continue;
    const pid = Number.parseInt(pidStr, 10);
    if (Number.isInteger(pid) && pid > 0) pids.add(pid);
  }
  return [...pids];
}

function parseUnixLsofPids(output: string): number[] {
  const pids = new Set<number>();
  for (const line of output.split(/\r?\n/)) {
    const pid = Number.parseInt(line.trim(), 10);
    if (Number.isInteger(pid) && pid > 0) pids.add(pid);
  }
  return [...pids];
}

function parseUnixSsPids(output: string): number[] {
  const pids = new Set<number>();
  for (const match of output.matchAll(/pid=(\d+)/g)) {
    const pid = Number.parseInt(match[1] ?? "", 10);
    if (Number.isInteger(pid) && pid > 0) pids.add(pid);
  }
  return [...pids];
}

export function findPidsByPort(
  targetPort: number,
  platform: PlatformName = process.platform,
): number[] {
  if (!Number.isInteger(targetPort) || targetPort <= 0 || targetPort > 65_535) return [];
  try {
    if (platform === "win32") {
      const output = execFileSync("netstat.exe", ["-ano", "-p", "TCP"], {
        encoding: "utf8",
        timeout: 5000,
        windowsHide: true,
      });
      return parseWindowsNetstatPids(output, targetPort);
    }

    try {
      const output = execFileSync("lsof", [`-ti:${String(targetPort)}`], {
        encoding: "utf8",
        timeout: 5000,
        stdio: ["ignore", "pipe", "ignore"],
      });
      const pids = parseUnixLsofPids(output);
      if (pids.length > 0) return pids;
    } catch {
      // Fall back to ss below.
    }

    const output = execFileSync("ss", ["-tanp"], {
      encoding: "utf8",
      timeout: 5000,
      stdio: ["ignore", "pipe", "ignore"],
    });
    return parseUnixSsPids(
      output
        .split(/\r?\n/)
        .filter((line) => line.includes(`:${String(targetPort)}`))
        .join("\n"),
    );
  } catch {
    return [];
  }
}

export function killPid(pid: number, platform: PlatformName = process.platform): boolean {
  if (!Number.isInteger(pid) || pid <= 0) return false;
  try {
    if (platform === "win32") {
      execFileSync("taskkill.exe", ["/PID", String(pid), "/F"], {
        encoding: "utf8",
        timeout: 5000,
        windowsHide: true,
        stdio: ["ignore", "pipe", "ignore"],
      });
      return true;
    }
    process.kill(pid, "SIGKILL");
    return true;
  } catch {
    return false;
  }
}

export const platformCommandParsersForTests = {
  parseUnixLsofPids,
  parseUnixSsPids,
  parseWindowsNetstatPids,
};
