'use strict';
const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const { EventEmitter } = require('events');
const {
  McpPhase,
  defaultMcpLifecycleRegistry,
} = require('../coding/mcp-lifecycle');

const CLAUDE_DIR = path.join(process.env.HOME, '.claude');

// Cache of active MCP connections
const connections = new Map();

/**
 * Read all MCP server configs from Claude Code.
 */
function loadMcpConfigs() {
  const configs = {};

  // From settings.json (HTTP servers)
  try {
    const settings = JSON.parse(fs.readFileSync(path.join(CLAUDE_DIR, 'settings.json'), 'utf8'));
    for (const [name, cfg] of Object.entries(settings.mcpServers || {})) {
      configs[name] = { ...cfg, name };
    }
  } catch {}

  // From .claude.json (global MCP servers — Glean, Mezmo, etc.)
  try {
    const claudeJson = JSON.parse(fs.readFileSync(path.join(process.env.HOME, '.claude.json'), 'utf8'));
    for (const [name, cfg] of Object.entries(claudeJson.mcpServers || {})) {
      if (!configs[name]) configs[name] = { ...cfg, name };
    }
  } catch {}

  // From mcp.json (stdio servers)
  try {
    const mcp = JSON.parse(fs.readFileSync(path.join(CLAUDE_DIR, 'mcp.json'), 'utf8'));
    for (const [name, cfg] of Object.entries(mcp.mcpServers || mcp)) {
      if (name === 'mcpServers') continue;
      configs[name] = { ...cfg, name };
    }
  } catch {}

  // From .credentials.json — OAuth-authenticated MCP servers (Glean, Slack, Mezmo, etc.)
  // Re-read every time since Claude Code may refresh tokens
  try {
    const creds = JSON.parse(fs.readFileSync(path.join(CLAUDE_DIR, '.credentials.json'), 'utf8'));
    const mcpOAuth = creds.mcpOAuth || {};
    for (const [key, entry] of Object.entries(mcpOAuth)) {
      const serverName = entry.serverName || key.split('|')[0];
      // Skip if already configured from settings.json (but allow credential override)
      if (configs[serverName] && !configs[serverName]._fromCredentials) {
        // Update token on existing config if we have a fresher one
        if (entry.accessToken && entry.serverUrl) {
          configs[serverName].oauth = {
            accessToken: entry.accessToken,
            refreshToken: entry.refreshToken,
            expiresAt: entry.expiresAt,
          };
        }
        continue;
      }
      // Only add servers that have a URL and a valid (non-expired) access token
      const isExpired = entry.expiresAt && entry.expiresAt < Date.now();
      if (entry.serverUrl && entry.accessToken && !isExpired) {
        configs[serverName] = {
          name: serverName,
          type: 'http',
          url: entry.serverUrl,
          oauth: {
            accessToken: entry.accessToken,
            refreshToken: entry.refreshToken,
            expiresAt: entry.expiresAt,
          },
          _fromCredentials: true,
        };
      }
    }
  } catch {}

  for (const [name, config] of Object.entries(configs)) {
    try { defaultMcpLifecycleRegistry.registerConfig(name, config); } catch {}
  }
  return configs;
}

/**
 * MCP stdio transport — spawns a process and communicates via JSON-RPC over stdin/stdout
 */
class StdioTransport extends EventEmitter {
  constructor(config) {
    super();
    this.config = config;
    this.process = null;
    this.buffer = '';
    this.nextId = 1;
    this.pending = new Map();
    this.initialized = false;
  }

  async connect() {
    const env = { ...process.env, ...(this.config.env || {}) };
    this.process = spawn(this.config.command, this.config.args || [], {
      env,
      stdio: ['pipe', 'pipe', 'pipe'],
      cwd: process.env.HOME,
    });

    this.process.stdout.on('data', (data) => {
      this.buffer += data.toString();
      this._processBuffer();
    });

    this.process.stderr.on('data', (data) => {
      // MCP servers may log to stderr — ignore
    });

    this.process.on('error', (err) => {
      console.error(`[mcp-client] Process error for ${this.config.name}:`, err.message);
    });

    this.process.on('exit', (code) => {
      this.initialized = false;
      // Reject all pending requests
      for (const [id, { reject }] of this.pending) {
        reject(new Error(`MCP server ${this.config.name} exited with code ${code}`));
      }
      this.pending.clear();
    });

    // Initialize
    await this._send('initialize', {
      protocolVersion: '2024-11-05',
      capabilities: {},
      clientInfo: { name: 'wall-e', version: '0.1.0' },
    });

    // Send initialized notification (no id, no response expected)
    this._write({ jsonrpc: '2.0', method: 'notifications/initialized' });
    this.initialized = true;
  }

