// ABOUTME: Tool executor that routes tool calls to file operations, MCP servers, or gateway.
// ABOUTME: Handles tool call parsing, execution, and result formatting.

import { invoke } from "@tauri-apps/api/core";
import { emit, listen, type UnlistenFn } from "@tauri-apps/api/event";
import { mcpClient } from "@/lib/mcp/client";
import { isOAuthTokenError } from "@/lib/oauth-tool-errors";
import type { ToolCall, ToolResult } from "@/lib/providers/types";
import { type PaymentRequirements, parsePaymentRequirements } from "@/lib/x402";
import {
  callGatewayTool,
  callSerenTool,
  type PaymentProxyInfo,
} from "@/services/mcp-gateway";
import { startShellProgressListener } from "@/services/shell-progress";
import { x402Service } from "@/services/x402";
import { getApprovalRequirement, requiresApproval } from "./approval-config";
import { parseGatewayToolName, parseMcpToolName } from "./definitions";

/**
 * File entry returned by list_directory.
 */
interface FileEntry {
  name: string;
  path: string;
  is_directory: boolean;
}

const GATEWAY_APPROVAL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
const SHELL_APPROVAL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
const MAX_RESULT_SIZE = 50_000; // 50KB cap
const MAX_ARRAY_ITEMS = 25;

/**
 * Emit an event to notify the UI that an OAuth connection has expired.
 * The OAuthLogins component listens for this to update the connection status.
 */
async function notifyOAuthExpired(
  publisherSlug: string,
  errorMessage: string,
): Promise<void> {
  try {
    await emit("oauth-connection-expired", {
      publisherSlug,
      errorMessage,
      timestamp: Date.now(),
    });
    console.log(
      `[Tool Executor] Emitted oauth-connection-expired for ${publisherSlug}`,
    );
  } catch (err) {
    console.error(
      "[Tool Executor] Failed to emit oauth-connection-expired:",
      err,
    );
  }
}

/**
 * Truncate large tool results to prevent overwhelming the AI context and database.
 * For JSON arrays (e.g. email lists), extracts key summary fields per item.
 */
function truncateToolResult(content: string): string {
  if (content.length <= MAX_RESULT_SIZE) return content;

  // Try to detect JSON array results (emails, records, etc.)
  try {
    const parsed = JSON.parse(content) as unknown;
    if (Array.isArray(parsed) && parsed.length > MAX_ARRAY_ITEMS) {
      const total = parsed.length;
      const summary = parsed.slice(0, MAX_ARRAY_ITEMS).map((item: unknown) => {
        if (typeof item === "object" && item !== null) {
          const record = item as Record<string, unknown>;
          const keys = Object.keys(record);
          const summaryKeys = [
            "id",
            "subject",
            "title",
            "name",
            "from",
            "sender",
            "date",
            "timestamp",
            "created_at",
            "snippet",
            "status",
            "type",
          ];
          const kept: Record<string, unknown> = {};
          for (const k of keys) {
            const val = record[k];
            if (
              summaryKeys.includes(k.toLowerCase()) ||
              typeof val !== "string" ||
              val.length < 200
            ) {
              kept[k] =
                typeof val === "string" && val.length > 200
                  ? `${val.slice(0, 200)}...`
                  : val;
            }
          }
          return Object.keys(kept).length > 0 ? kept : item;
        }
        return item;
      });
      return `${JSON.stringify(summary, null, 2)}\n\n[Showing ${MAX_ARRAY_ITEMS} of ${total} items. Full results truncated.]`;
    }
  } catch {
    // Not JSON, fall through to plain text truncation
  }

  return `${content.slice(0, MAX_RESULT_SIZE)}\n\n[Truncated: result was ${content.length.toLocaleString()} characters]`;
}

/**
 * Request user approval for a Gateway tool operation.
 * Returns a promise that resolves to true if approved, false if denied or timeout.
 */
