/**
 * Claude CLI wrapper script.
 *
 * This script is compiled to CJS by Vite and runs in Node.js when the user
 * invokes `claude` from a CodeHydra workspace terminal.
 *
 * It:
 * 1. Reads environment variables for configuration
 * 2. Finds the system-installed claude binary
 * 3. Reads and deletes initial prompt file if present
 * 4. Spawns claude with CodeHydra config flags injected
 *
 * Environment variables (set by sidekick extension):
 * - _CH_CLAUDE_SETTINGS: Path to codehydra-hooks.json
 * - _CH_CLAUDE_MCP_CONFIG: Path to codehydra-mcp.json
 * - _CH_BRIDGE_PORT: Bridge server port (for hook notifications)
 * - _CH_MCP_PORT: Main MCP server port
 * - _CH_WORKSPACE_PATH: Workspace path for MCP header
 * - _CH_INITIAL_PROMPT_FILE: Path to initial prompt JSON file (optional)
 * - _CH_CLAUDE_NO_SESSION_MARKER_PATH: Path to no-session marker (optional, new workspaces only)
 */

import { spawnSync, execSync } from "node:child_process";
import { readFileSync, unlinkSync, rmdirSync, existsSync } from "node:fs";
import { dirname } from "node:path";
import { request } from "node:http";

/**
 * Config read from initial-prompt.json file.
 * Model is stored as just the modelID string (not full PromptModel).
 */
export interface InitialPromptConfig {
  readonly prompt: string;
  readonly model?: string;
  readonly agent?: string;
}

/**
 * Read initial prompt config from file and delete it.
 * Returns undefined if no initial prompt file is set or if reading fails.
 *
 * Uses synchronous Node.js APIs to match wrapper's sync execution model.
 * Deletes the file and temp directory before returning to ensure one-time use.
 */
function getInitialPromptConfig(): InitialPromptConfig | undefined {
  const filePath = process.env._CH_INITIAL_PROMPT_FILE;

  // No initial prompt file configured
  if (!filePath) {
    return undefined;
  }

  try {
    // Read the file
    const content = readFileSync(filePath, "utf-8");

    // Delete the file first (before parsing, to ensure one-time use)
    try {
      unlinkSync(filePath);
    } catch {
      // Ignore deletion errors - file might already be gone
    }

    // Delete the parent temp directory
    try {
      rmdirSync(dirname(filePath));
    } catch {
      // Ignore - directory might not be empty or already gone
    }

    // Parse JSON
    const config = JSON.parse(content) as InitialPromptConfig;
    return config;
  } catch (error) {
    // File not found is expected on restart (consumed on first launch)
    if (
      error instanceof Error &&
      "code" in error &&
      (error as NodeJS.ErrnoException).code === "ENOENT"
    ) {
      return undefined;
    }

    // Log warning for unexpected errors but don't fail
    console.warn(
      `Warning: Failed to read initial prompt file: ${error instanceof Error ? error.message : String(error)}`
    );

    // Try to clean up anyway
    try {
      unlinkSync(filePath);
      rmdirSync(dirname(filePath));
    } catch {
      // Ignore cleanup errors
    }

    return undefined;
  }
}

/**
 * Build permission flags based on agent type.
 * - "plan" agent: read-only mode with --permissions-mode plan
 * - All others (including "implement" and no agent): full permissions
 *
 * @param agent - The agent name from initial prompt config
 * @returns Array of CLI permission flags
 */
function buildPermissionArgs(agent?: string): string[] {
  if (agent === "plan") {
    return ["--allow-dangerously-skip-permissions", "--permission-mode", "plan"];
  }
  return ["--dangerously-skip-permissions"];
}

/**
 * Build CLI arguments from initial prompt config.
 * Returns array of arguments to prepend to claude command.
 *
 * @param config - The initial prompt configuration
 * @returns Array of CLI arguments (prompt, --model, --agent flags as needed)
 */
