/**
 * MCP server over stdin/stdout for the traffic inspector.
 *
 * Claude adds this to .mcp.json as:
 *   { "mcpctl-inspect": { "command": "mcpctl", "args": ["console", "--stdin-mcp"] } }
 *
 * Subscribes to mcplocal's /inspect SSE endpoint and exposes traffic
 * data via MCP tools: list_sessions, get_traffic, get_session_info.
 */

import { createInterface } from 'node:readline';
import { request as httpRequest } from 'node:http';
import type { IncomingMessage } from 'node:http';

// ── Types ──

interface TrafficEvent {
  timestamp: string;
  projectName: string;
  sessionId: string;
  eventType: string;
  method?: string;
  upstreamName?: string;
  body: unknown;
  durationMs?: number;
}

interface ActiveSession {
  sessionId: string;
  projectName: string;
  startedAt: string;
  eventCount: number;
}

interface JsonRpcRequest {
  jsonrpc: string;
  id: string | number;
  method: string;
  params?: Record<string, unknown>;
}

// ── State ──

const sessions = new Map<string, ActiveSession>();
const events: TrafficEvent[] = [];
const MAX_EVENTS = 10000;
let mcplocalBaseUrl = 'http://localhost:3200';

// ── SSE Client ──

function connectSSE(url: string): void {
  const parsed = new URL(url);

  const req = httpRequest(
    {
      hostname: parsed.hostname,
      port: parsed.port,
      path: parsed.pathname + parsed.search,
      headers: { Accept: 'text/event-stream' },
    },
    (res: IncomingMessage) => {
      let buffer = '';
      let currentEventType = 'message';

      res.setEncoding('utf-8');
      res.on('data', (chunk: string) => {
        buffer += chunk;
        const lines = buffer.split('\n');
        buffer = lines.pop()!;

        for (const line of lines) {
          if (line.startsWith('event: ')) {
            currentEventType = line.slice(7).trim();
          } else if (line.startsWith('data: ')) {
            try {
              const data = JSON.parse(line.slice(6));
              if (currentEventType === 'sessions') {
                for (const s of data as Array<{ sessionId: string; projectName: string; startedAt: string }>) {
                  sessions.set(s.sessionId, { ...s, eventCount: 0 });
                }
              } else if (currentEventType !== 'live') {
                handleEvent(data as TrafficEvent);
              }
            } catch {
              // ignore
            }
            currentEventType = 'message';
          }
        }
      });

      res.on('end', () => {
        // Reconnect after 2s
        setTimeout(() => connectSSE(url), 2000);
      });

      res.on('error', () => {
        setTimeout(() => connectSSE(url), 2000);
      });
    },
  );

  req.on('error', () => {
    setTimeout(() => connectSSE(url), 2000);
  });

  req.end();
}

function handleEvent(event: TrafficEvent): void {
  events.push(event);
  if (events.length > MAX_EVENTS) {
    events.splice(0, events.length - MAX_EVENTS);
  }

  // Track sessions
  if (event.eventType === 'session_created') {
    sessions.set(event.sessionId, {
      sessionId: event.sessionId,
      projectName: event.projectName,
      startedAt: event.timestamp,
      eventCount: 0,
    });
  } else if (event.eventType === 'session_closed') {
    sessions.delete(event.sessionId);
  }

  // Increment event count
  const session = sessions.get(event.sessionId);
  if (session) {
    session.eventCount++;
  }
}

// ── MCP Protocol Handlers ──