async function requestGatewayApproval(
  publisherSlug: string,
  toolName: string,
  args: Record<string, unknown>,
): Promise<boolean> {
  const approvalId = `gateway-${Date.now()}-${Math.random().toString(36).slice(2)}`;
  const requirement = getApprovalRequirement(publisherSlug, toolName);

  console.log(
    `[Tool Executor] Requesting approval for ${publisherSlug}/${toolName} (ID: ${approvalId})`,
  );

  // Emit approval request event for UI to display
  try {
    await emit("gateway-tool-approval-request", {
      approvalId,
      publisherSlug,
      toolName,
      args,
      description: requirement?.description || "Execute operation",
      isDestructive: requirement?.isDestructive || false,
    });
  } catch (err) {
    console.error("[Tool Executor] Failed to emit approval request:", err);
    return false;
  }

  // Wait for approval response
  return new Promise((resolve) => {
    let unlisten: UnlistenFn | undefined;
    const timeout = setTimeout(() => {
      console.log(`[Tool Executor] Approval timeout for ${approvalId}`);
      unlisten?.();
      resolve(false);
    }, GATEWAY_APPROVAL_TIMEOUT_MS);

    listen<{ id: string; approved: boolean }>(
      "gateway-tool-approval-response",
      (event) => {
        if (event.payload.id !== approvalId) return;
        console.log(
          `[Tool Executor] Received approval response: ${event.payload.approved}`,
        );
        clearTimeout(timeout);
        unlisten?.();
        resolve(event.payload.approved);
      },
    )
      .then((fn) => {
        unlisten = fn;
      })
      .catch((err) => {
        console.error("[Tool Executor] Failed to listen for approval:", err);
        clearTimeout(timeout);
        resolve(false);
      });
  });
}

/**
 * Request user approval for a shell command execution.
 * All shell commands require approval — there is no bypass.
 */
async function requestShellApproval(
  command: string,
  timeoutSecs: number,
): Promise<boolean> {
  const approvalId = `shell-${Date.now()}-${Math.random().toString(36).slice(2)}`;

  console.log(
    `[Tool Executor] Requesting shell approval (ID: ${approvalId}): ${command}`,
  );

  try {
    await emit("shell-command-approval-request", {
      approvalId,
      command,
      timeoutSecs,
    });
  } catch (err) {
    console.error(
      "[Tool Executor] Failed to emit shell approval request:",
      err,
    );
    return false;
  }

  return new Promise((resolve) => {
    let unlisten: UnlistenFn | undefined;
    const timeout = setTimeout(() => {
      console.log(`[Tool Executor] Shell approval timeout for ${approvalId}`);
      unlisten?.();
      resolve(false);
    }, SHELL_APPROVAL_TIMEOUT_MS);

    listen<{ id: string; approved: boolean }>(
      "shell-command-approval-response",
      (event) => {
        if (event.payload.id !== approvalId) return;
        console.log(
          `[Tool Executor] Shell approval response: ${event.payload.approved}`,
        );
        clearTimeout(timeout);
        unlisten?.();
        resolve(event.payload.approved);
      },
    )
      .then((fn) => {
        unlisten = fn;
      })
      .catch((err) => {
        console.error(
          "[Tool Executor] Failed to listen for shell approval:",
          err,
        );
        clearTimeout(timeout);
        resolve(false);
      });
  });
}

/**
 * Execute a single tool call and return the result.
 * Routes to MCP servers or file tools based on prefix.
 */