function buildInitialPromptArgs(config: InitialPromptConfig): string[] {
  const args: string[] = [];
  if (config.prompt) {
    args.push(config.prompt);
  }

  if (config.model !== undefined) {
    args.push("--model", config.model);
  }

  if (config.agent !== undefined) {
    args.push("--agent", config.agent);
  }

  return args;
}

// Exit codes
const EXIT_ENV_ERROR = 1;
const EXIT_SPAWN_FAILED = 2;
const EXIT_NOT_FOUND = 3;

/**
 * Result of spawning Claude CLI.
 */
export interface SpawnResult {
  exitCode: number | null;
  error: Error | undefined;
}

/**
 * Options for running Claude.
 */
export interface RunClaudeOptions {
  /** Skip the automatic --continue attempt (new workspace with no prior session) */
  skipContinue?: boolean;
  /** Spawn through a shell (required on Windows when invoking .cmd/.bat shims) */
  useShell?: boolean;
}

/**
 * Result type returned by spawnSync for testing purposes.
 */
interface SpawnSyncResult {
  status: number | null;
  error: Error | undefined;
}

/**
 * Dependencies for runClaude that can be injected for testing.
 */
export interface RunClaudeDeps {
  spawnSync: (
    command: string,
    args: string[],
    options: { stdio: "inherit"; shell: boolean }
  ) => SpawnSyncResult;
}

/**
 * Quote an argument for cmd.exe. Wraps in double quotes and doubles any
 * embedded quotes so cmd.exe parses it as a single token. Used together
 * with windowsVerbatimArguments to bypass Node's arg mangling.
 */
function quoteForCmd(arg: string): string {
  return `"${arg.replace(/"/g, '""')}"`;
}

/**
 * Default dependencies using real implementations.
 *
 * Node's `shell: true` joins the file + args with single spaces and wraps the
 * whole line in one outer pair of quotes — it does NOT quote individual args.
 * Args containing spaces (like a prompt "hello world") therefore get split
 * by cmd.exe's tokenizer. Pre-quote each arg ourselves so the inner tokens
 * survive cmd.exe's parse after /S strips the outer pair.
 */
const defaultDeps: RunClaudeDeps = {
  spawnSync: (command, args, options) => {
    if (options.shell && process.platform === "win32") {
      const quotedCommand = quoteForCmd(command);
      const quotedArgs = args.map(quoteForCmd);
      const result = spawnSync(quotedCommand, quotedArgs, {
        stdio: "inherit",
        shell: true,
      });
      return { status: result.status, error: result.error };
    }
    const result = spawnSync(command, args, options);
    return { status: result.status, error: result.error };
  },
};

/**
 * Check if user args contain session resume flags.
 * Returns true if user explicitly passed --continue, -c, or --resume.
 */
function hasUserResumeFlag(args: string[]): boolean {
  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg === "--continue" || arg === "-c" || arg === "--resume") {
      return true;
    }
  }
  return false;
}

/**
 * Run Claude CLI with automatic session resume.
 *
 * If user hasn't passed explicit resume flags (--continue, -c, --resume),
 * first attempts to resume with --continue. If that fails (exit code non-zero),
 * retries without --continue.
 *
 * @param claudeBinary - The claude binary name (resolved via PATH)
 * @param baseArgs - Arguments to pass to Claude
 * @param options - Spawn options
 * @param deps - Injectable dependencies for testing
 * @returns SpawnResult with exit code and optional error
 */
export function runClaude(
  claudeBinary: string,
  baseArgs: string[],
  options: RunClaudeOptions,
  deps: RunClaudeDeps = defaultDeps
): SpawnResult {
  const shell = options.useShell ?? false;

  // Check if user already passed resume flags or skipContinue is set
  if (hasUserResumeFlag(baseArgs) || options.skipContinue) {
    const result = deps.spawnSync(claudeBinary, baseArgs, {
      stdio: "inherit",
      shell,
    });
    return {
      exitCode: result.status,
      error: result.error,
    };
  }

  // First attempt: try with --continue to resume session
  const continueArgs = ["--continue", ...baseArgs];
  const firstResult = deps.spawnSync(claudeBinary, continueArgs, {
    stdio: "inherit",
    shell,
  });

  // If successful, return
  if (firstResult.status === 0) {
    return {
      exitCode: 0,
      error: firstResult.error,
    };
  }

  // Retry without --continue
  const retryResult = deps.spawnSync(claudeBinary, baseArgs, {
    stdio: "inherit",
    shell,
  });

  return {
    exitCode: retryResult.status,
    error: retryResult.error,
  };
}

