/**
 * Upfyn-Code MCP Server
 *
 * Exposes the app's capabilities as MCP tools and resources so any MCP-compatible
 * client (ChatGPT, Claude Desktop, Cursor, etc.) can control the app.
 *
 * Transport: Streamable HTTP mounted at /mcp on the Express server
 *
 * Tools:
 *   - send-prompt: Send a message to Claude and get a response
 *   - list-projects: List all available projects
 *   - list-sessions: List sessions for a project
 *   - get-session-messages: Get messages from a session
 *   - get-canvas-state: Get current canvas nodes
 *   - add-canvas-node: Add a node to the canvas
 *   - clear-canvas: Clear all canvas nodes
 *   - create-session: Start a new Claude session
 *   - abort-session: Stop an active session
 *   - read-file: Read a file from a project
 *   - list-files: List files in a project directory
 *
 * Resources:
 *   - upfynai://canvas/state: Current canvas state
 *   - upfynai://sessions/active: Active sessions list
 */

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { z } from 'zod';
import crypto from 'crypto';
import jwt from 'jsonwebtoken';
import { userDb, apiKeysDb, relayTokensDb } from './database/db.js';

import { IS_PLATFORM } from './constants/config.js';
const JWT_SECRET = process.env.JWT_SECRET?.trim() || (IS_PLATFORM ? crypto.randomBytes(32).toString('hex') : (() => { throw new Error('JWT_SECRET required'); })());

// In-memory canvas state (Excalidraw elements, synced via WebSocket with browser clients)
let canvasElements = [];
const canvasListeners = new Set();

export function getCanvasElements() {
  return canvasElements;
}

export function setCanvasElements(elements) {
  canvasElements = elements;
  notifyCanvasListeners();
}

export function addCanvasElement(element) {
  canvasElements.push(element);
  notifyCanvasListeners();
}

export function clearCanvas() {
  canvasElements = [];
  notifyCanvasListeners();
}

export function onCanvasChange(listener) {
  canvasListeners.add(listener);
  return () => canvasListeners.delete(listener);
}

function notifyCanvasListeners() {
  for (const listener of canvasListeners) {
    try { listener(canvasElements); } catch (e) { /* ignore */ }
  }
}