export async function executeTool(toolCall: ToolCall): Promise<ToolResult> {
  const { name, arguments: argsJson } = toolCall.function;

  try {
    const args = (argsJson ? JSON.parse(argsJson) : {}) as Record<
      string,
      unknown
    >;

    // Check if this is a built-in Seren tool (seren__toolName)
    if (name.startsWith("seren__")) {
      const serenToolName = name.slice("seren__".length);
      const response = await callSerenTool(serenToolName, args);
      const content =
        typeof response.result === "string"
          ? response.result
          : JSON.stringify(response.result);
      return {
        tool_call_id: toolCall.id,
        content,
        is_error: response.is_error,
      };
    }

    // Check if this is a Seren Gateway tool call (gateway__publisher__toolName)
    const gatewayInfo = parseGatewayToolName(name);
    if (gatewayInfo) {
      return await executeGatewayTool(
        toolCall.id,
        gatewayInfo.publisherSlug,
        gatewayInfo.toolName,
        args,
      );
    }

    // Check if this is a local MCP tool call (mcp__server__toolName)
    const mcpInfo = parseMcpToolName(name);
    if (mcpInfo) {
      return await executeMcpTool(
        toolCall.id,
        mcpInfo.serverName,
        mcpInfo.toolName,
        args,
      );
    }

    // Otherwise, handle local file tools
    let result: unknown;

    switch (name) {
      case "read_file": {
        const path = args.path as string;
        validatePath(path);
        result = await invoke<string>("read_file", { path });
        break;
      }

      case "list_directory": {
        const path = args.path as string;
        validatePath(path);
        const entries = await invoke<FileEntry[]>("list_directory", { path });
        result = formatDirectoryListing(entries);
        break;
      }

      case "write_file": {
        const path = args.path as string;
        const content = args.content as string;
        validatePath(path);
        if (content == null) {
          throw new Error("Invalid content: content must be provided");
        }
        await invoke("write_file", { path, content });
        result = `Successfully wrote ${content.length} characters to ${path}`;
        break;
      }

      case "path_exists": {
        const path = args.path as string;
        validatePath(path);
        const exists = await invoke<boolean>("path_exists", { path });
        result = exists
          ? `Path exists: ${path}`
          : `Path does not exist: ${path}`;
        break;
      }

      case "create_directory": {
        const path = args.path as string;
        validatePath(path);
        await invoke("create_directory", { path });
        result = `Successfully created directory: ${path}`;
        break;
      }

      case "seren_web_fetch": {
        const url = args.url as string;
        const timeoutMs = args.timeout_ms as number | undefined;
        const response = await invoke<{
          content: string;
          content_type: string;
          url: string;
          status: number;
          truncated: boolean;
        }>("web_fetch", { url, timeoutMs });

        if (response.status >= 400) {
          result = `Error: HTTP ${response.status} for ${response.url}`;
        } else {
          result = response.content;
        }
        break;
      }

      case "execute_command": {
        const command = args.command as string;
        if (!command || typeof command !== "string") {
          throw new Error("Invalid command: must be a non-empty string");
        }
        const timeoutSecs = (args.timeout_secs as number) ?? 30;
        const invokeArgs: {
          command: string;
          timeoutSecs: number;
          injectSerenCredentials?: boolean;
          toolCallId: string;
        } = { command, timeoutSecs, toolCallId: toolCall.id };
        if (typeof args.inject_seren_credentials === "boolean") {
          invokeArgs.injectSerenCredentials = args.inject_seren_credentials;
        }

        const approved = await requestShellApproval(command, timeoutSecs);
        if (!approved) {
          return {
            tool_call_id: toolCall.id,
            content: "Command was not approved by user",
            is_error: true,
          };
        }

        // Backs the Tail / LIVE pane (#2100). Idempotent — first call
        // attaches the global subscription, subsequent ones await zero
        // work. Awaited so a chunk emitted before the bridge is up
        // doesn't get dropped.
        await startShellProgressListener();

        const cmdResult = await invoke<{
          stdout: string;
          stderr: string;
          exit_code: number | null;
          timed_out: boolean;
        }>("execute_shell_command_streaming", invokeArgs);

        if (cmdResult.timed_out) {
          result = `Command timed out after ${timeoutSecs} seconds.\nstderr: ${cmdResult.stderr}`;
        } else {
          const parts: string[] = [];
          if (cmdResult.stdout) parts.push(`stdout:\n${cmdResult.stdout}`);
          if (cmdResult.stderr) parts.push(`stderr:\n${cmdResult.stderr}`);
          parts.push(`exit_code: ${cmdResult.exit_code ?? "unknown"}`);
          result = parts.join("\n\n");
        }
        break;
      }

      case "run_skill_script": {
        const skillSlug = args.skill_slug as string;
        const cwd = args.cwd as string;
        const argv = args.argv as unknown;
        if (!skillSlug || typeof skillSlug !== "string") {
          throw new Error("Invalid skill_slug: must be a non-empty string");
        }
        if (!cwd || typeof cwd !== "string") {
          throw new Error("Invalid cwd: must be a non-empty string");
        }
        if (
          !Array.isArray(argv) ||
          argv.length === 0 ||
          argv.some((item) => typeof item !== "string" || item.length === 0)
        ) {
          throw new Error("Invalid argv: must be a non-empty string array");
        }
        const timeoutSecs = (args.timeout_secs as number) ?? 30;
        const preview = `${cwd}> ${argv.map((item) => JSON.stringify(item)).join(" ")}`;
        const approved = await requestShellApproval(preview, timeoutSecs);
        if (!approved) {
          return {
            tool_call_id: toolCall.id,
            content: "Skill script was not approved by user",
            is_error: true,
          };
        }

        const invokeArgs: {
          skillSlug: string;
          cwd: string;
          argv: string[];
          env?: Record<string, string>;
          timeoutSecs: number;
          injectSerenCredentials?: boolean;
        } = {
          skillSlug,
          cwd,
          argv,
          timeoutSecs,
        };
        if (
          args.env &&
          typeof args.env === "object" &&
          !Array.isArray(args.env)
        ) {
          invokeArgs.env = args.env as Record<string, string>;
        }
        if (typeof args.inject_seren_credentials === "boolean") {
          invokeArgs.injectSerenCredentials = args.inject_seren_credentials;
        }

        const cmdResult = await invoke<{
          stdout: string;
          stderr: string;
          exit_code: number | null;
          timed_out: boolean;
        }>("run_skill_script", invokeArgs);

        if (cmdResult.timed_out) {
          result = `Skill script timed out after ${timeoutSecs} seconds.\nstderr: ${cmdResult.stderr}`;
        } else {
          const parts: string[] = [];
          if (cmdResult.stdout) parts.push(`stdout:\n${cmdResult.stdout}`);
          if (cmdResult.stderr) parts.push(`stderr:\n${cmdResult.stderr}`);
          parts.push(`exit_code: ${cmdResult.exit_code ?? "unknown"}`);
          result = parts.join("\n\n");
        }
        break;
      }

      default:
        throw new Error(`Unknown tool: ${name}`);
    }

    const resultContent =
      typeof result === "string" ? result : JSON.stringify(result, null, 2);
    return {
      tool_call_id: toolCall.id,
      content: truncateToolResult(resultContent),
      is_error: false,
    };
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    console.error(`[Tool Executor] Tool "${name}" failed:`, message);
    return {
      tool_call_id: toolCall.id,
      content: `Error: ${message}`,
      is_error: true,
    };
  }
}

