/**
 * Codex Message Translator
 *
 * Handles outgoing browser messages by translating them into Codex
 * JSON-RPC calls. Extracted from codex-adapter.ts following the
 * ws-bridge-* decomposition pattern.
 */

import type pino from "pino";
import type { BrowserIncomingMessage, BrowserOutgoingMessage, McpServerConfig } from "./session-types.js";
import type { JsonRpcTransport } from "./json-rpc-transport.js";
import type { CodexAdapterMutableState } from "./codex-event-handlers.js";
import {
  buildDynamicToolCallResponse,
} from "./codex-approval-handlers.js";
import {
  mapApprovalPolicy,
  mapSandboxPolicyObject,
  mapCollaborationMode,
} from "./codex-session-helpers.js";
import {
  listAllMcpServerStatuses,
  readMcpServersConfig,
  reloadMcpServers,
  buildMcpServerDetails,
  buildMcpConfigEdits,
} from "./codex-mcp-operations.js";

// ─── Dependencies injected by the adapter class ──────────────────────────────

export interface OutgoingHandlerDeps {
  state: CodexAdapterMutableState;
  transport: JsonRpcTransport;
  log: pino.Logger;
  threadId: string | null;
  model: string;
  getExecutionCwd: () => string;
  emit: (msg: BrowserIncomingMessage) => void;
  lastSentCollaborationModeKind: "default" | "plan" | null;
  setLastSentCollaborationModeKind: (kind: "default" | "plan") => void;
  setCurrentTurnId: (turnId: string | null) => void;
}

// ─── Outgoing message dispatcher ─────────────────────────────────────────────

export function dispatchOutgoingMessage(
  msg: BrowserOutgoingMessage,
  deps: OutgoingHandlerDeps,
): boolean {
  switch (msg.type) {
    case "user_message":
      handleOutgoingUserMessage(msg as { type: "user_message"; content: string; images?: { media_type: string; data: string }[] }, deps);
      return true;
    case "permission_response":
      handleOutgoingPermissionResponse(
        msg as { type: "permission_response"; request_id: string; behavior: "allow" | "deny"; updated_input?: Record<string, unknown> },
        deps,
      );
      return true;
    case "interrupt":
      handleOutgoingInterrupt(deps);
      return true;
    case "set_model":
      deps.log.warn("Runtime model switching not supported by Codex");
      return false;
    case "set_permission_mode":
      handleOutgoingSetPermissionMode((msg as { type: "set_permission_mode"; mode: string }).mode, deps);
      return true;
    case "mcp_get_status":
      handleOutgoingMcpGetStatus(deps);
      return true;
    case "mcp_toggle":
      handleOutgoingMcpToggle(
        (msg as { type: "mcp_toggle"; serverName: string; enabled: boolean }).serverName,
        (msg as { type: "mcp_toggle"; serverName: string; enabled: boolean }).enabled,
        deps,
      );
      return true;
    case "mcp_reconnect":
      handleOutgoingMcpReconnect(deps);
      return true;
    case "mcp_set_servers":
      handleOutgoingMcpSetServers(
        (msg as { type: "mcp_set_servers"; servers: Record<string, McpServerConfig> }).servers,
        deps,
      );
      return true;
    default:
      return false;
  }
}

// ─── Individual outgoing handlers ────────────────────────────────────────────

async function handleOutgoingUserMessage(
  msg: { type: "user_message"; content: string; images?: { media_type: string; data: string }[] },
  deps: OutgoingHandlerDeps,
): Promise<void> {
  if (!deps.threadId) {
    deps.emit({ type: "error", message: "No Codex thread started yet" });
    return;
  }

  const input: Array<{ type: string; text?: string; url?: string }> = [];

  if (msg.images?.length) {
    for (const img of msg.images) {
      input.push({
        type: "image",
        url: `data:${img.media_type};base64,${img.data}`,
      });
    }
  }

  input.push({ type: "text", text: msg.content });

  try {
    const turnParams: Record<string, unknown> = {
      threadId: deps.threadId,
      input,
      cwd: deps.getExecutionCwd(),
      approvalPolicy: mapApprovalPolicy(deps.state.currentPermissionMode),
      sandboxPolicy: mapSandboxPolicyObject(deps.state.currentPermissionMode),
    };
    if (deps.state.currentCollaborationModeKind !== deps.lastSentCollaborationModeKind) {
      turnParams.collaborationMode = mapCollaborationMode(deps.state.currentCollaborationModeKind, deps.model);
      deps.setLastSentCollaborationModeKind(deps.state.currentCollaborationModeKind);
    }
    const result = await deps.transport.call("turn/start", turnParams) as { turn: { id: string } };
    deps.setCurrentTurnId(result.turn.id);
  } catch (err) {
    deps.emit({ type: "error", message: `Failed to start turn: ${err}` });
  }
}

