/**
 * PATH discovery and binary resolution for service environments.
 *
 * When Zencode runs as a macOS launchd or Linux systemd service, it inherits
 * a restricted PATH that omits directories from version managers (nvm, fnm, volta,
 * mise, etc.) and user-local installs (~/.local/bin, ~/.cargo/bin). This module
 * captures the user's real shell PATH at runtime and provides binary resolution
 * that works regardless of how the server was started.
 */

import { execSync } from "node:child_process";
import { existsSync, readdirSync } from "node:fs";
import { homedir } from "node:os";
import { delimiter, isAbsolute, join } from "node:path";

const IS_WINDOWS = process.platform === "win32";

/**
 * Capture the user's full interactive shell PATH by spawning a login shell.
 * This picks up all version manager initializations (nvm, fnm, volta, mise, etc.).
 * Falls back to probing common directories if shell sourcing fails.
 */
export function captureUserShellPath(): string {
  // On Windows, there is no login-shell PATH enrichment concept — the system
  // PATH already includes user-installed directories. Skip to fallback probing.
  if (IS_WINDOWS) {
    return buildFallbackPath();
  }

  try {
    const shell = process.env.SHELL || "/bin/bash";
    const captured = execSync(
      `${shell} -lic 'echo "___PATH_START___$PATH___PATH_END___"'`,
      {
        encoding: "utf-8",
        timeout: 10_000,
        env: { HOME: homedir(), USER: process.env.USER, SHELL: shell },
      },
    );
    const match = captured.match(/___PATH_START___(.+)___PATH_END___/);
    if (match?.[1]) {
      return match[1];
    }
  } catch {
    // Shell sourcing failed (timeout, compinit prompt, etc.)
  }

  return buildFallbackPath();
}

/**
 * Build a PATH by probing common binary installation directories.
 * Used as fallback when shell-sourcing fails.
 */
export function buildFallbackPath(): string {
  const home = homedir();
  const candidates: string[] = [];

  if (IS_WINDOWS) {
    const appData = process.env.APPDATA || join(home, "AppData", "Roaming");
    const localAppData =
      process.env.LOCALAPPDATA || join(home, "AppData", "Local");
    const programFiles = process.env.ProgramFiles || "C:\\Program Files";
    const programFilesX86 =
      process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
    const systemRoot = process.env.SystemRoot || "C:\\Windows";

    candidates.push(
      // Node.js / npm global installs
      join(appData, "npm"),
      join(programFiles, "nodejs"),
      // Bun
      join(home, ".bun", "bin"),
      // Claude CLI / user-local installs
      join(home, ".local", "bin"),
      join(localAppData, "Programs", "claude"),
      // Cargo / Rust
      join(home, ".cargo", "bin"),
      // Volta (Node version manager)
      join(home, ".volta", "bin"),
      // mise (formerly rtx)
      join(localAppData, "mise", "shims"),
      // pyenv for Windows
      join(home, ".pyenv", "pyenv-win", "bin"),
      join(home, ".pyenv", "pyenv-win", "shims"),
      // Go
      join(home, "go", "bin"),
      join(programFiles, "Go", "bin"),
      // Deno
      join(home, ".deno", "bin"),
      // Git
      join(programFiles, "Git", "cmd"),
      join(programFiles, "Git", "bin"),
      join(programFilesX86, "Git", "cmd"),
      join(programFilesX86, "Git", "bin"),
      // Windows system paths
      join(systemRoot, "System32"),
      systemRoot,
    );

    // fnm on Windows — node versions in %APPDATA%\fnm
    const fnmNodeVersions = join(appData, "fnm", "node-versions");
    if (existsSync(fnmNodeVersions)) {
      try {
        for (const v of readdirSync(fnmNodeVersions)) {
          candidates.push(join(fnmNodeVersions, v, "installation"));
        }
      } catch {
        /* ignore */
      }
    }
  } else {
    candidates.push(
      // Standard system paths
      "/opt/homebrew/bin",
      "/opt/homebrew/sbin",
      "/usr/local/bin",
      "/usr/bin",
      "/bin",
      "/usr/sbin",
      "/sbin",
      // Bun
      join(home, ".bun", "bin"),
      // Claude CLI / user-local installs
      join(home, ".local", "bin"),
      // Cargo / Rust
      join(home, ".cargo", "bin"),
      // Volta (Node version manager)
      join(home, ".volta", "bin"),
      // mise (formerly rtx)
      join(home, ".local", "share", "mise", "shims"),
      // pyenv
      join(home, ".pyenv", "bin"),
      join(home, ".pyenv", "shims"),
      // Go
      join(home, "go", "bin"),
      "/usr/local/go/bin",
      // Deno
      join(home, ".deno", "bin"),
    );

    // fnm (Fast Node Manager) — versions stored in fnm multishell or XDG data
    const fnmDir = join(
      home,
      "Library",
      "Application Support",
      "fnm",
      "node-versions",
    );
    if (existsSync(fnmDir)) {
      try {
        for (const v of readdirSync(fnmDir)) {
          candidates.push(join(fnmDir, v, "installation", "bin"));
        }
      } catch {
        /* ignore */
      }
    }
  }

  // Probe nvm-managed node versions (cross-platform)
  const nvmDir = process.env.NVM_DIR || join(home, ".nvm");
  const nvmVersionsDir = join(nvmDir, "versions", "node");
  if (existsSync(nvmVersionsDir)) {
    try {
      for (const v of readdirSync(nvmVersionsDir)) {
        candidates.push(join(nvmVersionsDir, v, "bin"));
      }
    } catch {
      /* ignore */
    }
  }

  return [...new Set(candidates.filter((dir) => existsSync(dir)))].join(
    delimiter,
  );
}

