/**
 * MCP protocol session — wraps HTTP transport with typed methods.
 *
 * Every request/response is logged via the onLog callback so
 * the console UI can display raw JSON-RPC traffic.
 */

import { postJsonRpc, sendDelete, extractJsonRpcMessages } from '../mcp.js';

export interface LogEntry {
  timestamp: Date;
  direction: 'request' | 'response' | 'error';
  method?: string;
  body: unknown;
}

export interface McpTool {
  name: string;
  description?: string;
  inputSchema?: Record<string, unknown>;
}

export interface McpResource {
  uri: string;
  name?: string;
  description?: string;
  mimeType?: string;
}

export interface McpPrompt {
  name: string;
  description?: string;
  arguments?: Array<{ name: string; description?: string; required?: boolean }>;
}

export interface InitializeResult {
  protocolVersion: string;
  serverInfo: { name: string; version: string };
  capabilities: Record<string, unknown>;
  instructions?: string;
}

export interface CallToolResult {
  content: Array<{ type: string; text?: string }>;
  isError?: boolean;
}

export interface ReadResourceResult {
  contents: Array<{ uri: string; mimeType?: string; text?: string }>;
}

export class McpSession {
  private sessionId?: string;
  private nextId = 1;
  private log: LogEntry[] = [];

  onLog?: (entry: LogEntry) => void;

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

  getSessionId(): string | undefined {
    return this.sessionId;
  }

  getLog(): LogEntry[] {
    return this.log;
  }

  async initialize(): Promise<InitializeResult> {
    const request = {
      jsonrpc: '2.0',
      id: this.nextId++,
      method: 'initialize',
      params: {
        protocolVersion: '2024-11-05',
        capabilities: {},
        clientInfo: { name: 'mcpctl-console', version: '1.0.0' },
      },
    };

    const result = await this.send(request);

    // Send initialized notification
    const notification = {
      jsonrpc: '2.0',
      method: 'notifications/initialized',
    };
    await this.sendNotification(notification);

    return result as InitializeResult;
  }

  async listTools(): Promise<McpTool[]> {
    const result = await this.send({
      jsonrpc: '2.0',
      id: this.nextId++,
      method: 'tools/list',
      params: {},
    }) as { tools: McpTool[] };
    return result.tools ?? [];
  }

  async callTool(name: string, args: Record<string, unknown>): Promise<CallToolResult> {
    return await this.send({
      jsonrpc: '2.0',
      id: this.nextId++,
      method: 'tools/call',
      params: { name, arguments: args },
    }) as CallToolResult;
  }

  async listResources(): Promise<McpResource[]> {
    const result = await this.send({
      jsonrpc: '2.0',
      id: this.nextId++,
      method: 'resources/list',
      params: {},
    }) as { resources: McpResource[] };
    return result.resources ?? [];
  }

  async readResource(uri: string): Promise<ReadResourceResult> {
    return await this.send({
      jsonrpc: '2.0',
      id: this.nextId++,
      method: 'resources/read',
      params: { uri },
    }) as ReadResourceResult;
  }

  async listPrompts(): Promise<McpPrompt[]> {
    const result = await this.send({
      jsonrpc: '2.0',
      id: this.nextId++,
      method: 'prompts/list',
      params: {},
    }) as { prompts: McpPrompt[] };
    return result.prompts ?? [];
  }

  async getPrompt(name: string, args?: Record<string, unknown>): Promise<unknown> {
    return await this.send({
      jsonrpc: '2.0',
      id: this.nextId++,
      method: 'prompts/get',
      params: { name, arguments: args ?? {} },
    });
  }

  async sendRaw(json: string): Promise<string> {
    this.addLog('request', undefined, JSON.parse(json));

    const result = await postJsonRpc(this.endpointUrl, json, this.sessionId, this.token);

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

    const messages = extractJsonRpcMessages(result.headers['content-type'], result.body);
    const combined = messages.join('\n');

    for (const msg of messages) {
      try {
        this.addLog('response', undefined, JSON.parse(msg));
      } catch {
        this.addLog('response', undefined, msg);
      }
    }

    return combined;
  }

  async close(): Promise<void> {
    if (this.sessionId) {
      await sendDelete(this.endpointUrl, this.sessionId, this.token);
      this.sessionId = undefined;
    }
  }

  private async send(request: Record<string, unknown>): Promise<unknown> {
    const method = request.method as string;
    this.addLog('request', method, request);

    const body = JSON.stringify(request);
    let result;
    try {
      result = await postJsonRpc(this.endpointUrl, body, this.sessionId, this.token);
    } catch (err) {
      this.addLog('error', method, { error: err instanceof Error ? err.message : String(err) });
      throw err;
    }

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

    const messages = extractJsonRpcMessages(result.headers['content-type'], result.body);
    const firstMsg = messages[0];
    if (!firstMsg) {
      throw new Error(`Empty response for ${method}`);
    }

    const parsed = JSON.parse(firstMsg) as { result?: unknown; error?: { code: number; message: string } };
    this.addLog('response', method, parsed);

    if (parsed.error) {
      throw new Error(`MCP error ${parsed.error.code}: ${parsed.error.message}`);
    }

    return parsed.result;
  }

  private async sendNotification(notification: Record<string, unknown>): Promise<void> {
    const body = JSON.stringify(notification);
    this.addLog('request', notification.method as string, notification);
    try {
      await postJsonRpc(this.endpointUrl, body, this.sessionId, this.token);
    } catch {
      // Notifications are fire-and-forget
    }
  }

  private addLog(direction: LogEntry['direction'], method: string | undefined, body: unknown): void {
    const entry: LogEntry = { timestamp: new Date(), direction, method, body };
    this.log.push(entry);
    this.onLog?.(entry);
  }
}
