import { spawnSync } from "node:child_process";
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
import { Codex, type CodexOptions, type ThreadEvent } from "@openai/codex-sdk";
import { log } from "../utils/cli.ts";
import { addInstructions } from "./instructions.ts";
import { agent, installFromNpmTarball } from "./shared.ts";

export const codex = agent({
  name: "codex",
  inputKey: "openai_api_key",
  install: async () => {
    return await installFromNpmTarball({
      packageName: "@openai/codex",
      version: "latest",
      executablePath: "bin/codex.js",
    });
  },
  run: async ({ prompt, mcpServers, apiKey, cliPath, githubInstallationToken }) => {
    process.env.OPENAI_API_KEY = apiKey;
    process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
    // Configure MCP servers for Codex (global config is fine - not part of repo)
    if (mcpServers && Object.keys(mcpServers).length > 0) {
      configureMcpServers({ mcpServers, apiKey, cliPath });
    }

    // Configure Codex
    const codexOptions: CodexOptions = {
      apiKey,
      codexPathOverride: cliPath,
    };

    const codex = new Codex(codexOptions);
    // Configure thread options to match Claude's permissions (bypassPermissions)
    // approvalPolicy: "never" = no approval needed (equivalent to bypassPermissions)
    // sandboxMode: "workspace-write" = allow file writes
    // networkAccessEnabled: true = allow network access (needed for GitHub API calls)
    const thread = codex.startThread({
      approvalPolicy: "never",
      sandboxMode: "workspace-write",
      networkAccessEnabled: true,
    });

    try {
      // Use runStreamed to get streaming events similar to claude.ts
      const streamedTurn = await thread.runStreamed(addInstructions(prompt));

      // Stream events and handle them
      let finalOutput = "";
      for await (const event of streamedTurn.events) {
        const handler = messageHandlers[event.type];
        if (handler) {
          handler(event as never);
        }

        // Capture final response from agent messages
        if (event.type === "item.completed" && event.item.type === "agent_message") {
          finalOutput = event.item.text;
        }
      }

      return {
        success: true,
        output: finalOutput,
      };
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      log.error(`Codex execution failed: ${errorMessage}`);
      return {
        success: false,
        error: errorMessage,
        output: "",
      };
    }
  },
});

// Track command execution IDs to identify when command results come back
const commandExecutionIds = new Set<string>();

type ThreadEventHandler<type extends ThreadEvent["type"]> = (
  event: Extract<ThreadEvent, { type: type }>
) => void;

const messageHandlers: {
  [type in ThreadEvent["type"]]: ThreadEventHandler<type>;
} = {
  "thread.started": () => {
    // No logging needed
  },
  "turn.started": () => {
    // No logging needed
  },
  "turn.completed": async (event) => {
    await log.summaryTable([
      [
        { data: "Input Tokens", header: true },
        { data: "Cached Input Tokens", header: true },
        { data: "Output Tokens", header: true },
      ],
      [
        String(event.usage.input_tokens || 0),
        String(event.usage.cached_input_tokens || 0),
        String(event.usage.output_tokens || 0),
      ],
    ]);
  },
  "turn.failed": (event) => {
    log.error(`Turn failed: ${event.error.message}`);
  },
  "item.started": (event) => {
    const item = event.item;
    if (item.type === "command_execution") {
      log.info(`→ ${item.command}`);
      commandExecutionIds.add(item.id);
    } else if (item.type === "agent_message") {
      // Will be handled on completion
    } else if (item.type === "mcp_tool_call") {
      log.info(`→ ${item.tool} (${item.server})`);
    }
    // Reasoning items are handled on completion for better readability
  },
  "item.updated": (event) => {
    const item = event.item;
    if (item.type === "command_execution") {
      if (item.status === "in_progress" && item.aggregated_output) {
        // Command is still running, could show progress if needed
      }
    }
  },
  "item.completed": (event) => {
    const item = event.item;
    if (item.type === "agent_message") {
      log.box(item.text.trim(), { title: "Codex" });
    } else if (item.type === "command_execution") {
      const isTracked = commandExecutionIds.has(item.id);
      if (isTracked) {
        log.startGroup(`bash output`);
        if (item.status === "failed" || (item.exit_code !== undefined && item.exit_code !== 0)) {
          log.warning(item.aggregated_output || "Command failed");
        } else {
          log.info(item.aggregated_output || "");
        }
        log.endGroup();
        commandExecutionIds.delete(item.id);
      }
    } else if (item.type === "mcp_tool_call") {
      if (item.status === "failed" && item.error) {
        log.warning(`MCP tool call failed: ${item.error.message}`);
      }
    } else if (item.type === "reasoning") {
      // Display reasoning in a human-readable format
      const reasoningText = item.text.trim();
      // Remove markdown bold markers if present for cleaner output
      const cleanText = reasoningText.replace(/\*\*/g, "");
      log.info(cleanText);
    }
  },
  error: (event) => {
    log.error(`Error: ${event.message}`);
  },
};

function configureMcpServers({
  mcpServers,
  apiKey,
  cliPath,
}: {
  mcpServers: Record<string, McpServerConfig>;
  apiKey: string;
  cliPath: string;
}): void {
  log.info("Configuring MCP servers for Codex...");
  for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
    // Only configure stdio servers (Codex CLI supports stdio MCP servers)
    // Check if it's a stdio server config (has 'command' property)
    if (!("command" in serverConfig)) {
      log.warning(`MCP server '${serverName}' is not a stdio server, skipping...`);
      continue;
    }

    // Build command and args
    const command = serverConfig.command;
    const args = serverConfig.args || [];
    const envVars = serverConfig.env || {};

    // Build the codex mcp add command with proper argument handling
    const addArgs = ["mcp", "add", serverName];

    // Add environment variables as --env flags
    for (const [key, value] of Object.entries(envVars)) {
      addArgs.push("--env", `${key}=${value}`);
    }

    addArgs.push("--", command, ...args);

    log.info(`Adding MCP server '${serverName}'...`);
    const addResult = spawnSync("node", [cliPath, ...addArgs], {
      stdio: "pipe",
      encoding: "utf-8",
      env: {
        ...process.env,
        OPENAI_API_KEY: apiKey,
      },
    });

    if (addResult.status !== 0) {
      throw new Error(
        `codex mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
      );
    }
    log.info(`✓ MCP server '${serverName}' configured`);
  }
}