/**
 * Consume the no-session marker file if present.
 * Returns true when the marker exists (new workspace, no prior session),
 * meaning --continue should be skipped. Deletes the marker so subsequent
 * runs will attempt --continue.
 */
function consumeNoSessionMarker(): boolean {
  const markerPath = process.env._CH_CLAUDE_NO_SESSION_MARKER_PATH;
  if (!markerPath) return false;
  if (!existsSync(markerPath)) return false;
  try {
    unlinkSync(markerPath);
  } catch {
    // Ignore deletion errors
  }
  return true;
}

/**
 * Get user arguments from process.argv.
 * Auto-detects the start of user flags to handle both terminal and panel modes.
 * In terminal mode: argv = [node, script, ...flags]
 * In panel mode: argv = [node, script, command, ...flags]
 */
function getUserArgs(): string[] {
  // Find the first argument that looks like a flag (starts with --)
  const firstFlagIndex = process.argv.findIndex((arg, i) => i >= 2 && arg.startsWith("--"));

  if (firstFlagIndex === -1) {
    // No flags found, return empty
    return [];
  }

  return process.argv.slice(firstFlagIndex);
}

/**
 * Find the system-installed claude binary.
 *
 * On Windows we probe explicit extensions because npm-installed claude only
 * drops a `claude.cmd` shim (no `.exe`), and `.cmd` files require `shell: true`
 * for spawnSync (Node refuses direct execution since CVE-2024-27980).
 *
 * Returns the command name and whether spawn must go through a shell, or null
 * when no working binary is found on PATH.
 */
function findSystemClaude(): { command: string; useShell: boolean } | null {
  const candidates =
    process.platform === "win32"
      ? [
          { command: "claude.exe", useShell: false },
          { command: "claude.cmd", useShell: true },
        ]
      : [{ command: "claude", useShell: false }];

  for (const candidate of candidates) {
    try {
      execSync(`${candidate.command} --version`, {
        encoding: "utf8",
        stdio: ["pipe", "pipe", "pipe"],
      });
      return candidate;
    } catch {
      // Try next candidate
    }
  }
  return null;
}

/**
 * Send a hook notification to the bridge server.
 * Waits for request to complete (or timeout) before returning.
 * Silent on error - fallback to 10-second timeout works.
 *
 * @param hookName - Name of the hook (WrapperStart or WrapperEnd)
 */
async function notifyHook(hookName: "WrapperStart" | "WrapperEnd"): Promise<void> {
  const bridgePort = process.env._CH_BRIDGE_PORT;
  const workspacePath = process.env._CH_WORKSPACE_PATH;

  // Not in CodeHydra context
  if (!bridgePort || !workspacePath) {
    return;
  }

  const payload = JSON.stringify({ workspacePath });

  return new Promise((resolve) => {
    const req = request(
      {
        hostname: "127.0.0.1",
        port: parseInt(bridgePort, 10),
        path: `/hook/${hookName}`,
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "Content-Length": Buffer.byteLength(payload),
        },
        timeout: 2000, // 2 second timeout
      },
      () => {
        resolve();
      }
    );

    req.on("error", () => {
      resolve(); // Silent failure
    });

    req.on("timeout", () => {
      req.destroy();
      resolve();
    });

    req.write(payload);
    req.end();
  });
}

/**
 * Signals to catch so the wrapper survives long enough to send WrapperEnd.
 *
 * - SIGHUP: PTY hangup when the terminal tab is closed.
 * - SIGTERM: Graceful termination from process managers (code-server may
 *   send this after SIGHUP if the process hasn't exited yet).
 *
 * Without these handlers Node.js terminates immediately on these signals,
 * preventing the async notifyHook("WrapperEnd") call from executing after
 * spawnSync returns.
 */
