import type { McpOrchestrator } from '../orchestrator.js';
import type { McpProxyResponse } from '../mcp-proxy-service.js';
import { buildRuntimeSpawnCmd } from '../mcp-proxy-service.js';

/**
 * STDIO transport client for MCP servers running as Docker containers.
 *
 * Runs `docker exec` with an inline script that spawns the MCP server
 * binary, pipes JSON-RPC messages via stdin/stdout, and returns the response.
 *
 * The inline script language matches the container runtime:
 *   node → Node.js script
 *   python → Python script
 *
 * Each call is self-contained: initialize → notifications/initialized → request → response.
 */
export async function sendViaStdio(
  orchestrator: McpOrchestrator,
  containerId: string,
  packageName: string | null,
  method: string,
  params?: Record<string, unknown>,
  timeoutMs = 30_000,
  command?: string[] | null,
  runtime = 'node',
): Promise<McpProxyResponse> {
  const initMsg = JSON.stringify({
    jsonrpc: '2.0',
    id: 1,
    method: 'initialize',
    params: {
      protocolVersion: '2024-11-05',
      capabilities: {},
      clientInfo: { name: 'mcpctl-proxy', version: '0.0.1' },
    },
  });
  const initializedMsg = JSON.stringify({
    jsonrpc: '2.0',
    method: 'notifications/initialized',
  });

  const requestBody: Record<string, unknown> = {
    jsonrpc: '2.0',
    id: 2,
    method,
  };
  if (params !== undefined) {
    requestBody.params = params;
  }
  const requestMsg = JSON.stringify(requestBody);

  // Determine spawn command
  let spawnCmd: string[];
  if (command && command.length > 0) {
    spawnCmd = command;
  } else if (packageName) {
    spawnCmd = buildRuntimeSpawnCmd(runtime, packageName);
  } else {
    return errorResponse('No packageName or command for STDIO server');
  }

  // Build the exec command based on runtime
  let execCmd: string[];
  if (runtime === 'python') {
    execCmd = buildPythonExecCmd(spawnCmd, initMsg, initializedMsg, requestMsg, timeoutMs);
  } else {
    execCmd = buildNodeExecCmd(spawnCmd, initMsg, initializedMsg, requestMsg, timeoutMs);
  }

  try {
    const result = await orchestrator.execInContainer(
      containerId,
      execCmd,
      { timeoutMs },
    );

    if (result.exitCode === 0 && result.stdout.trim()) {
      try {
        return JSON.parse(result.stdout.trim()) as McpProxyResponse;
      } catch {
        return errorResponse(`Failed to parse STDIO response: ${result.stdout.slice(0, 200)}`);
      }
    }

    // Try to parse error response from stdout
    try {
      return JSON.parse(result.stdout.trim()) as McpProxyResponse;
    } catch {
      const errorMsg = result.stderr.trim() || `docker exec exit code ${result.exitCode}`;
      return errorResponse(errorMsg);
    }
  } catch (err) {
    return errorResponse(err instanceof Error ? err.message : String(err));
  }
}

function buildNodeExecCmd(
  spawnCmd: string[],
  initMsg: string,
  initializedMsg: string,
  requestMsg: string,
  timeoutMs: number,
): string[] {
  const spawnArgs = JSON.stringify(spawnCmd);
  const probeScript = `
const { spawn } = require('child_process');
const args = ${spawnArgs};
const proc = spawn(args[0], args.slice(1), { stdio: ['pipe', 'pipe', 'pipe'] });
let output = '';
let responded = false;
proc.stdout.on('data', d => {
  output += d;
  const lines = output.split('\\n');
  for (const line of lines) {
    if (!line.trim()) continue;
    try {
      const msg = JSON.parse(line);
      if (msg.id === 2) {
        responded = true;
        process.stdout.write(JSON.stringify(msg), () => {
          proc.kill();
          process.exit(0);
        });
      }
    } catch {}
  }
  output = lines[lines.length - 1] || '';
});
proc.stderr.on('data', () => {});
proc.on('error', e => { process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:2,error:{code:-32000,message:e.message}})); process.exit(1); });
proc.on('exit', (code) => { if (!responded) { process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:2,error:{code:-32000,message:'process exited '+code}})); process.exit(1); } });
setTimeout(() => { if (!responded) { process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:2,error:{code:-32000,message:'timeout'}})); proc.kill(); process.exit(1); } }, ${timeoutMs - 2000});
proc.stdin.write(${JSON.stringify(initMsg)} + '\\n');
setTimeout(() => {
  proc.stdin.write(${JSON.stringify(initializedMsg)} + '\\n');
  setTimeout(() => {
    proc.stdin.write(${JSON.stringify(requestMsg)} + '\\n');
  }, 500);
}, 500);
`.trim();
  return ['node', '-e', probeScript];
}

function buildPythonExecCmd(
  spawnCmd: string[],
  initMsg: string,
  initializedMsg: string,
  requestMsg: string,
  timeoutMs: number,
): string[] {
  const spawnArgsJson = JSON.stringify(spawnCmd);
  const probeScript = `
import subprocess, sys, json, time, signal, threading
args = ${spawnArgsJson}
proc = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
responded = False
def timeout_handler():
    global responded
    if not responded:
        sys.stdout.write(json.dumps({"jsonrpc":"2.0","id":2,"error":{"code":-32000,"message":"timeout"}}))
        sys.stdout.flush()
        proc.kill()
        sys.exit(1)
timer = threading.Timer(${(timeoutMs - 2000) / 1000}, timeout_handler)
timer.daemon = True
timer.start()
proc.stdin.write((${JSON.stringify(initMsg)} + "\\n").encode())
proc.stdin.flush()
time.sleep(0.5)
proc.stdin.write((${JSON.stringify(initializedMsg)} + "\\n").encode())
proc.stdin.flush()
time.sleep(0.5)
proc.stdin.write((${JSON.stringify(requestMsg)} + "\\n").encode())
proc.stdin.flush()
output = ""
while True:
    line = proc.stdout.readline()
    if not line:
        break
    line = line.decode().strip()
    if not line:
        continue
    try:
        msg = json.loads(line)
        if msg.get("id") == 2:
            responded = True
            timer.cancel()
            sys.stdout.write(json.dumps(msg))
            sys.stdout.flush()
            proc.kill()
            sys.exit(0)
    except json.JSONDecodeError:
        pass
if not responded:
    sys.stdout.write(json.dumps({"jsonrpc":"2.0","id":2,"error":{"code":-32000,"message":"process exited " + str(proc.returncode)}}))
    sys.stdout.flush()
    sys.exit(1)
`.trim();
  return ['python3', '-c', probeScript];
}

function errorResponse(message: string): McpProxyResponse {
  return {
    jsonrpc: '2.0',
    id: 2,
    error: { code: -32000, message },
  };
}
