import { spawn, ChildProcess } from 'child_process';
import { join } from 'path';
import { fileURLToPath } from 'url';
import { getMcpUrl } from '../config';
import { mcpHttpService } from './mcp.http.service';

const __filename = fileURLToPath(import.meta.url);
const __dirname = join(__filename, '..');

// Path to the compiled MCP server (two levels up from backend/src/)
const MCP_BIN = join(__dirname, '..', '..', 'mcp', 'dist', 'index.js');

let _proc: ChildProcess | null = null;
let nextId = 1;
let pending = new Map<number | string, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();

function send(msg: Record<string, unknown>): number {
  const id = nextId++;
  _proc!.stdin!.write(JSON.stringify({ jsonrpc: '2.0', id, ...msg }) + '\n');
  return id;
}

export const mcpClient = {
  async start(): Promise<void> {
    const hasMcpBin = (() => {
      try {
        require('fs').accessSync(MCP_BIN);
        return true;
      } catch {
        return false;
      }
    })();

    if (!hasMcpBin) {
      const url = getMcpUrl();
      if (url) {
        console.log('[mcp] Local MCP binary not found, using HTTP service at', url);
        return;
      }
      throw new Error('MCP local binary and HTTP URL both unavailable');
    }

    if (_proc) return;
    return new Promise<void>((resolve, reject) => {
      _proc = spawn('node', [MCP_BIN], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env } });

      _proc.stdout!.on('data', (chunk: Buffer) => {
        const text = chunk.toString();
        for (const line of text.split('\n')) {
          if (!line.trim()) continue;
          let parsed: { jsonrpc?: string; id?: number | string; method?: string; result?: unknown; error?: unknown };
          try { parsed = JSON.parse(line); } catch { continue; }
          if (parsed.jsonrpc !== '2.0') continue;
          if (parsed.method) {
            // notifications or responses without matching id — ignore for now
            continue;
          }
          if (!parsed.id) continue;
          const p = pending.get(parsed.id);
          if (!p) continue;
          pending.delete(parsed.id);
          if (parsed.error) {
            p.reject(new Error(`MCP error: ${JSON.stringify(parsed.error)}`));
          } else {
            p.resolve(parsed.result);
          }
        }
      });

      _proc.stderr!.on('data', (chunk: Buffer) => {
        console.log(`[mcp-client] stderr: ${chunk.toString().trim()}`);
      });

      _proc.on('exit', (code, signal) => {
        console.error(`[mcp-client] Exited code=${code} signal=${signal}`);
        _proc = null;
        for (const [, p] of pending) {
          p.reject(new Error('MCP client process exited'));
        }
        pending.clear();
      });

      _proc.on('error', (err: Error) => {
        console.error(`[mcp-client] Error: ${err.message}`);
        reject(err);
      });

      // Send initialize request
      const initId = send({
        method: 'initialize',
        params: {
          protocolVersion: '2025-03-26',
          capabilities: {},
          clientInfo: { name: 'quibot-backend', version: '1.0.0' },
        },
      });

      pending.set(initId, {
        resolve: () => {
          // Send initialized notification
          _proc!.stdin!.write(
            JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n',
          );
          resolve();
        },
        reject,
      });

      setTimeout(() => {
        const p = pending.get(initId);
        if (p) {
          pending.delete(initId);
          p.reject(new Error('MCP initialize timed out'));
        }
      }, 15_000);
    });
  },

  async callTool(name: string, args: Record<string, unknown>): Promise<{ text: string; isError?: boolean }> {
    if (!_proc) {
      await this.start();
    }

    try {
      return await this._callToolLocal(name, args);
    } catch (localErr) {
      const url = getMcpUrl();
      if (url) {
        console.log(`[mcp] Local MCP failed: ${localErr instanceof Error ? localErr.message : localErr}. Falling back to HTTP service.`);
        return await mcpHttpService.callTool(name, args);
      }
      throw localErr;
    }
  },

  async _callToolLocal(name: string, args: Record<string, unknown>): Promise<{ text: string; isError?: boolean }> {
    if (!_proc?.stdin) {
      throw new Error('MCP client not ready');
    }

    return new Promise((resolve, reject) => {
      const id = send({
        method: 'tools/call',
        params: { name, arguments: args },
      });

      let cleared = false;
      const timer = setTimeout(() => {
        if (cleared) return;
        cleared = true;
        pending.delete(id);
        reject(new Error(`MCP tool "${name}" timed out`));
      }, 180_000);

      pending.set(id, {
        resolve: (result: unknown) => {
          if (cleared) return;
          cleared = true;
          clearTimeout(timer);
          pending.delete(id);
          const res = result as { content?: Array<{ type: string; text?: string }> };
          if (res?.content?.[0]?.text !== undefined) {
            resolve({ text: res.content[0].text });
          } else {
            reject(new Error('MCP tool returned no content'));
          }
        },
        reject: (err: Error) => {
          if (cleared) return;
          cleared = true;
          clearTimeout(timer);
          reject(err);
        },
      });
    });
  },

  async shutdown(): Promise<void> {
    if (!_proc) return;
    _proc.kill('SIGTERM');
    const current = _proc;
    await new Promise<void>((resolve) => {
      let done = false;
      const cleanup = () => {
        if (done) return;
        done = true;
        _proc = null;
        for (const [, p] of pending) {
          p.reject(new Error('MCP client shut down'));
        }
        pending.clear();
        resolve();
      };
      current.once('exit', () => cleanup());
      setTimeout(() => {
        const proc = _proc;
        if (proc && !proc.killed) proc.kill('SIGKILL');
        cleanup();
      }, 3000);
    });
  },
};