/**
 * Execute an MCP tool call via the MCP client (local stdio servers).
 */
async function executeMcpTool(
  toolCallId: string,
  serverName: string,
  toolName: string,
  args: Record<string, unknown>,
): Promise<ToolResult> {
  try {
    const result = await mcpClient.callTool(serverName, {
      name: toolName,
      arguments: args,
    });

    // Convert MCP result content to string
    let content = "";
    for (const item of result.content) {
      if (item.type === "text") {
        content += item.text;
      } else if (item.type === "image") {
        content += `[Image: ${item.mimeType}]`;
      } else if (item.type === "resource") {
        content += item.resource.text || `[Resource: ${item.resource.uri}]`;
      }
    }

    return {
      tool_call_id: toolCallId,
      content: truncateToolResult(content || "Tool executed successfully"),
      is_error: result.isError ?? false,
    };
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    console.error(
      `[Tool Executor] MCP tool "${serverName}/${toolName}" failed:`,
      message,
    );
    return {
      tool_call_id: toolCallId,
      content: `MCP tool error: ${message}`,
      is_error: true,
    };
  }
}

/**
 * Extract PaymentRequirements from proxy payment info.
 */
function extractPaymentRequirements(
  proxyInfo: PaymentProxyInfo,
): PaymentRequirements | null {
  // Try parsing from payment_requirements first (the body JSON)
  if (proxyInfo.payment_requirements) {
    try {
      return parsePaymentRequirements(
        JSON.stringify(proxyInfo.payment_requirements),
      );
    } catch {
      // Fall through to try header
    }
  }

  // Try parsing from the PAYMENT-REQUIRED header (base64-encoded)
  if (proxyInfo.payment_required_header) {
    try {
      const decoded = atob(proxyInfo.payment_required_header);
      return parsePaymentRequirements(decoded);
    } catch {
      // Failed to decode/parse header
    }
  }

  return null;
}

/**
 * Execute a gateway tool call via the MCP Gateway.
 * Handles x402 payment proxy flow: if server returns payment requirements,
 * signs the payment locally and retries with _x402_payment parameter.
 */