const TOOLS = [
  {
    name: 'list_sessions',
    description: 'List all active MCP sessions with their project name, start time, and event count.',
    inputSchema: {
      type: 'object' as const,
      properties: {
        project: { type: 'string' as const, description: 'Filter by project name' },
      },
    },
  },
  {
    name: 'get_traffic',
    description: 'Get captured MCP traffic events. Returns recent events, optionally filtered by session, method, or event type.',
    inputSchema: {
      type: 'object' as const,
      properties: {
        sessionId: { type: 'string' as const, description: 'Filter by session ID (first 8 chars is enough)' },
        method: { type: 'string' as const, description: 'Filter by JSON-RPC method (e.g. "tools/call", "initialize")' },
        eventType: { type: 'string' as const, description: 'Filter by event type: client_request, client_response, client_notification, upstream_request, upstream_response' },
        limit: { type: 'number' as const, description: 'Max events to return (default: 50)' },
        offset: { type: 'number' as const, description: 'Skip first N matching events' },
      },
    },
  },
  {
    name: 'get_session_info',
    description: 'Get detailed information about a specific session including its recent traffic summary.',
    inputSchema: {
      type: 'object' as const,
      properties: {
        sessionId: { type: 'string' as const, description: 'Session ID (first 8 chars is enough)' },
      },
      required: ['sessionId'] as const,
    },
  },
  // ── Studio tools (task 109) ──
  {
    name: 'list_models',
    description: 'List all available proxymodels (YAML pipelines and TypeScript plugins).',
    inputSchema: { type: 'object' as const, properties: {} },
  },
  {
    name: 'list_stages',
    description: 'List all available pipeline stages (built-in and custom).',
    inputSchema: { type: 'object' as const, properties: {} },
  },
  {
    name: 'switch_model',
    description: 'Hot-swap the active proxymodel on a running project. Optionally target a specific server.',
    inputSchema: {
      type: 'object' as const,
      properties: {
        project: { type: 'string' as const, description: 'Project name' },
        proxyModel: { type: 'string' as const, description: 'ProxyModel name to switch to' },
        serverName: { type: 'string' as const, description: 'Optional: target a specific server instead of project-wide' },
      },
      required: ['project', 'proxyModel'] as const,
    },
  },
  {
    name: 'get_model_info',
    description: 'Get detailed info about a specific proxymodel (stages, hooks, config).',
    inputSchema: {
      type: 'object' as const,
      properties: {
        name: { type: 'string' as const, description: 'ProxyModel name' },
      },
      required: ['name'] as const,
    },
  },
  {
    name: 'reload_stages',
    description: 'Force reload all custom stages from ~/.mcpctl/stages/. Use after editing stage files.',
    inputSchema: { type: 'object' as const, properties: {} },
  },
  {
    name: 'pause',
    description: 'Toggle pause mode. When paused, pipeline results are held in a queue for inspection/editing before being sent to the client.',
    inputSchema: {
      type: 'object' as const,
      properties: {
        paused: { type: 'boolean' as const, description: 'true to pause, false to resume (releases all queued items)' },
      },
      required: ['paused'] as const,
    },
  },
  {
    name: 'get_pause_queue',
    description: 'List all items currently held in the pause queue. Each item shows original and transformed content.',
    inputSchema: { type: 'object' as const, properties: {} },
  },
  {
    name: 'release_paused',
    description: 'Release a paused item (send transformed content to client), edit it (send custom content), or drop it (send empty).',
    inputSchema: {
      type: 'object' as const,
      properties: {
        id: { type: 'string' as const, description: 'Item ID from pause queue' },
        action: { type: 'string' as const, description: 'Action: "release", "edit", or "drop"' },
        content: { type: 'string' as const, description: 'Required for "edit" action: the modified content to send' },
      },
      required: ['id', 'action'] as const,
    },
  },
];

function handleInitialize(id: string | number): void {
  send({
    jsonrpc: '2.0',
    id,
    result: {
      protocolVersion: '2024-11-05',
      serverInfo: { name: 'mcpctl-inspector', version: '1.0.0' },
      capabilities: { tools: {} },
    },
  });
}

function handleToolsList(id: string | number): void {
  send({ jsonrpc: '2.0', id, result: { tools: TOOLS } });
}

// ── HTTP helpers for mcplocal API calls ──

function fetchApi<T>(path: string, method = 'GET', body?: unknown): Promise<T> {
  return new Promise((resolve, reject) => {
    const url = new URL(`${mcplocalBaseUrl}${path}`);
    const payload = body !== undefined ? JSON.stringify(body) : undefined;
    const req = httpRequest(
      {
        hostname: url.hostname,
        port: url.port,
        path: url.pathname + url.search,
        method,
        headers: payload ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) } : {},
        timeout: 10_000,
      },
      (res) => {
        const chunks: Buffer[] = [];
        res.on('data', (chunk: Buffer) => chunks.push(chunk));
        res.on('end', () => {
          try {
            resolve(JSON.parse(Buffer.concat(chunks).toString()) as T);
          } catch {
            reject(new Error(`Invalid JSON from ${path}`));
          }
        });
      },
    );
    req.on('error', (err) => reject(err));
    req.on('timeout', () => { req.destroy(); reject(new Error(`Timeout: ${path}`)); });
    if (payload) req.write(payload);
    req.end();
  });
}