async function handleOutgoingPermissionResponse(
  msg: { type: "permission_response"; request_id: string; behavior: "allow" | "deny"; updated_input?: Record<string, unknown> },
  deps: OutgoingHandlerDeps,
): Promise<void> {
  const jsonRpcId = deps.state.pendingApprovals.get(msg.request_id);
  if (jsonRpcId === undefined) {
    deps.log.warn({ requestId: msg.request_id }, "No pending approval for request");
    return;
  }

  // Dynamic tool calls require a DynamicToolCallResponse payload
  const pendingDynamic = deps.state.pendingDynamicToolCalls.get(msg.request_id);
  if (pendingDynamic) {
    deps.state.pendingDynamicToolCalls.delete(msg.request_id);
    deps.state.pendingApprovals.delete(msg.request_id);
    clearTimeout(pendingDynamic.timeout);

    const result = buildDynamicToolCallResponse(msg, pendingDynamic.toolName);
    await deps.transport.respond(jsonRpcId, result);
    return;
  }

  deps.state.pendingApprovals.delete(msg.request_id);

  // User input requests need ToolRequestUserInputResponse
  const questionIds = deps.state.pendingUserInputQuestionIds.get(msg.request_id);
  if (questionIds) {
    deps.state.pendingUserInputQuestionIds.delete(msg.request_id);

    if (msg.behavior === "deny") {
      await deps.transport.respond(jsonRpcId, { answers: {} });
      return;
    }

    const browserAnswers = msg.updated_input?.answers as Record<string, string> || {};
    const codexAnswers: Record<string, { answers: string[] }> = {};
    for (let i = 0; i < questionIds.length; i++) {
      const answer = browserAnswers[String(i)];
      if (answer !== undefined) {
        codexAnswers[questionIds[i]] = { answers: [answer] };
      }
    }

    await deps.transport.respond(jsonRpcId, { answers: codexAnswers });
    return;
  }

  // Review decisions need ReviewDecision format
  if (deps.state.pendingReviewDecisions.has(msg.request_id)) {
    deps.state.pendingReviewDecisions.delete(msg.request_id);
    const decision = msg.behavior === "allow" ? "approved" : "denied";
    await deps.transport.respond(jsonRpcId, { decision });
    return;
  }

  // Standard item/*/requestApproval — uses accept/decline
  const decision = msg.behavior === "allow" ? "accept" : "decline";
  await deps.transport.respond(jsonRpcId, { decision });
}

async function handleOutgoingInterrupt(deps: OutgoingHandlerDeps): Promise<void> {
  if (!deps.threadId || !deps.state.currentTurnId) return;

  try {
    await deps.transport.call("turn/interrupt", {
      threadId: deps.threadId,
      turnId: deps.state.currentTurnId,
    });
  } catch (err) {
    deps.log.warn({ err }, "Interrupt failed");
  }
}

function handleOutgoingSetPermissionMode(mode: string, deps: OutgoingHandlerDeps): void {
  const nextMode = mode === "default"
    ? deps.state.lastNonPlanPermissionMode
    : mode;

  deps.state.currentPermissionMode = nextMode;
  if (nextMode === "plan") {
    deps.state.currentCollaborationModeKind = "plan";
  } else {
    deps.state.currentCollaborationModeKind = "default";
    deps.state.lastNonPlanPermissionMode = nextMode;
  }

  deps.emit({
    type: "session_update",
    session: { permissionMode: deps.state.currentPermissionMode },
  });
}

async function handleOutgoingMcpGetStatus(deps: OutgoingHandlerDeps): Promise<void> {
  try {
    const statusEntries = await listAllMcpServerStatuses(deps.transport);
    const configMap = await readMcpServersConfig(deps.transport);
    const servers = buildMcpServerDetails(statusEntries, configMap);
    deps.emit({ type: "mcp_status", servers });
  } catch (err) {
    deps.emit({ type: "error", message: `Failed to get MCP status: ${err}` });
  }
}

async function handleOutgoingMcpToggle(serverName: string, enabled: boolean, deps: OutgoingHandlerDeps): Promise<void> {
  try {
    if (serverName.includes(".")) {
      throw new Error("Server names containing '.' are not supported for toggle");
    }
    await deps.transport.call("config/value/write", {
      keyPath: `mcp_servers.${serverName}.enabled`,
      value: enabled,
      mergeStrategy: "upsert",
    });
    await reloadMcpServers(deps.transport);
    await handleOutgoingMcpGetStatus(deps);
  } catch (err) {
    const msg = String(err);
    if (msg.includes("invalid transport")) {
      try {
        await deps.transport.call("config/value/write", {
          keyPath: `mcp_servers.${serverName}`,
          value: null,
          mergeStrategy: "replace",
        });
        await reloadMcpServers(deps.transport);
        await handleOutgoingMcpGetStatus(deps);
        return;
      } catch (removeErr) {
        deps.log.debug({ serverName, err: removeErr }, "Failed to remove MCP server config, falling through to user-visible error");
      }
    }
    deps.emit({ type: "error", message: `Failed to toggle MCP server "${serverName}": ${err}` });
  }
}

async function handleOutgoingMcpReconnect(deps: OutgoingHandlerDeps): Promise<void> {
  try {
    await reloadMcpServers(deps.transport);
    await handleOutgoingMcpGetStatus(deps);
  } catch (err) {
    deps.emit({ type: "error", message: `Failed to reload MCP servers: ${err}` });
  }
}

async function handleOutgoingMcpSetServers(
  servers: Record<string, McpServerConfig>,
  deps: OutgoingHandlerDeps,
): Promise<void> {
  try {
    const edits = buildMcpConfigEdits(servers);
    if (edits.length > 0) {
      await deps.transport.call("config/batchWrite", { edits });
    }
    await reloadMcpServers(deps.transport);
    await handleOutgoingMcpGetStatus(deps);
  } catch (err) {
    deps.emit({ type: "error", message: `Failed to configure MCP servers: ${err}` });
  }
}
