#!/usr/bin/env node
/**
 * Fixed HTTP server for n8n-MCP that properly handles StreamableHTTPServerTransport initialization
 * This implementation ensures the transport is properly initialized before handling requests
 */
import express from 'express';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { n8nDocumentationToolsFinal } from './mcp/tools';
import { n8nManagementTools } from './mcp/tools-n8n-manager';
import { N8NDocumentationMCPServer } from './mcp/server';
import { logger } from './utils/logger';
import { AuthManager } from './utils/auth';
import { PROJECT_VERSION } from './utils/version';
import { isN8nApiConfigured } from './config/n8n-api';
import dotenv from 'dotenv';
import { readFileSync } from 'fs';
import { getStartupBaseUrl, formatEndpointUrls, detectBaseUrl } from './utils/url-detector';
import { 
  negotiateProtocolVersion, 
  logProtocolNegotiation,
  N8N_PROTOCOL_VERSION 
} from './utils/protocol-version';

dotenv.config();

let expressServer: any;
let authToken: string | null = null;

/**
 * Load auth token from environment variable or file
 */
export function loadAuthToken(): string | null {
  // First, try AUTH_TOKEN environment variable
  if (process.env.AUTH_TOKEN) {
    logger.info('Using AUTH_TOKEN from environment variable');
    return process.env.AUTH_TOKEN;
  }
  
  // Then, try AUTH_TOKEN_FILE
  if (process.env.AUTH_TOKEN_FILE) {
    try {
      const token = readFileSync(process.env.AUTH_TOKEN_FILE, 'utf-8').trim();
      logger.info(`Loaded AUTH_TOKEN from file: ${process.env.AUTH_TOKEN_FILE}`);
      return token;
    } catch (error) {
      logger.error(`Failed to read AUTH_TOKEN_FILE: ${process.env.AUTH_TOKEN_FILE}`, error);
      console.error(`ERROR: Failed to read AUTH_TOKEN_FILE: ${process.env.AUTH_TOKEN_FILE}`);
      console.error(error instanceof Error ? error.message : 'Unknown error');
      return null;
    }
  }
  
  return null;
}

/**
 * Validate required environment variables
 */
function validateEnvironment() {
  // Load auth token from env var or file
  authToken = loadAuthToken();
  
  if (!authToken || authToken.trim() === '') {
    logger.error('No authentication token found or token is empty');
    console.error('ERROR: AUTH_TOKEN is required for HTTP mode and cannot be empty');
    console.error('Set AUTH_TOKEN environment variable or AUTH_TOKEN_FILE pointing to a file containing the token');
    console.error('Generate AUTH_TOKEN with: openssl rand -base64 32');
    process.exit(1);
  }
  
  // Update authToken to trimmed version
  authToken = authToken.trim();
  
  if (authToken.length < 32) {
    logger.warn('AUTH_TOKEN should be at least 32 characters for security');
    console.warn('WARNING: AUTH_TOKEN should be at least 32 characters for security');
  }
  
  // Check for default token and show prominent warnings
  if (authToken === 'REPLACE_THIS_AUTH_TOKEN_32_CHARS_MIN_abcdefgh') {
    logger.warn('⚠️ SECURITY WARNING: Using default AUTH_TOKEN - CHANGE IMMEDIATELY!');
    logger.warn('Generate secure token with: openssl rand -base64 32');
    
    // Only show console warnings in HTTP mode
    if (process.env.MCP_MODE === 'http') {
      console.warn('\n⚠️  SECURITY WARNING ⚠️');
      console.warn('Using default AUTH_TOKEN - CHANGE IMMEDIATELY!');
      console.warn('Generate secure token: openssl rand -base64 32');
      console.warn('Update via Railway dashboard environment variables\n');
    }
  }
}

/**
 * Graceful shutdown handler
 */
async function shutdown() {
  logger.info('Shutting down HTTP server...');
  console.log('Shutting down HTTP server...');
  
  if (expressServer) {
    expressServer.close(() => {
      logger.info('HTTP server closed');
      console.log('HTTP server closed');
      process.exit(0);
    });
    
    setTimeout(() => {
      logger.error('Forced shutdown after timeout');
      process.exit(1);
    }, 10000);
  } else {
    process.exit(0);
  }
}