function sendText(id: string | number, text: string): void {
  send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }] } });
}

function sendError(id: string | number, message: string): void {
  send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: message }], isError: true } });
}

async function handleToolsCall(id: string | number, params: { name: string; arguments?: Record<string, unknown> }): Promise<void> {
  const args = params.arguments ?? {};

  switch (params.name) {
    case 'list_sessions': {
      let result = [...sessions.values()];
      const project = args['project'] as string | undefined;
      if (project) {
        result = result.filter((s) => s.projectName === project);
      }
      sendText(id, JSON.stringify(result, null, 2));
      break;
    }

    case 'get_traffic': {
      const sessionFilter = args['sessionId'] as string | undefined;
      const methodFilter = args['method'] as string | undefined;
      const typeFilter = args['eventType'] as string | undefined;
      const limit = (args['limit'] as number | undefined) ?? 50;
      const offset = (args['offset'] as number | undefined) ?? 0;

      let filtered = events;
      if (sessionFilter) {
        filtered = filtered.filter((e) => e.sessionId.startsWith(sessionFilter));
      }
      if (methodFilter) {
        filtered = filtered.filter((e) => e.method === methodFilter);
      }
      if (typeFilter) {
        filtered = filtered.filter((e) => e.eventType === typeFilter);
      }

      const sliced = filtered.slice(offset, offset + limit);

      const lines = sliced.map((e) => {
        const arrow = e.eventType === 'client_request' ? '→'
          : e.eventType === 'client_response' ? '←'
          : e.eventType === 'client_notification' ? '◂'
          : e.eventType === 'upstream_request' ? '⇢'
          : e.eventType === 'upstream_response' ? '⇠'
          : e.eventType === 'session_created' ? '●'
          : e.eventType === 'session_closed' ? '○'
          : '?';
        const layer = e.eventType.startsWith('upstream') ? 'internal' : 'client';
        const ms = e.durationMs !== undefined ? ` (${e.durationMs}ms)` : '';
        const upstream = e.upstreamName ? `${e.upstreamName}/` : '';
        const time = e.timestamp.split('T')[1]?.replace('Z', '') ?? e.timestamp;

        const body = e.body as Record<string, unknown> | null;
        let content = '';
        if (body) {
          if (e.eventType.includes('request') || e.eventType === 'client_notification') {
            const p = body['params'] as Record<string, unknown> | undefined;
            if (e.method === 'tools/call' && p) {
              const toolArgs = p['arguments'] as Record<string, unknown> | undefined;
              content = `tool=${p['name']}${toolArgs ? ` args=${JSON.stringify(toolArgs)}` : ''}`;
            } else if (e.method === 'resources/read' && p) {
              content = `uri=${p['uri']}`;
            } else if (e.method === 'initialize' && p) {
              const ci = p['clientInfo'] as Record<string, unknown> | undefined;
              content = ci ? `client=${ci['name']} v${ci['version']}` : '';
            } else if (p && Object.keys(p).length > 0) {
              content = JSON.stringify(p);
            }
          } else if (e.eventType.includes('response')) {
            const result = body['result'] as Record<string, unknown> | undefined;
            const error = body['error'] as Record<string, unknown> | undefined;
            if (error) {
              content = `ERROR ${error['code']}: ${error['message']}`;
            } else if (result) {
              if (e.method === 'tools/list') {
                const tools = (result['tools'] ?? []) as Array<{ name: string }>;
                content = `${tools.length} tools: ${tools.map((t) => t.name).join(', ')}`;
              } else if (e.method === 'resources/list') {
                const res = (result['resources'] ?? []) as Array<{ name: string }>;
                content = `${res.length} resources: ${res.map((r) => r.name).join(', ')}`;
              } else if (e.method === 'tools/call') {
                const c = (result['content'] ?? []) as Array<{ text?: string }>;
                const text = c[0]?.text ?? '';
                content = text.length > 200 ? text.slice(0, 200) + '…' : text;
              } else if (e.method === 'initialize') {
                const si = result['serverInfo'] as Record<string, unknown> | undefined;
                content = si ? `server=${si['name']} v${si['version']}` : '';
              } else if (Object.keys(result).length > 0) {
                const s = JSON.stringify(result);
                content = s.length > 200 ? s.slice(0, 200) + '…' : s;
              }
            }
          }
        }

        return `${time} ${arrow} [${layer}] ${upstream}${e.method ?? e.eventType}${ms}${content ? '  ' + content : ''}`;
      });

      sendText(id, `${filtered.length} total events (showing ${offset + 1}-${offset + sliced.length})\n\n${lines.join('\n')}`);
      break;
    }

    case 'get_session_info': {
      const sid = args['sessionId'] as string;
      const session = [...sessions.values()].find((s) => s.sessionId.startsWith(sid));
      if (!session) {
        sendError(id, `Session not found: ${sid}`);
        return;
      }

      const sessionEvents = events.filter((e) => e.sessionId === session.sessionId);
      const methods = new Map<string, number>();
      for (const e of sessionEvents) {
        if (e.method) {
          methods.set(e.method, (methods.get(e.method) ?? 0) + 1);
        }
      }

      const info = {
        ...session,
        totalEvents: sessionEvents.length,
        methodCounts: Object.fromEntries(methods),
        lastEvent: sessionEvents.length > 0
          ? sessionEvents[sessionEvents.length - 1]!.timestamp
          : null,
      };

      sendText(id, JSON.stringify(info, null, 2));
      break;
    }

    // ── Studio tools ──

    case 'list_models': {
      try {
        const models = await fetchApi<unknown[]>('/proxymodels');
        sendText(id, JSON.stringify(models, null, 2));
      } catch (err) {
        sendError(id, `Failed to list models: ${err instanceof Error ? err.message : String(err)}`);
      }
      break;
    }

    case 'list_stages': {
      try {
        const stages = await fetchApi<unknown[]>('/proxymodels/stages');
        sendText(id, JSON.stringify(stages, null, 2));
      } catch {
        // Fallback: stages endpoint may not exist yet, list from models
        sendError(id, 'Stages endpoint not available. Check mcplocal version.');
      }
      break;
    }

    case 'switch_model': {
      const project = args['project'] as string;
      const proxyModel = args['proxyModel'] as string;
      const serverName = args['serverName'] as string | undefined;
      if (!project || !proxyModel) {
        sendError(id, 'project and proxyModel are required');
        return;
      }
      try {
        const body: Record<string, string> = serverName
          ? { serverName, serverProxyModel: proxyModel }
          : { proxyModel };
        const result = await fetchApi<unknown>(`/projects/${encodeURIComponent(project)}/override`, 'PUT', body);
        sendText(id, `Switched to ${proxyModel}${serverName ? ` on ${serverName}` : ' (project-wide)'}.\n\n${JSON.stringify(result, null, 2)}`);
      } catch (err) {
        sendError(id, `Failed to switch model: ${err instanceof Error ? err.message : String(err)}`);
      }
      break;
    }

    case 'get_model_info': {
      const name = args['name'] as string;
      if (!name) {
        sendError(id, 'name is required');
        return;
      }
      try {
        const info = await fetchApi<unknown>(`/proxymodels/${encodeURIComponent(name)}`);
        sendText(id, JSON.stringify(info, null, 2));
      } catch (err) {
        sendError(id, `Failed to get model info: ${err instanceof Error ? err.message : String(err)}`);
      }
      break;
    }

    case 'reload_stages': {
      try {
        const result = await fetchApi<unknown>('/proxymodels/reload', 'POST');
        sendText(id, `Stages reloaded.\n\n${JSON.stringify(result, null, 2)}`);
      } catch {
        sendError(id, 'Reload endpoint not available. Check mcplocal version.');
      }
      break;
    }

    case 'pause': {
      const paused = args['paused'] as boolean;
      if (typeof paused !== 'boolean') {
        sendError(id, 'paused must be a boolean');
        return;
      }
      try {
        const result = await fetchApi<{ paused: boolean; queueSize: number }>('/pause', 'PUT', { paused });
        sendText(id, paused
          ? `Paused. Pipeline results will be held for inspection. Queue size: ${result.queueSize}`
          : `Resumed. Released ${result.queueSize} queued items.`);
      } catch (err) {
        sendError(id, `Failed to toggle pause: ${err instanceof Error ? err.message : String(err)}`);
      }
      break;
    }

    case 'get_pause_queue': {
      try {
        const result = await fetchApi<{ paused: boolean; items: Array<{ id: string; sourceName: string; contentType: string; original: string; transformed: string; timestamp: number }> }>('/pause/queue');
        if (result.items.length === 0) {
          sendText(id, `Pause mode: ${result.paused ? 'ON' : 'OFF'}. Queue is empty.`);
        } else {
          const lines = result.items.map((item, i) => {
            const age = Math.round((Date.now() - item.timestamp) / 1000);
            const origLen = item.original.length;
            const transLen = item.transformed.length;
            const preview = item.transformed.length > 200 ? item.transformed.slice(0, 200) + '...' : item.transformed;
            return `[${i + 1}] id=${item.id}\n    source: ${item.sourceName} (${item.contentType})\n    original: ${origLen} chars → transformed: ${transLen} chars (${age}s ago)\n    preview: ${preview}`;
          });
          sendText(id, `Pause mode: ${result.paused ? 'ON' : 'OFF'}. ${result.items.length} item(s) queued:\n\n${lines.join('\n\n')}`);
        }
      } catch (err) {
        sendError(id, `Failed to get pause queue: ${err instanceof Error ? err.message : String(err)}`);
      }
      break;
    }

    case 'release_paused': {
      const itemId = args['id'] as string;
      const action = args['action'] as string;
      if (!itemId || !action) {
        sendError(id, 'id and action are required');
        return;
      }
      try {
        if (action === 'release') {
          await fetchApi<unknown>(`/pause/queue/${encodeURIComponent(itemId)}/release`, 'POST');
          sendText(id, `Released item ${itemId} with transformed content.`);
        } else if (action === 'edit') {
          const content = args['content'] as string;
          if (typeof content !== 'string') {
            sendError(id, 'content is required for edit action');
            return;
          }
          await fetchApi<unknown>(`/pause/queue/${encodeURIComponent(itemId)}/edit`, 'POST', { content });
          sendText(id, `Edited and released item ${itemId} with custom content (${content.length} chars).`);
        } else if (action === 'drop') {
          await fetchApi<unknown>(`/pause/queue/${encodeURIComponent(itemId)}/drop`, 'POST');
          sendText(id, `Dropped item ${itemId}. Empty content sent to client.`);
        } else {
          sendError(id, `Unknown action: ${action}. Use "release", "edit", or "drop".`);
        }
      } catch (err) {
        sendError(id, `Failed to ${action} item: ${err instanceof Error ? err.message : String(err)}`);
      }
      break;
    }

    default:
      send({
        jsonrpc: '2.0',
        id,
        error: { code: -32601, message: `Unknown tool: ${params.name}` },
      });
  }
}

