#!/usr/bin/env node
/**
 * MCPBundles Cloud Proxy Server
 * 
 * Connects your AI client to MCPBundles cloud services via stdio.
 * Handles automatic reconnection, health checks, and error recovery.
 */

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';

const BUNDLE_ENABLED_CONFIRM = process.env.BUNDLE_ENABLED_CONFIRM;
const MCP_API_KEY = process.env.MCP_API_KEY;
const REMOTE_URL = process.env.REMOTE_URL;
const BUNDLE_SLUG = process.env.BUNDLE_SLUG;

if (BUNDLE_ENABLED_CONFIRM !== undefined && BUNDLE_ENABLED_CONFIRM.toLowerCase() !== 'enabled') {
  console.error("ERROR: Bundle must be enabled before use");
  console.error(`Visit https://www.mcpbundles.com/bundles/${BUNDLE_SLUG || 'your-bundle'} to enable.`);
  process.exit(1);
}

if (!REMOTE_URL) {
  console.error("ERROR: Missing required configuration (REMOTE_URL)");
  process.exit(1);
}

// MCP_API_KEY is optional for public endpoints (e.g., Discovery)
if (MCP_API_KEY) {
  console.error(`[MCPBundles] Connecting to ${REMOTE_URL} (authenticated)`);
} else {
  console.error(`[MCPBundles] Connecting to ${REMOTE_URL} (public endpoint)`);
}

const server = new Server({
  name: "mcpbundles-app",
  version: "1.0.0",
}, {
  capabilities: { tools: {} },
});

const connectionState = {
  client: null,
  transport: null,
  status: 'disconnected',
  lastSuccessfulRequest: null,
  reconnectAttempts: 0,
  maxReconnectAttempts: 10,
  reconnectDelays: [1000, 2000, 4000, 8000, 16000, 30000],
  healthCheckInterval: null,
  isReconnecting: false,
};

let remoteClient = null;

function isSessionClosedError(error) {
  if (!error || !error.message) return false;
  
  const errorMessage = error.message.toLowerCase();
  return (
    errorMessage.includes('503') || 
    errorMessage.includes('service unavailable') ||
    errorMessage.includes('temporarily unavailable') ||
    errorMessage.includes('session') ||
    errorMessage.includes('session expired') ||
    errorMessage.includes('session_expired') ||
    errorMessage.includes('no valid session') ||
    errorMessage.includes('connection refused') ||
    errorMessage.includes('econnrefused') ||
    errorMessage.includes('econnreset') ||
    errorMessage.includes('etimedout') ||
    errorMessage.includes('network error') ||
    error.code === 'ECONNREFUSED' ||
    error.code === 'ECONNRESET' ||
    error.code === 'ETIMEDOUT'
  );
}

function is503Error(error) {
  return error.message && (
    error.message.includes('503') || 
    error.message.includes('Service Unavailable') ||
    error.message.includes('temporarily unavailable')
  );
}

async function cleanupConnection() {
  connectionState.client = null;
  connectionState.transport = null;
  remoteClient = null;
}

async function connectToRemote() {
  connectionState.status = 'connecting';
  
  // Build headers - only include X-API-Key if provided
  const headers = {
    'Accept': 'application/json, text/event-stream',
  };
  
  if (MCP_API_KEY) {
    headers['X-API-Key'] = MCP_API_KEY;
  }
  
  const transport = new StreamableHTTPClientTransport(
    new URL(REMOTE_URL),
    {
      requestInit: {
        headers,
      },
    }
  );
  
  const client = new Client({
    name: "mcpbundles-proxy-client",
    version: "1.0.0",
  }, {
    capabilities: { tools: {} }
  });
  
  await client.connect(transport);
  
  connectionState.client = client;
  connectionState.transport = transport;
  connectionState.status = 'connected';
  connectionState.lastSuccessfulRequest = Date.now();
  connectionState.reconnectAttempts = 0;
  remoteClient = client;
  
  console.error('[MCPBundles] Connected successfully');
  return true;
}