const WRAPPER_SIGNALS: NodeJS.Signals[] = ["SIGHUP", "SIGTERM"];

/**
 * Install no-op signal handlers to prevent default process termination.
 *
 * Must be called before spawnSync so the wrapper survives terminal-close
 * signals and can still send the WrapperEnd hook notification.
 * The process exits via the explicit process.exit() in main().
 *
 * @param proc - Process-like object for testability (defaults to global process)
 */
function installSignalHandlers(proc: Pick<NodeJS.Process, "on"> = process): void {
  const noop = (): void => {};
  for (const signal of WRAPPER_SIGNALS) {
    proc.on(signal, noop);
  }
}

/**
 * Main entry point for the wrapper script.
 */
async function main(): Promise<never> {
  // 1. Validate required environment variables
  const settingsPath = process.env._CH_CLAUDE_SETTINGS;
  const mcpConfigPath = process.env._CH_CLAUDE_MCP_CONFIG;

  if (!settingsPath || !mcpConfigPath) {
    console.error("Error: CodeHydra Claude configuration not set.");
    console.error("Make sure you're in a CodeHydra workspace terminal.");
    process.exit(EXIT_ENV_ERROR);
  }

  // 2. Find the system claude binary
  const claudeBinary = findSystemClaude();
  if (!claudeBinary) {
    console.error("Error: Claude CLI not found.");
    console.error("");
    console.error("Please install Claude CLI:");
    console.error("  npm install -g @anthropic-ai/claude");
    console.error("  or see: https://docs.anthropic.com/claude/installation");
    process.exit(EXIT_NOT_FOUND);
  }

  // 3. Check for initial prompt (read and delete file before Claude starts)
  const initialPromptConfig = getInitialPromptConfig();
  const initialPromptArgs = initialPromptConfig ? buildInitialPromptArgs(initialPromptConfig) : [];

  // 4. Build arguments
  // --ide: Enable IDE-specific features
  // --settings: Our hooks config (merges with user's)
  // --mcp-config: Our MCP config (merges with user's)
  // Initial prompt args come first (prompt as positional, then --model/--agent)
  // User args can override these if they come after
  const args = [
    ...initialPromptArgs,
    ...buildPermissionArgs(initialPromptConfig?.agent),
    "--ide",
    "--settings",
    settingsPath,
    "--mcp-config",
    mcpConfigPath,
    ...getUserArgs(), // Auto-detect user args for both terminal and panel modes
  ];

  // 5. Clear CLAUDECODE to allow nested Claude Code sessions inside CodeHydra
  delete process.env.CLAUDECODE;

  // 6. Catch SIGHUP/SIGTERM so the wrapper survives terminal-close signals
  //    and can still send WrapperEnd after Claude exits.
  installSignalHandlers();

  // 7. Notify wrapper start (clears loading screen before Claude shows dialogs)
  await notifyHook("WrapperStart");

  // 8. Spawn Claude with automatic session resume
  // Skip --continue attempt for new workspaces (no prior session to resume)
  const result = runClaude(claudeBinary.command, args, {
    skipContinue: consumeNoSessionMarker(),
    useShell: claudeBinary.useShell,
  });

  // 9. Notify wrapper end (Claude has exited)
  await notifyHook("WrapperEnd");

  // 10. Handle result
  if (result.error) {
    console.error(`Error: Failed to start Claude: ${result.error.message}`);
    process.exit(EXIT_SPAWN_FAILED);
  }

  process.exit(result.exitCode ?? EXIT_SPAWN_FAILED);
}

// Run main and handle any uncaught errors
// Skip when running in test environment (Vitest sets VITEST env var)
if (!process.env.VITEST) {
  main().catch((error: unknown) => {
    console.error("Fatal error:", error instanceof Error ? error.message : error);
    process.exit(EXIT_ENV_ERROR);
  });
}

// Export for testing
export {
  findSystemClaude,
  notifyHook,
  getInitialPromptConfig,
  buildInitialPromptArgs,
  buildPermissionArgs,
  consumeNoSessionMarker,
  installSignalHandlers,
};