export async function startFixedHTTPServer() {
  validateEnvironment();
  
  const app = express();
  
  // Configure trust proxy for correct IP logging behind reverse proxies
  const trustProxy = process.env.TRUST_PROXY ? Number(process.env.TRUST_PROXY) : 0;
  if (trustProxy > 0) {
    app.set('trust proxy', trustProxy);
    logger.info(`Trust proxy enabled with ${trustProxy} hop(s)`);
  }
  
  // CRITICAL: Don't use any body parser - StreamableHTTPServerTransport needs raw stream
  
  // Security headers
  app.use((req, res, next) => {
    res.setHeader('X-Content-Type-Options', 'nosniff');
    res.setHeader('X-Frame-Options', 'DENY');
    res.setHeader('X-XSS-Protection', '1; mode=block');
    res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
    next();
  });
  
  // CORS configuration
  app.use((req, res, next) => {
    const allowedOrigin = process.env.CORS_ORIGIN || '*';
    res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
    res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept');
    res.setHeader('Access-Control-Max-Age', '86400');
    
    if (req.method === 'OPTIONS') {
      res.sendStatus(204);
      return;
    }
    next();
  });
  
  // Request logging
  app.use((req, res, next) => {
    logger.info(`${req.method} ${req.path}`, {
      ip: req.ip,
      userAgent: req.get('user-agent'),
      contentLength: req.get('content-length')
    });
    next();
  });
  
  // Create a single persistent MCP server instance
  const mcpServer = new N8NDocumentationMCPServer();
  logger.info('Created persistent MCP server instance');

  // Root endpoint with API information
  app.get('/', (req, res) => {
    const port = parseInt(process.env.PORT || '3000');
    const host = process.env.HOST || '0.0.0.0';
    const baseUrl = detectBaseUrl(req, host, port);
    const endpoints = formatEndpointUrls(baseUrl);
    
    res.json({
      name: 'n8n Documentation MCP Server',
      version: PROJECT_VERSION,
      description: 'Model Context Protocol server providing comprehensive n8n node documentation and workflow management',
      endpoints: {
        health: {
          url: endpoints.health,
          method: 'GET',
          description: 'Health check and status information'
        },
        mcp: {
          url: endpoints.mcp,
          method: 'GET/POST',
          description: 'MCP endpoint - GET for info, POST for JSON-RPC'
        }
      },
      authentication: {
        type: 'Bearer Token',
        header: 'Authorization: Bearer <token>',
        required_for: ['POST /mcp']
      },
      documentation: 'https://github.com/czlonkowski/n8n-mcp'
    });
  });

  // Health check endpoint
  app.get('/health', (req, res) => {
    res.json({ 
      status: 'ok', 
      mode: 'http-fixed',
      version: PROJECT_VERSION,
      uptime: Math.floor(process.uptime()),
      memory: {
        used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
        total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024),
        unit: 'MB'
      },
      timestamp: new Date().toISOString()
    });
  });

  // Version endpoint
  app.get('/version', (req, res) => {
    res.json({ 
      version: PROJECT_VERSION,
      buildTime: new Date().toISOString(),
      tools: n8nDocumentationToolsFinal.map(t => t.name),
      commit: process.env.GIT_COMMIT || 'unknown'
    });
  });

  // Test tools endpoint
  app.get('/test-tools', async (req, res) => {
    try {
      const result = await mcpServer.executeTool('get_node_essentials', { nodeType: 'nodes-base.httpRequest' });
      res.json({ status: 'ok', hasData: !!result, toolCount: n8nDocumentationToolsFinal.length });
    } catch (error) {
      res.json({ status: 'error', message: error instanceof Error ? error.message : 'Unknown error' });
    }
  });
  
  // MCP information endpoint (no auth required for discovery)
  app.get('/mcp', (req, res) => {
    res.json({
      description: 'n8n Documentation MCP Server',
      version: PROJECT_VERSION,
      endpoints: {
        mcp: {
          method: 'POST',
          path: '/mcp',
          description: 'Main MCP JSON-RPC endpoint',
          authentication: 'Bearer token required'
        },
        health: {
          method: 'GET',
          path: '/health',
          description: 'Health check endpoint',
          authentication: 'None'
        },
        root: {
          method: 'GET',
          path: '/',
          description: 'API information',
          authentication: 'None'
        }
      },
      documentation: 'https://github.com/czlonkowski/n8n-mcp'
    });
  });

  // Main MCP endpoint - handle each request with custom transport handling
  app.post('/mcp', async (req: express.Request, res: express.Response): Promise<void> => {
    const startTime = Date.now();
    
    // Enhanced authentication check with specific logging
    const authHeader = req.headers.authorization;
    
    // Check if Authorization header is missing
    if (!authHeader) {
      logger.warn('Authentication failed: Missing Authorization header', { 
        ip: req.ip,
        userAgent: req.get('user-agent'),
        reason: 'no_auth_header'
      });
      res.status(401).json({ 
        jsonrpc: '2.0',
        error: {
          code: -32001,
          message: 'Unauthorized'
        },
        id: null
      });
      return;
    }
    
    // Check if Authorization header has Bearer prefix
    if (!authHeader.startsWith('Bearer ')) {
      logger.warn('Authentication failed: Invalid Authorization header format (expected Bearer token)', { 
        ip: req.ip,
        userAgent: req.get('user-agent'),
        reason: 'invalid_auth_format',
        headerPrefix: authHeader.substring(0, Math.min(authHeader.length, 10)) + '...'  // Log first 10 chars for debugging
      });
      res.status(401).json({ 
        jsonrpc: '2.0',
        error: {
          code: -32001,
          message: 'Unauthorized'
        },
        id: null
      });
      return;
    }
    
    // Extract token and trim whitespace
    const token = authHeader.slice(7).trim();

    // SECURITY: Use timing-safe comparison to prevent timing attacks
    // See: https://github.com/czlonkowski/n8n-mcp/issues/265 (CRITICAL-02)
    const isValidToken = authToken &&
      AuthManager.timingSafeCompare(token, authToken);

    if (!isValidToken) {
      logger.warn('Authentication failed: Invalid token', {
        ip: req.ip,
        userAgent: req.get('user-agent'),
        reason: 'invalid_token'
      });
      res.status(401).json({
        jsonrpc: '2.0',
        error: {
          code: -32001,
          message: 'Unauthorized'
        },
        id: null
      });
      return;
    }
    
    try {
      // Instead of using StreamableHTTPServerTransport, we'll handle the request directly
      // This avoids the initialization issues with the transport
      
      // Collect the raw body
      let body = '';
      req.on('data', chunk => {
        body += chunk.toString();
      });
      
      req.on('end', async () => {
        try {
          const jsonRpcRequest = JSON.parse(body);
          logger.debug('Received JSON-RPC request:', { method: jsonRpcRequest.method });
          
          // Handle the request based on method
          let response;
          
          switch (jsonRpcRequest.method) {
            case 'initialize':
              // Negotiate protocol version for this client/request
              const negotiationResult = negotiateProtocolVersion(
                jsonRpcRequest.params?.protocolVersion,
                jsonRpcRequest.params?.clientInfo,
                req.get('user-agent'),
                req.headers
              );
              
              logProtocolNegotiation(negotiationResult, logger, 'HTTP_SERVER_INITIALIZE');
              
              response = {
                jsonrpc: '2.0',
                result: {
                  protocolVersion: negotiationResult.version,
                  capabilities: {
                    tools: {},
                    resources: {}
                  },
                  serverInfo: {
                    name: 'n8n-documentation-mcp',
                    version: PROJECT_VERSION
                  }
                },
                id: jsonRpcRequest.id
              };
              break;
              
            case 'tools/list':
              // Use the proper tool list that includes management tools when configured
              const tools = [...n8nDocumentationToolsFinal];
              
              // Add management tools if n8n API is configured
              if (isN8nApiConfigured()) {
                tools.push(...n8nManagementTools);
              }
              
              response = {
                jsonrpc: '2.0',
                result: {
                  tools
                },
                id: jsonRpcRequest.id
              };
              break;
              
            case 'tools/call':
              // Delegate to the MCP server
              const toolName = jsonRpcRequest.params?.name;
              const toolArgs = jsonRpcRequest.params?.arguments || {};
              
              try {
                const result = await mcpServer.executeTool(toolName, toolArgs);
                response = {
                  jsonrpc: '2.0',
                  result: {
                    content: [
                      {
                        type: 'text',
                        text: JSON.stringify(result, null, 2)
                      }
                    ]
                  },
                  id: jsonRpcRequest.id
                };
              } catch (error) {
                response = {
                  jsonrpc: '2.0',
                  error: {
                    code: -32603,
                    message: `Error executing tool ${toolName}: ${error instanceof Error ? error.message : 'Unknown error'}`
                  },
                  id: jsonRpcRequest.id
                };
              }
              break;
              
            default:
              response = {
                jsonrpc: '2.0',
                error: {
                  code: -32601,
                  message: `Method not found: ${jsonRpcRequest.method}`
                },
                id: jsonRpcRequest.id
              };
          }
          
          // Send response
          res.setHeader('Content-Type', 'application/json');
          res.json(response);
          
          const duration = Date.now() - startTime;
          logger.info('MCP request completed', { 
            duration,
            method: jsonRpcRequest.method 
          });
        } catch (error) {
          logger.error('Error processing request:', error);
          res.status(400).json({
            jsonrpc: '2.0',
            error: {
              code: -32700,
              message: 'Parse error',
              data: error instanceof Error ? error.message : 'Unknown error'
            },
            id: null
          });
        }
      });
    } catch (error) {
      logger.error('MCP request error:', error);
      
      if (!res.headersSent) {
        res.status(500).json({ 
          jsonrpc: '2.0',
          error: {
            code: -32603,
            message: 'Internal server error',
            data: process.env.NODE_ENV === 'development' 
              ? (error as Error).message 
              : undefined
          },
          id: null
        });
      }
    }
  });
  
  // 404 handler
  app.use((req, res) => {
    res.status(404).json({ 
      error: 'Not found',
      message: `Cannot ${req.method} ${req.path}`
    });
  });
  
  // Error handler
  app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
    logger.error('Express error handler:', err);
    
    if (!res.headersSent) {
      res.status(500).json({ 
        jsonrpc: '2.0',
        error: {
          code: -32603,
          message: 'Internal server error',
          data: process.env.NODE_ENV === 'development' ? err.message : undefined
        },
        id: null
      });
    }
  });
  
  const port = parseInt(process.env.PORT || '3000');
  const host = process.env.HOST || '0.0.0.0';
  
  expressServer = app.listen(port, host, () => {
    logger.info(`n8n MCP Fixed HTTP Server started`, { port, host });
    
    // Detect the base URL using our utility
    const baseUrl = getStartupBaseUrl(host, port);
    const endpoints = formatEndpointUrls(baseUrl);
    
    console.log(`n8n MCP Fixed HTTP Server running on ${host}:${port}`);
    console.log(`Health check: ${endpoints.health}`);
    console.log(`MCP endpoint: ${endpoints.mcp}`);
    console.log('\nPress Ctrl+C to stop the server');
    
    // Start periodic warning timer if using default token
    if (authToken === 'REPLACE_THIS_AUTH_TOKEN_32_CHARS_MIN_abcdefgh') {
      setInterval(() => {
        logger.warn('⚠️ Still using default AUTH_TOKEN - security risk!');
        if (process.env.MCP_MODE === 'http') {
          console.warn('⚠️ REMINDER: Still using default AUTH_TOKEN - please change it!');
        }
      }, 300000); // Every 5 minutes
    }
    
    if (process.env.BASE_URL || process.env.PUBLIC_URL) {
      console.log(`\nPublic URL configured: ${baseUrl}`);
    } else if (process.env.TRUST_PROXY && Number(process.env.TRUST_PROXY) > 0) {
      console.log(`\nNote: TRUST_PROXY is enabled. URLs will be auto-detected from proxy headers.`);
    }
  });
  
  // Handle errors
  expressServer.on('error', (error: any) => {
    if (error.code === 'EADDRINUSE') {
      logger.error(`Port ${port} is already in use`);
      console.error(`ERROR: Port ${port} is already in use`);
      process.exit(1);
    } else {
      logger.error('Server error:', error);
      console.error('Server error:', error);
      process.exit(1);
    }
  });
  
  // Graceful shutdown handlers
  process.on('SIGTERM', shutdown);
  process.on('SIGINT', shutdown);
  
  // Handle uncaught errors
  process.on('uncaughtException', (error) => {
    logger.error('Uncaught exception:', error);
    console.error('Uncaught exception:', error);
    shutdown();
  });
  
  process.on('unhandledRejection', (reason, promise) => {
    logger.error('Unhandled rejection:', reason);
    console.error('Unhandled rejection at:', promise, 'reason:', reason);
    shutdown();
  });
}

// Make executeTool public on the server
declare module './mcp/server' {
  interface N8NDocumentationMCPServer {
    executeTool(name: string, args: any): Promise<any>;
  }
}

// Start if called directly
// Check if this file is being run directly (not imported)
// In ES modules, we check import.meta.url against process.argv[1]
// But since we're transpiling to CommonJS, we use the require.main check
if (typeof require !== 'undefined' && require.main === module) {
  startFixedHTTPServer().catch(error => {
    logger.error('Failed to start Fixed HTTP server:', error);
    console.error('Failed to start Fixed HTTP server:', error);
    process.exit(1);
  });
}