  async _send(method, params) {
    return new Promise((resolve, reject) => {
      const id = this.nextId++;
      const timeout = setTimeout(() => {
        this.pending.delete(id);
        reject(new Error(`MCP request ${method} timed out after 30s`));
      }, 30000);

      this.pending.set(id, {
        resolve: (result) => { clearTimeout(timeout); resolve(result); },
        reject: (err) => { clearTimeout(timeout); reject(err); },
      });

      this._write({ jsonrpc: '2.0', id, method, params });
    });
  }

  _write(msg) {
    if (!this.process || !this.process.stdin.writable) {
      throw new Error(`MCP server ${this.config.name} not connected`);
    }
    this.process.stdin.write(JSON.stringify(msg) + '\n');
  }

  _processBuffer() {
    const lines = this.buffer.split('\n');
    this.buffer = lines.pop() || '';
    for (const line of lines) {
      if (!line.trim()) continue;
      try {
        const msg = JSON.parse(line);
        if (msg.id && this.pending.has(msg.id)) {
          const { resolve, reject } = this.pending.get(msg.id);
          this.pending.delete(msg.id);
          if (msg.error) {
            reject(new Error(msg.error.message || JSON.stringify(msg.error)));
          } else {
            resolve(msg.result);
          }
        }
      } catch {}
    }
  }

  async listTools() {
    const result = await this._send('tools/list', {});
    return result.tools || [];
  }

  async callTool(name, args) {
    const result = await this._send('tools/call', { name, arguments: args });
    return result;
  }

  disconnect() {
    if (this.process) {
      this.process.kill();
      this.process = null;
    }
    this.initialized = false;
  }
}

/**
 * MCP HTTP transport — sends JSON-RPC over HTTP POST
 */
class HttpTransport {
  constructor(config) {
    this.config = config;
    this.url = config.url;
    this.nextId = 1;
    this.initialized = false;
    this.sessionUrl = null;
    // Start with static headers from config (e.g. Mezmo, Rootly bearer tokens)
    this.authHeaders = config.headers ? { ...config.headers } : {};
  }

  async connect() {
    // Try to load OAuth token if configured
    if (this.config.oauth) {
      this._loadOAuthToken();
    }
    // Also try loading a previously saved token from our own storage
    if (!this.authHeaders['Authorization']) {
      this._loadSavedToken();
    }

    try {
      // Initialize
      const result = await this._send('initialize', {
        protocolVersion: '2024-11-05',
        capabilities: {},
        clientInfo: { name: 'wall-e', version: '0.1.0' },
      });

      // Send initialized notification
      await this._sendNotification('notifications/initialized');
      this.initialized = true;
      return result;
    } catch (err) {
      // If 401, try to discover OAuth and re-throw with auth info
      if (err.message && err.message.includes('401')) {
        const oauthMeta = await this._discoverOAuth();
        if (oauthMeta) {
          err.oauthDiscovered = true;
          err.oauthMeta = oauthMeta;
        }
      }
      throw err;
    }
  }

  _loadSavedToken() {
    try {
      // Check slack-mcp.js token for Slack servers (setup page uses this path)
      if (this.config.url && this.config.url.includes('mcp.slack.com')) {
        const slackTokenPath = path.join(CLAUDE_DIR, 'wall-e-slack-token.json');
        if (fs.existsSync(slackTokenPath)) {
          const data = JSON.parse(fs.readFileSync(slackTokenPath, 'utf8'));
          if (data.access_token) {
            this.authHeaders['Authorization'] = 'Bearer ' + data.access_token;
            return;
          }
        }
      }
      const tokenPath = path.join(CLAUDE_DIR, 'wall-e-mcp-tokens', this.config.name + '.json');
      if (fs.existsSync(tokenPath)) {
        const data = JSON.parse(fs.readFileSync(tokenPath, 'utf8'));
        if (data.access_token && (!data.expires_at || data.expires_at > Date.now())) {
          this.authHeaders['Authorization'] = 'Bearer ' + data.access_token;
        }
      }
    } catch {}
  }

