import { formatGeneratedCode } from './format';
import type { ExportConfig } from './types';

function sanitizeComment(value: string): string {
  return value.replace(/\*\//g, '* /');
}

function generateToolDefinitions(config: ExportConfig): string {
  return config.tools
    .map(
      (tool) => `  {
    name: ${JSON.stringify(tool.name)},
    description: ${JSON.stringify(tool.description)},
    inputSchema: ${JSON.stringify(tool.inputSchema, null, 6).replace(/\n/g, '\n    ')},
    method: ${JSON.stringify(tool.method)},
    path: ${JSON.stringify(tool.path)},
  }`,
    )
    .join(',\n');
}

function generateAuthHeaders(config: ExportConfig): string {
  switch (config.authMode) {
    case 'api-key':
      return `
  const apiKey = process.env['API_KEY'];
  if (!apiKey) {
    throw new Error('API_KEY environment variable is required');
  }
  return { 'x-api-key': apiKey };`;
    case 'bearer':
      return `
  const token = process.env['API_TOKEN'];
  if (!token) {
    throw new Error('API_TOKEN environment variable is required');
  }
  return { Authorization: \`Bearer \${token}\` };`;
    case 'none':
    default:
      return `
  return {};`;
  }
}

export function generateStandaloneServer(config: ExportConfig): string {
  const code = `/**
 * Standalone MCP Server: ${sanitizeComment(config.name)}
 * Slug: ${sanitizeComment(config.slug)}
 * Generated by ApiFold
 *
 * Usage:
 *   npm install express cors express-rate-limit
 *   npx tsc server.ts --esModuleInterop --module commonjs --target es2020
 *   node server.js
 *
 * Environment variables:
 *   PORT          — Server port (default: 3001)
 *   BASE_URL      — Upstream API base URL
 *   CORS_ORIGIN   — Required. Allowed CORS origin (e.g. "http://localhost:3000" or your domain)
 *   RATE_LIMIT_MAX — Max requests per 15-minute window (default: 100)
 *   API_KEY / API_TOKEN — Upstream API credentials (depending on auth mode)
 */

import express from 'express';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
import { randomUUID } from 'node:crypto';

const PORT = Number(process.env['PORT'] ?? 3001);
const BASE_URL = process.env['BASE_URL'] ?? ${JSON.stringify(config.baseUrl)};

interface Tool {
  readonly name: string;
  readonly description: string;
  readonly inputSchema: Record<string, unknown>;
  readonly method: string;
  readonly path: string;
}

interface JsonRpcRequest {
  readonly jsonrpc: '2.0';
  readonly id: string | number;
  readonly method: string;
  readonly params?: Record<string, unknown>;
}

interface Session {
  readonly id: string;
  readonly res: express.Response;
}

const tools: readonly Tool[] = [
${generateToolDefinitions(config)}
];

function getAuthHeaders(): Record<string, string> {${generateAuthHeaders(config)}
}

const sessions = new Map<string, Session>();

function sendEvent(session: Session, event: string, data: string): void {
  session.res.write(\`event: \${event}\\ndata: \${data}\\n\\n\`);
}

async function executeTool(
  toolName: string,
  args: Record<string, unknown>,
): Promise<unknown> {
  const tool = tools.find((t) => t.name === toolName);
  if (!tool) {
    throw new Error(\`Unknown tool: \${toolName}\`);
  }

  let path = tool.path;
  const queryParams = new URLSearchParams();
  const bodyParams: Record<string, unknown> = {};

  for (const [key, value] of Object.entries(args)) {
    if (path.includes(\`{\${key}}\`)) {
      path = path.replace(\`{\${key}}\`, encodeURIComponent(String(value)));
    } else if (tool.method === 'GET' || tool.method === 'DELETE') {
      queryParams.append(key, String(value));
    } else {
      bodyParams[key] = value;
    }
  }

  const url = new URL(\`\${BASE_URL}\${path}\`);
  if (queryParams.toString()) {
    url.search = queryParams.toString();
  }

  const authHeaders = getAuthHeaders();
  const options: RequestInit = {
    method: tool.method,
    headers: {
      'Content-Type': 'application/json',
      ...authHeaders,
    },
  };

  if (tool.method !== 'GET' && tool.method !== 'DELETE') {
    options.body = JSON.stringify(bodyParams);
  }

  const response = await fetch(url, { ...options, signal: AbortSignal.timeout(30_000) });
  if (!response.ok) {
    const text = await response.text();
    const safeMessage = text.length > 500 ? text.slice(0, 500) + '...' : text;
    throw new Error(\`Upstream error (\${response.status}): \${safeMessage}\`);
  }

  return response.json();
}

async function handleMessage(
  session: Session,
  message: JsonRpcRequest,
): Promise<void> {
  const { id, method, params } = message;

  switch (method) {
    case 'initialize': {
      sendEvent(session, 'message', JSON.stringify({
        jsonrpc: '2.0',
        id,
        result: {
          protocolVersion: '2024-11-05',
          capabilities: { tools: {} },
          serverInfo: { name: ${JSON.stringify(config.name)}, version: '1.0.0' },
        },
      }));
      break;
    }

    case 'tools/list': {
      sendEvent(session, 'message', JSON.stringify({
        jsonrpc: '2.0',
        id,
        result: {
          tools: tools.map((t) => ({
            name: t.name,
            description: t.description,
            inputSchema: t.inputSchema,
          })),
        },
      }));
      break;
    }

    case 'tools/call': {
      const toolName = (params as { name?: string })?.name;
      const toolArgs = (params as { arguments?: Record<string, unknown> })?.arguments ?? {};

      if (!toolName) {
        sendEvent(session, 'message', JSON.stringify({
          jsonrpc: '2.0',
          id,
          error: { code: -32602, message: 'Missing tool name' },
        }));
        break;
      }

      try {
        const result = await executeTool(toolName, toolArgs);
        sendEvent(session, 'message', JSON.stringify({
          jsonrpc: '2.0',
          id,
          result: {
            content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
          },
        }));
      } catch (err) {
        const errorMessage = err instanceof Error ? err.message : 'Unknown error';
        sendEvent(session, 'message', JSON.stringify({
          jsonrpc: '2.0',
          id,
          result: {
            content: [{ type: 'text', text: errorMessage }],
            isError: true,
          },
        }));
      }
      break;
    }

    case 'notifications/initialized':
    case 'ping': {
      if (method === 'ping') {
        sendEvent(session, 'message', JSON.stringify({
          jsonrpc: '2.0',
          id,
          result: {},
        }));
      }
      break;
    }

    default: {
      sendEvent(session, 'message', JSON.stringify({
        jsonrpc: '2.0',
        id,
        error: { code: -32601, message: \`Method not found: \${method}\` },
      }));
    }
  }
}

const app = express();
const CORS_ORIGIN = process.env['CORS_ORIGIN'];
if (!CORS_ORIGIN) {
  throw new Error('CORS_ORIGIN environment variable is required. Set it to your client origin (e.g. "http://localhost:3000") or "*" for development only.');
}
app.use(cors({ origin: CORS_ORIGIN }));
app.use(rateLimit({
  windowMs: 15 * 60 * 1000,
  max: Number(process.env['RATE_LIMIT_MAX'] ?? 100),
  standardHeaders: true,
  legacyHeaders: false,
}));
app.use(express.json({ limit: '100kb' }));

app.get('/health', (_req, res) => {
  res.json({ status: 'ok' });
});

app.get('/sse', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.setHeader('X-Accel-Buffering', 'no');
  res.flushHeaders();

  const session: Session = { id: randomUUID(), res };
  sessions.set(session.id, session);

  sendEvent(session, 'endpoint', JSON.stringify({
    sessionId: session.id,
    url: '/message',
  }));

  req.on('close', () => {
    sessions.delete(session.id);
  });
});

app.post('/message', async (req, res) => {
  const sessionId = req.headers['x-session-id'] as string | undefined;
  if (!sessionId) {
    res.status(400).json({ error: 'Missing X-Session-ID header' });
    return;
  }

  const session = sessions.get(sessionId);
  if (!session) {
    res.status(404).json({ error: 'Session not found' });
    return;
  }

  try {
    await handleMessage(session, req.body as JsonRpcRequest);
    res.status(202).json({ ok: true });
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Internal error';
    res.status(500).json({ error: message });
  }
});

app.listen(PORT, () => {
  console.log(\`MCP Server "${sanitizeComment(config.name)}" listening on port \${PORT}\`);
});
`;

  return formatGeneratedCode(code);
}
