import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { saveBinary, type JobQuotaState } from './binary-saver.js';
import { saveMcpRaw } from './raw-logger.js';
import { logger } from '../logger.js';

export interface ExecuteCtx {
  workspacePath: string;
  ownerId: string;
  jobId: string;
  config: {
    maxBinarySizeMb: number;
    maxOutputFilesPerJob: number;
    maxOutputSizeMbPerJob: number;
    callTimeoutSeconds: number;
  };
  quotaState: JobQuotaState;
}

export interface ExecuteInput {
  client: Client;
  serverId: string;
  toolName: string;
  input: Record<string, unknown>;
  ctx: ExecuteCtx;
}

export interface ExecuteResult {
  output: string;
  isError: boolean;
}

interface McpContentBlock {
  type: string;
  text?: string;
  data?: string;
  mimeType?: string;
  resource?: { uri?: string; blob?: string; mimeType?: string };
}

export async function executeMcpCall(input: ExecuteInput): Promise<ExecuteResult> {
  const { client, serverId, toolName, ctx } = input;
  const timeoutMs = ctx.config.callTimeoutSeconds * 1000;
  const abortController = new AbortController();
  const timer = setTimeout(() => abortController.abort(), timeoutMs);
  try {
    const resp = (await client.callTool({ name: toolName, arguments: input.input })) as {
      content?: McpContentBlock[];
      isError?: boolean;
    };
    const content = resp.content ?? [];
    const texts: string[] = [];
    const savedPaths: string[] = [];

    for (const block of content) {
      if (block.type === 'text' && typeof block.text === 'string') {
        texts.push(block.text);
      } else if (block.type === 'image' && typeof block.data === 'string') {
        const bytes = Buffer.from(block.data, 'base64');
        const mimeType = block.mimeType ?? 'application/octet-stream';
        const saved = await saveBinary({
          workspacePath: ctx.workspacePath,
          serverId,
          toolName,
          bytes,
          mimeType,
          maxBytes: ctx.config.maxBinarySizeMb * 1024 * 1024,
          jobQuota: {
            maxFiles: ctx.config.maxOutputFilesPerJob,
            maxBytes: ctx.config.maxOutputSizeMbPerJob * 1024 * 1024,
            state: ctx.quotaState,
          },
        });
        if (saved.ok) savedPaths.push(saved.relPath);
        else texts.push(`(failed to save image: ${saved.reason})`);
      } else if (block.type === 'resource' && block.resource) {
        if (block.resource.blob) {
          const bytes = Buffer.from(block.resource.blob, 'base64');
          const mimeType = block.resource.mimeType ?? 'application/octet-stream';
          const saved = await saveBinary({
            workspacePath: ctx.workspacePath,
            serverId,
            toolName,
            bytes,
            mimeType,
            maxBytes: ctx.config.maxBinarySizeMb * 1024 * 1024,
            jobQuota: {
              maxFiles: ctx.config.maxOutputFilesPerJob,
              maxBytes: ctx.config.maxOutputSizeMbPerJob * 1024 * 1024,
              state: ctx.quotaState,
            },
          });
          if (saved.ok) savedPaths.push(saved.relPath);
          else texts.push(`(failed to save resource: ${saved.reason})`);
        } else if (block.resource.uri) {
          texts.push(`(resource uri reference: ${block.resource.uri} — not downloaded)`);
        }
      }
    }

    // NOTE: text blocks are concatenated with empty separator (per test contract);
    // adjacent blocks are pieces of one response and should join naturally.
    const output = [texts.join(''), ...savedPaths.map((p) => `Saved: ${p}`)]
      .filter((s) => s.length > 0)
      .join('\n');
    const finalOutput = output || '(empty)';
    const isError = resp.isError === true;
    saveMcpRaw({
      workspacePath: ctx.workspacePath,
      serverId,
      toolName,
      args: input.input,
      content,
      isError,
      output: finalOutput,
      savedPaths,
    });
    return { output: finalOutput, isError };
  } catch (err) {
    const msg = (err as Error).message;
    logger.warn(`[mcp:executor] callTool failed server=${serverId} tool=${toolName}: ${msg}`);
    const failureOutput = `MCP call failed: ${msg}`;
    saveMcpRaw({
      workspacePath: ctx.workspacePath,
      serverId,
      toolName,
      args: input.input,
      content: [],
      isError: true,
      output: failureOutput,
      savedPaths: [],
    });
    return { output: failureOutput, isError: true };
  } finally {
    clearTimeout(timer);
  }
}
