/**
 * Stdio transport for MCP server.
 * Reads JSON-RPC messages from stdin (one per line), writes responses to stdout.
 * When stdin closes (EOF), the process exits — this is the expected stdio lifecycle.
 */

import * as readline from 'readline';
import { MCPResponse, MCPErrorCodes } from '../types/mcp';
import { MCPTransport, TransportMessageContext } from './index';
import { shutdownSyncBestEffort } from '../utils/sync-shutdown';

export class StdioTransport implements MCPTransport {
  private rl: readline.Interface | null = null;
  private messageHandler:
    | ((msg: Record<string, unknown>, signal?: AbortSignal, context?: TransportMessageContext) => Promise<MCPResponse | null>)
    | null = null;

  onMessage(
    handler: (msg: Record<string, unknown>, signal?: AbortSignal, context?: TransportMessageContext) => Promise<MCPResponse | null>,
  ): void {
    this.messageHandler = handler;
  }

  send(response: MCPResponse): void {
    // stdout is the MCP JSON-RPC channel in stdio mode
    console.log(JSON.stringify(response));
  }

  start(): void {
    this.rl = readline.createInterface({
      input: process.stdin,
      // Do NOT set output to process.stdout — stdout is the MCP JSON-RPC channel.
      // Setting it risks protocol corruption if readline writes internally (prompts, echoes).
      terminal: false,
    });

    this.rl.on('line', (line) => {
      if (!line.trim()) return;

      let parsed: Record<string, unknown>;
      try {
        parsed = JSON.parse(line) as Record<string, unknown>;
      } catch (error) {
        const errorResponse: MCPResponse = {
          jsonrpc: '2.0',
          id: 0,
          error: {
            code: MCPErrorCodes.PARSE_ERROR,
            message: error instanceof Error ? error.message : 'Parse error',
          },
        };
        this.send(errorResponse);
        return;
      }

      if (!this.messageHandler) {
        console.error('[StdioTransport] No message handler registered, dropping message');
        return;
      }

      this.messageHandler(parsed)
        .then((response) => {
          if (response) {
            this.send(response);
          }
        })
        .catch((error) => {
          const id = (parsed.id as string | number) ?? 0;
          const errorResponse: MCPResponse = {
            jsonrpc: '2.0',
            id,
            error: {
              code: MCPErrorCodes.INTERNAL_ERROR,
              message: error instanceof Error ? error.message : 'Internal error',
            },
          };
          this.send(errorResponse);
        });
    });

    this.rl.on('close', () => {
      console.error('[StdioTransport] stdin closed (readline), shutting down...');
      // #661 Phase 2: synchronous best-effort kill before process.exit so we
      // don't orphan Chrome when the parent agent disconnects.
      try { shutdownSyncBestEffort(); } catch { /* never throw at exit */ }
      process.exit(0);
    });

    // Belt-and-suspenders: monitor stdin directly for EOF/error.
    // When a parent process dies without cleanly closing the pipe,
    // readline may not fire 'close'. Listening on the raw stream
    // catches these edge cases.
    process.stdin.on('end', () => {
      console.error('[StdioTransport] stdin ended, shutting down...');
      try { shutdownSyncBestEffort(); } catch { /* never throw at exit */ }
      process.exit(0);
    });
    process.stdin.on('error', () => {
      console.error('[StdioTransport] stdin error, shutting down...');
      try { shutdownSyncBestEffort(); } catch { /* never throw at exit */ }
      process.exit(0);
    });
  }

  async close(): Promise<void> {
    if (this.rl) {
      this.rl.close();
      this.rl = null;
    }
  }
}