async function executeGatewayTool(
  toolCallId: string,
  publisherSlug: string,
  toolName: string,
  args: Record<string, unknown>,
): Promise<ToolResult> {
  try {
    // Check if this operation requires user approval
    if (requiresApproval(publisherSlug, toolName)) {
      console.log(
        `[Tool Executor] Operation requires approval: ${publisherSlug}/${toolName}`,
      );
      const approved = await requestGatewayApproval(
        publisherSlug,
        toolName,
        args,
      );

      if (!approved) {
        console.log("[Tool Executor] Operation denied by user");
        return {
          tool_call_id: toolCallId,
          content: "Operation was not approved by user",
          is_error: true,
        };
      }

      console.log("[Tool Executor] Operation approved by user");
    }

    const response = await callGatewayTool(publisherSlug, toolName, args);

    // Check if this is a payment proxy response (requires client-side signing)
    if (response.is_error && response.payment_proxy) {
      console.log(
        "[Tool Executor] Payment proxy detected, attempting local signing...",
      );

      const requirements = extractPaymentRequirements(response.payment_proxy);
      if (!requirements) {
        return {
          tool_call_id: toolCallId,
          content:
            "Payment required but could not parse payment requirements from server response",
          is_error: true,
        };
      }

      // Use the x402 service to handle payment (shows UI, signs, etc.)
      const paymentResult = await x402Service.handlePaymentRequired(
        `seren-gateway/${publisherSlug}`,
        toolName,
        new Error(JSON.stringify(response.payment_proxy)),
      );

      if (!paymentResult?.success) {
        return {
          tool_call_id: toolCallId,
          content: paymentResult?.error || "Payment was cancelled or failed",
          is_error: true,
        };
      }

      // If crypto payment was signed, retry with the payment header
      if (paymentResult.paymentHeader) {
        console.log("[Tool Executor] Retrying with signed payment...");

        const retryArgs = {
          ...args,
          _x402_payment: paymentResult.paymentHeader,
        };

        const retryResponse = await callGatewayTool(
          publisherSlug,
          toolName,
          retryArgs,
        );

        const retryContent =
          typeof retryResponse.result === "string"
            ? retryResponse.result
            : JSON.stringify(retryResponse.result, null, 2);

        return {
          tool_call_id: toolCallId,
          content: truncateToolResult(
            retryContent || "Tool executed successfully with payment",
          ),
          is_error: retryResponse.is_error,
        };
      }

      // SerenBucks payment - server handles it via auth token
      // Just retry the original call (auth token is always sent)
      if (paymentResult.method === "serenbucks") {
        console.log(
          "[Tool Executor] SerenBucks selected, retrying (server uses auth token)...",
        );

        // For SerenBucks, we might need to add a flag to indicate user confirmed
        // For now, just retry - the server should accept prepaid if available
        const retryResponse = await callGatewayTool(
          publisherSlug,
          toolName,
          args,
        );

        const retryContent =
          typeof retryResponse.result === "string"
            ? retryResponse.result
            : JSON.stringify(retryResponse.result, null, 2);

        return {
          tool_call_id: toolCallId,
          content: truncateToolResult(
            retryContent || "Tool executed successfully",
          ),
          is_error: retryResponse.is_error,
        };
      }
    }

    // Convert result to string content
    const content =
      typeof response.result === "string"
        ? response.result
        : JSON.stringify(response.result, null, 2);

    // Check for OAuth token errors in the response
    if (response.is_error && isOAuthTokenError(content)) {
      notifyOAuthExpired(publisherSlug, content);
    }

    return {
      tool_call_id: toolCallId,
      content: truncateToolResult(content || "Tool executed successfully"),
      is_error: response.is_error,
    };
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    console.error(
      `[Tool Executor] Gateway tool "${publisherSlug}/${toolName}" failed:`,
      message,
    );

    // Check for OAuth token errors and notify the UI
    if (isOAuthTokenError(message)) {
      notifyOAuthExpired(publisherSlug, message);
    }

    return {
      tool_call_id: toolCallId,
      content: `Gateway tool error: ${message}`,
      is_error: true,
    };
  }
}

/**
 * Execute multiple tool calls in parallel.
 */
export async function executeTools(
  toolCalls: ToolCall[],
): Promise<ToolResult[]> {
  return Promise.all(toolCalls.map(executeTool));
}

/**
 * Validate a path to prevent directory traversal attacks.
 * Throws if the path is suspicious.
 */
function validatePath(path: string): void {
  if (!path || typeof path !== "string") {
    throw new Error("Invalid path: path must be a non-empty string");
  }

  // Check for null bytes (common attack vector)
  if (path.includes("\0")) {
    throw new Error("Invalid path: contains null byte");
  }

  // Warn about suspicious patterns but don't block (user may have legitimate use)
  // The Tauri sandbox should handle actual security
  const normalized = path.replace(/\\/g, "/");
  if (normalized.includes("/../") || normalized.startsWith("../")) {
    console.warn(
      `[Tool Executor] Path contains parent directory traversal: ${path}`,
    );
  }
}

/**
 * Format directory listing for readable output.
 */
function formatDirectoryListing(entries: FileEntry[]): string {
  if (entries.length === 0) {
    return "Directory is empty";
  }

  const lines = entries.map((entry) => {
    const prefix = entry.is_directory ? "[DIR]  " : "[FILE] ";
    return `${prefix}${entry.name}`;
  });

  return lines.join("\n");
}