async function reconnectWithBackoff() {
  if (connectionState.isReconnecting) return false;
  
  connectionState.isReconnecting = true;
  await cleanupConnection();
  
  while (connectionState.reconnectAttempts < connectionState.maxReconnectAttempts) {
    try {
      await connectToRemote();
      connectionState.isReconnecting = false;
      return true;
    } catch (error) {
      connectionState.reconnectAttempts++;
      
      if (connectionState.reconnectAttempts >= connectionState.maxReconnectAttempts) {
        connectionState.isReconnecting = false;
        return false;
      }
      
      const delayIndex = Math.min(connectionState.reconnectAttempts - 1, connectionState.reconnectDelays.length - 1);
      const delay = connectionState.reconnectDelays[delayIndex];
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
  
  connectionState.isReconnecting = false;
  return false;
}

async function executeWithReconnect(operation, operationName) {
  if (connectionState.status !== 'connected' || !remoteClient) {
    const reconnected = await reconnectWithBackoff();
    if (!reconnected) throw new Error('Connection failed');
  }
  
  try {
    const result = await operation();
    connectionState.lastSuccessfulRequest = Date.now();
    return result;
  } catch (error) {
    if (isSessionClosedError(error)) {
      const reconnected = await reconnectWithBackoff();
      if (reconnected) {
        const result = await operation();
        connectionState.lastSuccessfulRequest = Date.now();
        return result;
      }
      throw new Error('Reconnection failed');
    }
    throw error;
  }
}

async function performHealthCheck() {
  try {
    if (connectionState.status !== 'connected' || !remoteClient) {
      await reconnectWithBackoff();
      return;
    }
    
    await remoteClient.callTool({ name: "health_check", arguments: {} });
    connectionState.lastSuccessfulRequest = Date.now();
  } catch (error) {
    if (isSessionClosedError(error)) {
      await reconnectWithBackoff();
    }
  }
}

function startHealthChecks() {
  connectionState.healthCheckInterval = setInterval(() => {
    performHealthCheck().catch(() => {});
  }, 120000);
}

function stopHealthChecks() {
  if (connectionState.healthCheckInterval) {
    clearInterval(connectionState.healthCheckInterval);
    connectionState.healthCheckInterval = null;
  }
}

server.setRequestHandler(ListToolsRequestSchema, async () => {
  try {
    const result = await executeWithReconnect(
      async () => await remoteClient.listTools(),
      'listTools'
    );
    return { tools: result.tools || [] };
  } catch (error) {
    return { tools: [] };
  }
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;
  
  try {
    return await executeWithReconnect(
      async () => await remoteClient.callTool({ name, arguments: args }),
      `callTool(${name})`
    );
  } catch (error) {
    let errorMsg = `Error: ${error.message}`;
    
    if (is503Error(error)) {
      errorMsg = 'Service temporarily unavailable. Please try again later.';
    } else if (isSessionClosedError(error)) {
      errorMsg = 'Connection lost. Please try again.';
    }
    
    return {
      content: [{ type: "text", text: errorMsg }],
      isError: true
    };
  }
});

async function main() {
  await connectToRemote();
  startHealthChecks();
  
  const stdioTransport = new StdioServerTransport();
  await server.connect(stdioTransport);
  
  console.error('[MCPBundles] Ready');
}

process.on('unhandledRejection', (reason) => {
  console.error('[MCPBundles] Unhandled rejection:', reason);
});

process.on('uncaughtException', (error) => {
  console.error('[MCPBundles] Uncaught exception:', error);
  process.exit(1);
});

process.on('SIGINT', () => {
  stopHealthChecks();
  process.exit(0);
});

process.on('SIGTERM', () => {
  stopHealthChecks();
  process.exit(0);
});

main().catch((error) => {
  console.error('[MCPBundles] Fatal error:', error.message);
  process.exit(1);
});