  async _discoverOAuth() {
    try {
      // Standard MCP OAuth discovery: /.well-known/oauth-authorization-server at the server's origin
      const urlObj = new URL(this.url);
      const discoveryUrl = urlObj.origin + '/.well-known/oauth-authorization-server';
      const res = await fetch(discoveryUrl, { signal: AbortSignal.timeout(5000) });
      if (res.ok) {
        const meta = await res.json();
        return {
          authorization_endpoint: meta.authorization_endpoint,
          token_endpoint: meta.token_endpoint,
          registration_endpoint: meta.registration_endpoint,
          scopes_supported: meta.scopes_supported,
          issuer: meta.issuer,
        };
      }
    } catch {}
    return null;
  }

  _loadOAuthToken() {
    // First check if the config already has an OAuth token (from .credentials.json)
    if (this.config.oauth && this.config.oauth.accessToken) {
      this.authHeaders['Authorization'] = `Bearer ${this.config.oauth.accessToken}`;
      return;
    }
    // Fallback: try file-based token storage
    const tokenPaths = [
      path.join(CLAUDE_DIR, '.mcp-auth', this.config.name, 'tokens.json'),
      path.join(CLAUDE_DIR, 'mcp-tokens', this.config.name + '.json'),
    ];
    for (const p of tokenPaths) {
      try {
        if (fs.existsSync(p)) {
          const tokens = JSON.parse(fs.readFileSync(p, 'utf8'));
          if (tokens.access_token) {
            this.authHeaders['Authorization'] = `Bearer ${tokens.access_token}`;
            return;
          }
        }
      } catch {}
    }
  }

  async _send(method, params) {
    const id = this.nextId++;
    const body = { jsonrpc: '2.0', id, method };
    if (params) body.params = params;

    const res = await fetch(this.sessionUrl || this.url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json, text/event-stream',
        ...this.authHeaders,
      },
      body: JSON.stringify(body),
      signal: AbortSignal.timeout(30000),
    });

    // Check for session URL in Mcp-Session header
    const sessionHeader = res.headers.get('mcp-session');
    if (sessionHeader) this.sessionUrl = this.url;

    if (!res.ok) {
      const text = await res.text();
      throw new Error(`MCP HTTP error ${res.status}: ${text.slice(0, 200)}`);
    }

    const contentType = res.headers.get('content-type') || '';

    // Handle SSE responses
    if (contentType.includes('text/event-stream')) {
      const text = await res.text();
      // Parse SSE for the result
      for (const line of text.split('\n')) {
        if (line.startsWith('data: ')) {
          try {
            const data = JSON.parse(line.slice(6));
            if (data.id === id) {
              if (data.error) throw new Error(data.error.message || JSON.stringify(data.error));
              return data.result;
            }
          } catch (e) {
            if (e.message.includes('MCP')) throw e;
          }
        }
      }
      return null;
    }

    // Handle JSON response
    const data = await res.json();
    if (data.error) throw new Error(data.error.message || JSON.stringify(data.error));
    return data.result;
  }

  async _sendNotification(method) {
    try {
      await fetch(this.sessionUrl || this.url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', ...this.authHeaders },
        body: JSON.stringify({ jsonrpc: '2.0', method }),
        signal: AbortSignal.timeout(5000),
      });
    } catch {}
  }

  async listTools() {
    const result = await this._send('tools/list', {});
    return result?.tools || [];
  }

  async callTool(name, args) {
    return this._send('tools/call', { name, arguments: args });
  }

  disconnect() {
    this.initialized = false;
    this.sessionUrl = null;
  }
}

/**
 * Get or create a connection to an MCP server.
 */
async function getConnection(serverName) {
  if (connections.has(serverName)) {
    const conn = connections.get(serverName);
    if (conn.initialized) return conn;
    // Reconnect if disconnected
    connections.delete(serverName);
  }

  const configs = loadMcpConfigs();
  const config = configs[serverName];
  if (!config) throw new Error(`MCP server "${serverName}" not found in config`);
  const lifecycle = defaultMcpLifecycleRegistry.get(serverName, config);

  let transport;
  if ((config.type === 'http' || config.type === 'sse') && config.url) {
    transport = new HttpTransport(config);
  } else if (config.command) {
    transport = new StdioTransport(config);
  } else {
    const err = new Error(`Unsupported MCP server config for "${serverName}"`);
    lifecycle.fail(McpPhase.SERVER_REGISTRATION, err, { recoverable: false });
    throw err;
  }

  lifecycle.enter(McpPhase.SPAWN_CONNECT, { type: config.type || (config.command ? 'stdio' : 'http') });
  try {
    lifecycle.enter(McpPhase.INITIALIZE_HANDSHAKE);
    await transport.connect();
    lifecycle.ready();
  } catch (err) {
    lifecycle.fail(McpPhase.INITIALIZE_HANDSHAKE, err);
    throw err;
  }
  connections.set(serverName, transport);
  return transport;
}