// ─── Enriched PATH (cached) ───────────────────────────────────────────────────

let _cachedPath: string | null = null;

/**
 * Returns an enriched PATH that merges the user's shell PATH (or probed common
 * directories) with the current process PATH. Deduplicates entries.
 * Result is cached after the first call.
 */
export function getEnrichedPath(): string {
  if (_cachedPath) return _cachedPath;

  const currentPath = process.env.PATH || "";
  const userPath = captureUserShellPath();

  // Merge: user shell PATH first (takes precedence), then current process PATH
  const allDirs = [
    ...userPath.split(delimiter),
    ...currentPath.split(delimiter),
  ];
  const seen = new Set<string>();
  const deduped: string[] = [];
  for (const dir of allDirs) {
    if (dir && !seen.has(dir)) {
      seen.add(dir);
      deduped.push(dir);
    }
  }

  _cachedPath = deduped.join(delimiter);
  return _cachedPath;
}

/** Reset the cached PATH (for testing). */
export function _resetPathCache(): void {
  _cachedPath = null;
}

// ─── Binary resolution ────────────────────────────────────────────────────────

/**
 * Resolve a binary name to an absolute path using the enriched PATH.
 * Returns null if the binary is not found anywhere.
 * @deprecated Use resolveBinaryAsync() in request-handling code paths.
 */
export function resolveBinary(name: string): string | null {
  if (isAbsolute(name)) {
    return existsSync(name) ? name : null;
  }

  const enrichedPath = getEnrichedPath();
  try {
    const sanitized = IS_WINDOWS
      ? name.replace(/[^a-zA-Z0-9._@/\\-]/g, "")
      : name.replace(/[^a-zA-Z0-9._@/-]/g, "");
    const lookupCmd = IS_WINDOWS ? `where ${sanitized}` : `which ${sanitized}`;

    const resolved = execSync(lookupCmd, {
      encoding: "utf-8",
      timeout: 5_000,
      env: { ...process.env, PATH: enrichedPath },
    }).trim();

    // `where` on Windows may return multiple lines; take the first result
    const firstResult = resolved.split(/\r?\n/)[0]?.trim();
    return firstResult || null;
  } catch {
    return null;
  }
}

/**
 * Async version of resolveBinary — uses Bun.spawn to avoid blocking the event loop.
 * Preferred in request-handling and session-creation code paths.
 */
export async function resolveBinaryAsync(name: string): Promise<string | null> {
  if (isAbsolute(name)) {
    return existsSync(name) ? name : null;
  }

  const enrichedPath = getEnrichedPath();
  try {
    const sanitized = IS_WINDOWS
      ? name.replace(/[^a-zA-Z0-9._@/\\-]/g, "")
      : name.replace(/[^a-zA-Z0-9._@/-]/g, "");
    const lookupCmd = IS_WINDOWS ? "where" : "which";

    const proc = Bun.spawn([lookupCmd, sanitized], {
      stdout: "pipe",
      stderr: "pipe",
      env: { ...process.env, PATH: enrichedPath },
    });
    const timeout = setTimeout(() => proc.kill(), 5_000);
    const exitCode = await proc.exited;
    clearTimeout(timeout);

    if (exitCode !== 0) return null;

    const resolved = (await new Response(proc.stdout).text()).trim();
    // `where` on Windows may return multiple lines; take the first result
    const firstResult = resolved.split(/\r?\n/)[0]?.trim();
    return firstResult || null;
  } catch {
    return null;
  }
}

/**
 * Returns a PATH string suitable for embedding in service definitions
 * (plist/systemd unit). Captures the user's shell PATH at install time.
 */
export function getServicePath(): string {
  return getEnrichedPath();
}