async function handleRequest(request: JsonRpcRequest): Promise<void> {
  switch (request.method) {
    case 'initialize':
      handleInitialize(request.id);
      break;
    case 'notifications/initialized':
      // Notification — no response
      break;
    case 'tools/list':
      handleToolsList(request.id);
      break;
    case 'tools/call':
      await handleToolsCall(request.id, request.params as { name: string; arguments?: Record<string, unknown> });
      break;
    default:
      if (request.id !== undefined) {
        send({
          jsonrpc: '2.0',
          id: request.id,
          error: { code: -32601, message: `Method not supported: ${request.method}` },
        });
      }
  }
}

function send(message: unknown): void {
  process.stdout.write(JSON.stringify(message) + '\n');
}

// ── Entrypoint ──

export async function runInspectMcp(mcplocalUrl: string): Promise<void> {
  mcplocalBaseUrl = mcplocalUrl.replace(/\/$/, '');
  const inspectUrl = `${mcplocalBaseUrl}/inspect`;
  connectSSE(inspectUrl);

  const rl = createInterface({ input: process.stdin });

  for await (const line of rl) {
    const trimmed = line.trim();
    if (!trimmed) continue;

    try {
      const request = JSON.parse(trimmed) as JsonRpcRequest;
      await handleRequest(request);
    } catch {
      // Ignore unparseable lines
    }
  }
}
