import { spawn } from "node:child_process";
import { getHomeDirectory } from "./home-directory";
import { findBashPath, findZshPath } from "./shell-path";

export interface CommandResult {
  exitCode: number;
  stdout?: string;
  stderr?: string;
}

const DEFAULT_HOOK_TIMEOUT_MS = 30_000;
const SIGKILL_GRACE_MS = 5_000;

export interface ExecuteHookOptions {
  forceZsh?: boolean;
  zshPath?: string;
  /** Timeout in milliseconds. Process is killed after this. Default: 30000 */
  timeoutMs?: number;
}

export async function executeHookCommand(
  command: string,
  stdin: string,
  cwd: string,
  options?: ExecuteHookOptions,
): Promise<CommandResult> {
  const home = getHomeDirectory();
  const timeoutMs = options?.timeoutMs ?? DEFAULT_HOOK_TIMEOUT_MS;

  const expandedCommand = command
    .replace(/^~(?=\/|$)/g, home)
    .replace(/\s~(?=\/)/g, ` ${home}`)
    .replace(/\$CLAUDE_PROJECT_DIR/g, cwd)
    .replace(/\$\{CLAUDE_PROJECT_DIR\}/g, cwd);

  let finalCommand = expandedCommand;

  if (options?.forceZsh) {
    const zshPath = findZshPath(options.zshPath);
    const escapedCommand = expandedCommand.replace(/'/g, "'\\''");
    if (zshPath) {
      finalCommand = `${zshPath} -lc '${escapedCommand}'`;
    } else {
      const bashPath = findBashPath();
      if (bashPath) {
        finalCommand = `${bashPath} -lc '${escapedCommand}'`;
      }
    }
  }

  return new Promise(resolve => {
    let settled = false;
    let killTimer: ReturnType<typeof setTimeout> | null = null;

    const isWin32 = process.platform === "win32";
    const proc = spawn(finalCommand, {
      cwd,
      shell: true,
      detached: !isWin32,
      env: { ...process.env, HOME: home, CLAUDE_PROJECT_DIR: cwd },
    });

    let stdout = "";
    let stderr = "";

    proc.stdout?.on("data", (data: Buffer) => {
      stdout += data.toString();
    });

    proc.stderr?.on("data", (data: Buffer) => {
      stderr += data.toString();
    });

    proc.stdin?.on("error", () => {});
    proc.stdin?.write(stdin);
    proc.stdin?.end();

    const settle = (result: CommandResult) => {
      if (settled) return;
      settled = true;
      if (killTimer) clearTimeout(killTimer);
      if (timeoutTimer) clearTimeout(timeoutTimer);
      resolve(result);
    };

    proc.on("close", code => {
      settle({
        exitCode: code ?? 1,
        stdout: stdout.trim(),
        stderr: stderr.trim(),
      });
    });

    proc.on("error", err => {
      settle({ exitCode: 1, stderr: err.message });
    });

    const killProcessGroup = (signal: NodeJS.Signals) => {
      try {
        if (!isWin32 && proc.pid) {
          try {
            process.kill(-proc.pid, signal);
          } catch {
            proc.kill(signal);
          }
        } else {
          proc.kill(signal);
        }
      } catch {}
    };

    const timeoutTimer = setTimeout(() => {
      if (settled) return;
      // Kill entire process group to avoid orphaned children
      killProcessGroup("SIGTERM");
      killTimer = setTimeout(() => {
        if (settled) return;
        killProcessGroup("SIGKILL");
      }, SIGKILL_GRACE_MS);
      // Append timeout notice to stderr
      stderr += `\nHook command timed out after ${timeoutMs}ms`;
    }, timeoutMs);

    // Don't let the timeout timer keep the process alive
    if (timeoutTimer && typeof timeoutTimer === "object" && "unref" in timeoutTimer) {
      timeoutTimer.unref();
    }
  });
}
