/**
 * Lightweight MCP HTTP client for smoke tests.
 * Sends JSON-RPC messages to mcplocal's HTTP endpoint and parses SSE responses.
 */
import http from 'node:http';

export interface McpResponse {
  status: number;
  sessionId?: string;
  messages: unknown[];
}

const MCPLOCAL_URL = process.env.MCPLOCAL_URL ?? 'http://localhost:3200';
const MCPD_URL = process.env.MCPD_URL ?? 'http://localhost:3100';

export function getMcplocalUrl(): string {
  return MCPLOCAL_URL;
}

export function getMcpdUrl(): string {
  return MCPD_URL;
}

function httpRequest(opts: {
  url: string;
  method: string;
  headers?: Record<string, string>;
  body?: string;
  timeout?: number;
}): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: string }> {
  return new Promise((resolve, reject) => {
    const parsed = new URL(opts.url);
    const req = http.request(
      {
        hostname: parsed.hostname,
        port: parsed.port,
        path: parsed.pathname + parsed.search,
        method: opts.method,
        headers: opts.headers,
        timeout: opts.timeout ?? 30_000,
      },
      (res) => {
        const chunks: Buffer[] = [];
        res.on('data', (chunk: Buffer) => chunks.push(chunk));
        res.on('end', () => {
          resolve({
            status: res.statusCode ?? 0,
            headers: res.headers,
            body: Buffer.concat(chunks).toString('utf-8'),
          });
        });
      },
    );
    req.on('error', reject);
    req.on('timeout', () => {
      req.destroy();
      reject(new Error('Request timed out'));
    });
    if (opts.body) req.write(opts.body);
    req.end();
  });
}

function parseSSE(body: string): unknown[] {
  const messages: unknown[] = [];
  for (const line of body.split('\n')) {
    if (line.startsWith('data: ')) {
      try {
        messages.push(JSON.parse(line.slice(6)));
      } catch {
        // skip
      }
    }
  }
  return messages;
}

/**
 * MCP session for smoke tests.
 * Manages session ID and sends JSON-RPC requests.
 */
export class SmokeMcpSession {
  private sessionId?: string;
  private nextId = 1;

  constructor(
    private readonly projectName: string,
    private readonly token?: string,
  ) {}

  get endpoint(): string {
    return `${MCPLOCAL_URL}/projects/${encodeURIComponent(this.projectName)}/mcp`;
  }

  async send(method: string, params: Record<string, unknown> = {}, timeout?: number): Promise<unknown> {
    const id = this.nextId++;
    const request = { jsonrpc: '2.0', id, method, params };

    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
      'Accept': 'application/json, text/event-stream',
    };
    if (this.sessionId) headers['mcp-session-id'] = this.sessionId;
    if (this.token) headers['Authorization'] = `Bearer ${this.token}`;

    const result = await httpRequest({
      url: this.endpoint,
      method: 'POST',
      headers,
      body: JSON.stringify(request),
      timeout,
    });

    // Capture session ID
    if (!this.sessionId) {
      const sid = result.headers['mcp-session-id'];
      if (typeof sid === 'string') this.sessionId = sid;
    }

    // Handle HTTP-level errors (e.g. 502 for nonexistent project)
    if (result.status >= 400) {
      let errorMsg = `HTTP ${result.status}`;
      try {
        const body = JSON.parse(result.body) as { error?: string };
        if (body.error) errorMsg = body.error;
      } catch {
        errorMsg = `HTTP ${result.status}: ${result.body.slice(0, 200)}`;
      }
      throw new Error(errorMsg);
    }

    // Parse response — handle SSE with multiple messages (notifications + response)
    const messages = result.headers['content-type']?.includes('text/event-stream')
      ? parseSSE(result.body)
      : [JSON.parse(result.body)];

    // Find the response matching our request ID (skip notifications)
    const response = messages.find((m) => {
      const msg = m as { id?: unknown };
      return msg.id === id;
    }) as { result?: unknown; error?: { code: number; message: string } } | undefined;

    // Fall back to first message if no ID match (e.g. error responses)
    const parsed = response ?? messages[0] as { result?: unknown; error?: { code: number; message: string } } | undefined;
    if (!parsed) throw new Error(`No response for ${method}`);
    if (parsed.error) throw new Error(`MCP error ${parsed.error.code}: ${parsed.error.message}`);
    return parsed.result;
  }

  async initialize(): Promise<unknown> {
    return this.send('initialize', {
      protocolVersion: '2024-11-05',
      capabilities: {},
      clientInfo: { name: 'mcpctl-smoke-test', version: '1.0.0' },
    });
  }

  async sendNotification(method: string, params: Record<string, unknown> = {}): Promise<void> {
    const notification = { jsonrpc: '2.0', method, params };
    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
      'Accept': 'application/json, text/event-stream',
    };
    if (this.sessionId) headers['mcp-session-id'] = this.sessionId;
    if (this.token) headers['Authorization'] = `Bearer ${this.token}`;

    await httpRequest({
      url: this.endpoint,
      method: 'POST',
      headers,
      body: JSON.stringify(notification),
    }).catch(() => {});
  }

  async listTools(): Promise<Array<{ name: string; description?: string; inputSchema?: unknown }>> {
    const result = await this.send('tools/list') as { tools: Array<{ name: string; description?: string; inputSchema?: unknown }> };
    return result.tools ?? [];
  }

  async callTool(name: string, args: Record<string, unknown> = {}, timeout?: number): Promise<{ content: Array<{ type: string; text?: string }>; isError?: boolean }> {
    return await this.send('tools/call', { name, arguments: args }, timeout) as { content: Array<{ type: string; text?: string }>; isError?: boolean };
  }

  async close(): Promise<void> {
    if (this.sessionId) {
      const headers: Record<string, string> = { 'mcp-session-id': this.sessionId };
      if (this.token) headers['Authorization'] = `Bearer ${this.token}`;
      await httpRequest({
        url: this.endpoint,
        method: 'DELETE',
        headers,
        timeout: 5_000,
      }).catch(() => {});
      this.sessionId = undefined;
    }
  }
}

/**
 * Check if mcplocal is reachable.
 */
export async function isMcplocalRunning(): Promise<boolean> {
  try {
    const result = await httpRequest({
      url: `${MCPLOCAL_URL}/health`,
      method: 'GET',
      timeout: 3_000,
    });
    return result.status < 500;
  } catch {
    return false;
  }
}

/**
 * Run an mcpctl CLI command and return stdout.
 */
export function mcpctl(args: string): Promise<string> {
  const { execSync } = require('node:child_process') as typeof import('node:child_process');
  try {
    return Promise.resolve(execSync(`mcpctl ${args}`, { encoding: 'utf-8', timeout: 30_000 }).trim());
  } catch (err) {
    const e = err as { stderr?: string; stdout?: string };
    return Promise.reject(new Error(e.stderr ?? e.stdout ?? String(err)));
  }
}