/**
 * List all available MCP tools across all configured servers.
 */
async function listAllTools() {
  const configs = loadMcpConfigs();
  const allTools = [];

  for (const [name, config] of Object.entries(configs)) {
    const lifecycle = defaultMcpLifecycleRegistry.get(name, config);
    try {
      const conn = await getConnection(name);
      lifecycle.enter(McpPhase.TOOL_DISCOVERY);
      const tools = await conn.listTools();
      lifecycle.setTools(tools);
      lifecycle.ready({ toolCount: tools.length });
      for (const tool of tools) {
        allTools.push({ ...tool, server: name });
      }
    } catch (err) {
      lifecycle.fail(McpPhase.TOOL_DISCOVERY, err);
      console.error(`[mcp-client] Failed to list tools from ${name}:`, err.message);
    }
  }

  return allTools;
}

/**
 * Call an MCP tool on a specific server.
 */
async function callMcpTool(serverName, toolName, args) {
  const conn = await getConnection(serverName);
  const lifecycle = defaultMcpLifecycleRegistry.get(serverName);
  lifecycle.enter(McpPhase.INVOCATION, { toolName });
  try {
    return await conn.callTool(toolName, args);
  } catch (err) {
    lifecycle.fail(McpPhase.INVOCATION, err, { toolName });
    throw err;
  }
}

/**
 * Disconnect all MCP servers.
 */
function disconnectAll() {
  for (const [name, conn] of connections) {
    const lifecycle = defaultMcpLifecycleRegistry.get(name);
    try {
      lifecycle.enter(McpPhase.SHUTDOWN);
      conn.disconnect();
      lifecycle.enter(McpPhase.CLEANUP);
    } catch (err) {
      lifecycle.fail(McpPhase.CLEANUP, err);
    }
  }
  connections.clear();
}

function getActiveConnections() {
  return connections;
}

function getMcpLifecycleReport() {
  return defaultMcpLifecycleRegistry.report();
}

/**
 * Run OAuth flow for an MCP server: discover metadata, register client,
 * open browser for auth, handle callback, save token.
 */