// Helper: create an Excalidraw rectangle + text element pair
function createExcalidrawNote(text, { x = 100, y = 100, width = 300, height = 100, label = '' } = {}) {
  const id = `el-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
  const textId = `${id}-text`;
  const fullText = label ? `${label}\n\n${text}` : text;

  const rect = {
    id,
    type: 'rectangle',
    x,
    y,
    width,
    height,
    strokeColor: '#a855f7',
    backgroundColor: '#1a1a2e',
    fillStyle: 'solid',
    strokeWidth: 2,
    roughness: 0,
    opacity: 100,
    angle: 0,
    groupIds: [],
    roundness: { type: 3 },
    boundElements: [{ id: textId, type: 'text' }],
    isDeleted: false,
    version: 1,
    versionNonce: Math.floor(Math.random() * 1e9),
  };

  const textEl = {
    id: textId,
    type: 'text',
    x: x + 10,
    y: y + 10,
    width: width - 20,
    height: height - 20,
    text: fullText,
    fontSize: 16,
    fontFamily: 1,
    textAlign: 'left',
    verticalAlign: 'top',
    containerId: id,
    originalText: fullText,
    isDeleted: false,
    version: 1,
    versionNonce: Math.floor(Math.random() * 1e9),
  };

  return [rect, textEl];
}

/**
 * Create and configure the MCP server with all tools and resources
 */
export function createMcpServer({ getProjects, getSessions, getSessionMessages, queryClaudeSDK, abortClaudeSDKSession, getActiveClaudeSDKSessions, connectedClients }) {
  const server = new McpServer({
    name: 'upfynai-code',
    version: '2.0.0',
  });

  // ═══════════════════════════════════════════
  // TOOLS
  // ═══════════════════════════════════════════

  // Send a prompt to Claude
  server.tool(
    'send-prompt',
    'Send a message to Claude and get a streaming response. The response will appear on the canvas and in chat.',
    {
      prompt: z.string().describe('The message to send to Claude'),
      projectPath: z.string().optional().describe('Project directory path for context'),
      sessionId: z.string().optional().describe('Session ID to resume, or omit for new session'),
      model: z.string().optional().describe('Model to use (e.g. claude-sonnet-4-5-20250929)'),
    },
    async ({ prompt, projectPath, sessionId, model }) => {
      return new Promise((resolve) => {
        const responseChunks = [];
        let resolved = false;

        // Create a mock WebSocket writer that collects the response
        const mockWs = {
          readyState: 1, // WebSocket.OPEN
          send: (data) => {
            try {
              const msg = typeof data === 'string' ? JSON.parse(data) : data;
              if (msg.type === 'claude-response' && msg.data?.text) {
                responseChunks.push(msg.data.text);
              }
              if (msg.type === 'claude-complete' && !resolved) {
                resolved = true;
                const fullText = responseChunks.join('');
                // Add to canvas as Excalidraw elements
                const yOffset = canvasElements.length * 60;
                const els = createExcalidrawNote(fullText, { y: 100 + yOffset, label: 'Claude' });
                els.forEach(el => addCanvasElement(el));
                // Broadcast canvas update to browser clients
                broadcastToClients(connectedClients, {
                  type: 'canvas-update',
                  elements: canvasElements,
                });
                resolve({
                  content: [{ type: 'text', text: fullText || 'No response received.' }],
                });
              }
              if (msg.type === 'error' && !resolved) {
                resolved = true;
                resolve({
                  content: [{ type: 'text', text: `Error: ${msg.error || 'Unknown error'}` }],
                  isError: true,
                });
              }
            } catch (e) { /* ignore parse errors */ }
          },
        };

        // Add user prompt to canvas as Excalidraw elements
        const userYOffset = canvasElements.length * 60;
        const userEls = createExcalidrawNote(prompt, { y: 100 + userYOffset, label: 'You (MCP)' });
        userEls.forEach(el => addCanvasElement(el));

        // Broadcast the user elements
        broadcastToClients(connectedClients, {
          type: 'canvas-update',
          elements: canvasElements,
        });

        const options = {
          projectPath: projectPath || process.cwd(),
          cwd: projectPath || process.cwd(),
          sessionId: sessionId || undefined,
          resume: Boolean(sessionId),
          model: model || undefined,
        };

        queryClaudeSDK(prompt, options, mockWs).catch((err) => {
          if (!resolved) {
            resolved = true;
            resolve({
              content: [{ type: 'text', text: `SDK Error: ${err.message}` }],
              isError: true,
            });
          }
        });

        // Safety timeout
        setTimeout(() => {
          if (!resolved) {
            resolved = true;
            const partial = responseChunks.join('');
            resolve({
              content: [{ type: 'text', text: partial || 'Response timed out after 5 minutes.' }],
            });
          }
        }, 300000);
      });
    }
  );

  // List projects
  server.tool(
    'list-projects',
    'List all available projects that Claude can work on',
    {},
    async () => {
      try {
        const projects = await getProjects();
        const summary = projects.map((p) => ({
          name: p.name,
          displayName: p.displayName || p.name,
          path: p.fullPath || p.path,
          sessions: (p.sessions || []).length,
        }));
        return {
          content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }],
        };
      } catch (err) {
        return {
          content: [{ type: 'text', text: `Error listing projects: ${err.message}` }],
          isError: true,
        };
      }
    }
  );

  // List sessions
  server.tool(
    'list-sessions',
    'List sessions for a project',
    {
      projectName: z.string().describe('Project name to list sessions for'),
      limit: z.number().optional().describe('Max sessions to return (default 10)'),
    },
    async ({ projectName, limit }) => {
      try {
        const sessions = await getSessions(projectName, limit || 10, 0);
        return {
          content: [{ type: 'text', text: JSON.stringify(sessions, null, 2) }],
        };
      } catch (err) {
        return {
          content: [{ type: 'text', text: `Error: ${err.message}` }],
          isError: true,
        };
      }
    }
  );

  // Get session messages
  server.tool(
    'get-session-messages',
    'Get message history from a specific session',
    {
      projectName: z.string().describe('Project name'),
      sessionId: z.string().describe('Session ID'),
      limit: z.number().optional().describe('Max messages (default 50)'),
    },
    async ({ projectName, sessionId, limit }) => {
      try {
        const messages = await getSessionMessages(projectName, sessionId, limit || 50, 0);
        return {
          content: [{ type: 'text', text: JSON.stringify(messages, null, 2) }],
        };
      } catch (err) {
        return {
          content: [{ type: 'text', text: `Error: ${err.message}` }],
          isError: true,
        };
      }
    }
  );

  // Get canvas state
  server.tool(
    'get-canvas-state',
    'Get the current Upfyn-Canvas state including all elements',
    {},
    async () => {
      return {
        content: [{
          type: 'text',
          text: JSON.stringify({
            elementCount: canvasElements.length,
            elements: canvasElements,
          }, null, 2),
        }],
      };
    }
  );

  // Add canvas node (creates Upfyn-Canvas rectangle + text)
  server.tool(
    'add-canvas-node',
    'Add a visual note to the Upfyn-Canvas whiteboard (rectangle with text)',
    {
      text: z.string().describe('Text content for the note'),
      label: z.string().optional().describe('Label for the note (default: "MCP Note")'),
      x: z.number().optional().describe('X position (default: 100)'),
      y: z.number().optional().describe('Y position (default: auto)'),
    },
    async ({ text, label, x, y }) => {
      const yPos = y ?? (100 + canvasElements.length * 60);
      const els = createExcalidrawNote(text, { x: x ?? 100, y: yPos, label: label || 'MCP Note' });
      els.forEach(el => addCanvasElement(el));
      broadcastToClients(connectedClients, {
        type: 'canvas-update',
        elements: canvasElements,
      });
      return {
        content: [{ type: 'text', text: `Note added to canvas: ${els[0].id}` }],
      };
    }
  );

  // Clear canvas
  server.tool(
    'clear-canvas',
    'Clear all elements from the Upfyn-Canvas whiteboard',
    {},
    async () => {
      clearCanvas();
      broadcastToClients(connectedClients, {
        type: 'canvas-update',
        elements: [],
      });
      return {
        content: [{ type: 'text', text: 'Canvas cleared.' }],
      };
    }
  );

  // Update full canvas scene
  server.tool(
    'update-canvas-scene',
    'Replace the entire Upfyn-Canvas with new elements',
    {
      elements: z.array(z.object({}).passthrough()).describe('Array of canvas elements'),
    },
    async ({ elements }) => {
      setCanvasElements(elements);
      broadcastToClients(connectedClients, {
        type: 'canvas-update',
        elements: canvasElements,
      });
      return {
        content: [{ type: 'text', text: `Canvas updated with ${elements.length} elements.` }],
      };
    }
  );

  // Get active sessions
  server.tool(
    'get-active-sessions',
    'Get list of currently active Claude sessions',
    {},
    async () => {
      try {
        const sessions = getActiveClaudeSDKSessions();
        return {
          content: [{ type: 'text', text: JSON.stringify(sessions, null, 2) }],
        };
      } catch (err) {
        return {
          content: [{ type: 'text', text: `Error: ${err.message}` }],
          isError: true,
        };
      }
    }
  );

  // Abort session
  server.tool(
    'abort-session',
    'Stop an active Claude session',
    {
      sessionId: z.string().describe('Session ID to abort'),
    },
    async ({ sessionId }) => {
      try {
        const result = await abortClaudeSDKSession(sessionId);
        return {
          content: [{ type: 'text', text: result ? `Session ${sessionId} aborted.` : `Session ${sessionId} not found or already stopped.` }],
        };
      } catch (err) {
        return {
          content: [{ type: 'text', text: `Error: ${err.message}` }],
          isError: true,
        };
      }
    }
  );

  // ═══════════════════════════════════════════
  // RESOURCES
  // ═══════════════════════════════════════════

  server.resource(
    'canvas-state',
    'upfynai://canvas/state',
    {
      description: 'Current Upfyn-Canvas state with all elements',
      mimeType: 'application/json',
    },
    async (uri) => ({
      contents: [{
        uri: uri.href,
        text: JSON.stringify({ elementCount: canvasElements.length, elements: canvasElements }, null, 2),
      }],
    })
  );

  server.resource(
    'active-sessions',
    'upfynai://sessions/active',
    {
      description: 'Currently active Claude sessions',
      mimeType: 'application/json',
    },
    async (uri) => ({
      contents: [{
        uri: uri.href,
        text: JSON.stringify(getActiveClaudeSDKSessions(), null, 2),
      }],
    })
  );

  return server;
}

/**
 * Mount the MCP server on an Express app at /mcp
 */
export async function mountMcpServer(app, mcpServer, mcpServerFactory) {
  // On Vercel serverless, in-memory session state is lost between invocations.
  // Use per-request stateless MCP servers on Vercel; session-based on local.
  const isServerless = !!process.env.VERCEL;
  const transports = new Map();

  // MCP authentication middleware — cookie → Bearer → API key → query param
  const authenticateMcp = async (req, res, next) => {
    // 1. Try httpOnly cookie (browser sessions)
    if (req.cookies?.session) {
      try {
        const decoded = jwt.verify(req.cookies.session, JWT_SECRET);
        const user = await userDb.getUserById(decoded.userId);
        if (user) { req.user = user; return next(); }
      } catch (e) { /* fall through */ }
    }

    // 2. Try Bearer token — supports JWT, relay token (upfyn_/rt_), or API key (up-cli-)
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];
    if (token) {
      // 2a. Relay token (upfyn_xxx or legacy rt_xxx) — same token used for CLI connect
      if (token.startsWith('upfyn_') || token.startsWith('rt_')) {
        try {
          const tokenData = await relayTokensDb.validateToken(token);
          if (tokenData) {
            const user = await userDb.getUserById(tokenData.user_id);
            if (user) { req.user = user; return next(); }
          }
        } catch (e) { /* fall through */ }
      }
      // 2b. API key (up-cli-xxx)
      if (token.startsWith('up-cli-')) {
        try {
          const user = await apiKeysDb.validateApiKey(token);
          if (user) { req.user = user; return next(); }
        } catch (e) { /* fall through */ }
      }
      // 2c. JWT token
      try {
        const decoded = jwt.verify(token, JWT_SECRET);
        const user = await userDb.getUserById(decoded.userId);
        if (user) { req.user = user; return next(); }
      } catch (e) { /* fall through */ }
    }

    // 3. Try API key header (MCP clients: ChatGPT, Claude Desktop, Cursor, etc.)
    const apiKey = req.headers['x-api-key'];
    if (apiKey) {
      try {
        const user = await apiKeysDb.validateApiKey(apiKey);
        if (user) { req.user = user; return next(); }
      } catch (e) { /* ignore */ }
    }

    // 4. Try query param token (SSE EventSource fallback)
    if (req.query.token) {
      try {
        const decoded = jwt.verify(req.query.token, JWT_SECRET);
        const user = await userDb.getUserById(decoded.userId);
        if (user) { req.user = user; return next(); }
      } catch (e) { /* ignore */ }
    }

    res.status(401).json({ error: 'Authentication required. Use Authorization: Bearer <jwt>, x-api-key header, or up-cli- API key.' });
  };

  // Handle MCP requests at /mcp endpoint
  app.post('/mcp', authenticateMcp, async (req, res) => {
    try {
      const sessionId = req.headers['mcp-session-id'];
      let transport;

      if (!isServerless && sessionId && transports.has(sessionId)) {
        // Local: reuse existing session transport
        transport = transports.get(sessionId);
      } else {
        // Create a fresh MCP server + transport per request on serverless,
        // or a new session on local
        const server = isServerless && mcpServerFactory ? mcpServerFactory() : mcpServer;

        // On serverless: no sessionIdGenerator → disables session validation
        // entirely, so requests don't need the initialize handshake.
        // On local: sessionIdGenerator enables session tracking.
        transport = new StreamableHTTPServerTransport({
          sessionIdGenerator: isServerless ? undefined : (() => crypto.randomUUID()),
          stateless: isServerless,
        });

        transport.onclose = () => {
          const sid = transport.sessionId;
          if (sid) transports.delete(sid);
        };

        await server.connect(transport);

        if (!isServerless && transport.sessionId) {
          transports.set(transport.sessionId, transport);
        }
      }

      // Express already consumed the raw body stream, so we must pass the
      // parsed body as the 3rd arg to avoid "Parse error: Invalid JSON"
      await transport.handleRequest(req, res, req.body);
    } catch (err) {
      console.error('[MCP] Error handling POST:', err.message);
      if (!res.headersSent) {
        res.status(500).json({ error: 'MCP server error', details: err.message });
      }
    }
  });

  // SSE stream for server-initiated messages (local only — serverless is stateless)
  app.get('/mcp', authenticateMcp, async (req, res) => {
    if (isServerless) {
      return res.status(405).json({ error: 'SSE not supported on serverless. Use stateless POST requests.' });
    }
    const sessionId = req.headers['mcp-session-id'];
    if (!sessionId || !transports.has(sessionId)) {
      res.status(400).json({ error: 'Invalid or missing session ID' });
      return;
    }
    const transport = transports.get(sessionId);
    await transport.handleRequest(req, res);
  });

  // Session termination
  app.delete('/mcp', authenticateMcp, async (req, res) => {
    if (isServerless) {
      return res.json({ ok: true });
    }
    const sessionId = req.headers['mcp-session-id'];
    if (sessionId && transports.has(sessionId)) {
      const transport = transports.get(sessionId);
      await transport.handleRequest(req, res);
      transports.delete(sessionId);
    } else {
      res.status(404).json({ error: 'Session not found' });
    }
  });

  console.log(`${c_info('[MCP]')} MCP server mounted at /mcp (${isServerless ? 'stateless/serverless' : 'session-based'})`);
  console.log(`${c_info('[MCP]')} Tools: send-prompt, list-projects, list-sessions, get-session-messages, get-canvas-state, add-canvas-node, clear-canvas, update-canvas-scene, get-active-sessions, abort-session`);
}

// Simple color helper (avoids importing from index.js)
function c_info(text) {
  return `\x1b[36m${text}\x1b[0m`;
}

function broadcastToClients(clients, message) {
  if (!clients || clients.size === 0) return;
  const data = JSON.stringify(message);
  for (const client of clients) {
    try {
      if (client.readyState === 1) {
        client.send(data);
      }
    } catch (e) { /* ignore */ }
  }
}