async function authenticateMcpServer(serverName) {
  const configs = loadMcpConfigs();
  const config = configs[serverName];
  if (!config) throw new Error('MCP server "' + serverName + '" not found');

  // Slack uses its own OAuth flow — delegate to slack-mcp.js
  if (config.url && config.url.includes('mcp.slack.com')) {
    const slackMcp = require('../tools/slack-mcp');
    return slackMcp.authenticate();
  }

  // Discover OAuth metadata
  const urlObj = new URL(config.url);
  const discoveryUrl = urlObj.origin + '/.well-known/oauth-authorization-server';
  const discRes = await fetch(discoveryUrl, { signal: AbortSignal.timeout(5000) });
  if (!discRes.ok) throw new Error('No OAuth discovery at ' + discoveryUrl);
  const meta = await discRes.json();

  if (!meta.authorization_endpoint || !meta.token_endpoint) {
    throw new Error('OAuth metadata missing endpoints');
  }

  // Dynamic client registration (if supported)
  let clientId, clientSecret;
  if (meta.registration_endpoint) {
    const regRes = await fetch(meta.registration_endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        client_name: 'Wall-E AI Agent',
        redirect_uris: ['http://localhost:3119/callback'],
        grant_types: ['authorization_code'],
        response_types: ['code'],
        token_endpoint_auth_method: 'none',
      }),
      signal: AbortSignal.timeout(10000),
    });
    if (regRes.ok) {
      const reg = await regRes.json();
      clientId = reg.client_id;
      clientSecret = reg.client_secret;
    }
  }
  // Fallback: use configured clientId if dynamic registration fails
  if (!clientId && config.oauth?.clientId) {
    clientId = config.oauth.clientId;
  }
  if (!clientId) throw new Error('No client_id available (dynamic registration failed and none configured)');

  // PKCE challenge
  const crypto = require('crypto');
  const codeVerifier = crypto.randomBytes(32).toString('base64url');
  const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
  const state = crypto.randomBytes(16).toString('hex');

  // Determine scopes
  const scopes = (meta.scopes_supported || []).filter(function(s) {
    return ['mcp', 'openid', 'search', 'people', 'chat', 'offline_access'].includes(s);
  }).join(' ') || 'mcp';

  const CALLBACK_PORT = 3119;

  return new Promise(function(resolve, reject) {
    const http = require('http');
    const timeout = setTimeout(function() {
      server.close();
      reject(new Error('OAuth timeout (2 minutes)'));
    }, 120000);

    const server = http.createServer(async function(req, res) {
      const reqUrl = new URL(req.url, 'http://localhost:' + CALLBACK_PORT);
      if (reqUrl.pathname !== '/callback') { res.writeHead(404); res.end(); return; }

      const code = reqUrl.searchParams.get('code');
      const retState = reqUrl.searchParams.get('state');
      if (retState !== state) { res.writeHead(400); res.end('Invalid state'); clearTimeout(timeout); server.close(); reject(new Error('OAuth state mismatch')); return; }

      try {
        // Exchange code for token
        const tokenBody = new URLSearchParams({
          grant_type: 'authorization_code',
          code: code,
          redirect_uri: 'http://localhost:' + CALLBACK_PORT + '/callback',
          client_id: clientId,
          code_verifier: codeVerifier,
        });
        if (clientSecret) tokenBody.set('client_secret', clientSecret);

        const tokenRes = await fetch(meta.token_endpoint, {
          method: 'POST',
          headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
          body: tokenBody.toString(),
        });

        if (!tokenRes.ok) {
          const errText = await tokenRes.text();
          throw new Error('Token exchange failed: ' + errText.slice(0, 200));
        }

        const tokenData = await tokenRes.json();

        // Save token
        const tokenDir = path.join(CLAUDE_DIR, 'wall-e-mcp-tokens');
        if (!fs.existsSync(tokenDir)) fs.mkdirSync(tokenDir, { recursive: true });
        fs.writeFileSync(path.join(tokenDir, serverName + '.json'), JSON.stringify({
          access_token: tokenData.access_token,
          refresh_token: tokenData.refresh_token,
          expires_at: tokenData.expires_in ? Date.now() + tokenData.expires_in * 1000 : null,
          obtained_at: new Date().toISOString(),
          server: serverName,
        }, null, 2));

        res.writeHead(200, { 'Content-Type': 'text/html' });
        res.end('<html><body style="font-family:system-ui;text-align:center;padding:40px"><h2>Wall-E connected to ' + serverName + '!</h2><p>You can close this tab.</p></body></html>');

        clearTimeout(timeout);
        server.close();
        resolve(tokenData.access_token);
      } catch (err) {
        res.writeHead(500);
        res.end('Token exchange failed: ' + err.message);
        clearTimeout(timeout);
        server.close();
        reject(err);
      }
    });

    server.listen(CALLBACK_PORT, function() {
      const authUrl = meta.authorization_endpoint
        + '?response_type=code'
        + '&client_id=' + encodeURIComponent(clientId)
        + '&redirect_uri=' + encodeURIComponent('http://localhost:' + CALLBACK_PORT + '/callback')
        + '&scope=' + encodeURIComponent(scopes)
        + '&state=' + state
        + '&code_challenge=' + codeChallenge
        + '&code_challenge_method=S256';

      console.log('[mcp-client] Opening browser for ' + serverName + ' OAuth...');
      const { execFile } = require('child_process');
      execFile('open', [authUrl], function(err) {
        if (err) console.error('[mcp-client] Failed to open browser:', err.message);
      });
    });

    server.on('error', function(err) {
      clearTimeout(timeout);
      reject(new Error('OAuth server failed: ' + err.message));
    });
  });
}

module.exports = {
  loadMcpConfigs,
  getConnection,
  getActiveConnections,
  getMcpLifecycleReport,
  listAllTools,
  callMcpTool,
  authenticateMcpServer,
  disconnectAll,
  StdioTransport,
  HttpTransport,
};
