import Fastify from 'fastify';
import websocket from '@fastify/websocket';
import cors from '@fastify/cors';
import Redis from 'ioredis';
import { readFileSync } from 'fs';
import { load as loadYaml } from 'js-yaml';
import { UserService } from './db/user-service.js';
import { Authenticator } from './auth/authenticator.js';
import { createBetterAuth } from './auth/better-auth-config.js';
import { AuthService } from './auth/auth-service.js';
import { AuthRoutes } from './routes/auth-routes.js';
import { WebSocketHandler } from './channels/websocket-handler.js';
import { TelegramHandler } from './channels/telegram-handler.js';
import { KubernetesClient } from './k8s/client.js';
import { ContainerManager } from './k8s/container-manager.js';
import { ZMQRelayClient } from './clients/zmq-relay-client.js';
import { IcebergClient } from './clients/iceberg-client.js';
import { ConversationStore } from './harness/memory/conversation-store.js';
import { BlobStore } from './harness/memory/blob-store.js';
import { ConversationService } from './services/conversation-service.js';
import { AgentHarness, type HarnessSessionConfig } from './harness/agent-harness.js';
import { OHLCService } from './services/ohlc-service.js';
import { SymbolIndexService } from './services/symbol-index-service.js';
import { SymbolRoutes } from './routes/symbol-routes.js';
import { AdminRoutes } from './routes/admin-routes.js';

// Catch unhandled promise rejections for better debugging
process.on('unhandledRejection', (reason: any, promise) => {
  console.error('=== UNHANDLED PROMISE REJECTION ===');
  console.error('Reason:', reason);
  console.error('Message:', reason?.message);
  console.error('Stack:', reason?.stack);
  console.error('Cause:', reason?.cause);
  console.error('Promise:', promise);
  console.error('===================================');
  process.exit(1);
});
import {
  SessionRegistry,
  EventSubscriber,
  EventRouter,
  DeliveryService,
} from './events/index.js';
import { initializeToolRegistry } from './tools/tool-registry.js';

// Load configuration from YAML files
function loadConfig() {
  const configPath = process.env.CONFIG_PATH || '/config/config.yaml';
  const secretsPath = process.env.SECRETS_PATH || '/config/secrets.yaml';

  let configData: any = {};
  let secretsData: any = {};

  try {
    const configFile = readFileSync(configPath, 'utf8');
    configData = loadYaml(configFile) || {};
    console.log(`Loaded configuration from ${configPath}`);
  } catch (error: any) {
    console.warn(`Could not load config from ${configPath}: ${error.message}, using defaults`);
  }

  try {
    const secretsFile = readFileSync(secretsPath, 'utf8');
    secretsData = loadYaml(secretsFile) || {};
    console.log(`Loaded secrets from ${secretsPath}`);
  } catch (error: any) {
    console.warn(`Could not load secrets from ${secretsPath}: ${error.message}`);
  }

  return {
    port: configData.server?.port || parseInt(process.env.PORT || '3000'),
    host: configData.server?.host || process.env.HOST || '0.0.0.0',
    logLevel: configData.server?.log_level || process.env.LOG_LEVEL || 'info',
    corsOrigin: configData.server?.cors_origin || process.env.CORS_ORIGIN || '*',
    baseUrl: configData.server?.base_url || process.env.BASE_URL || 'http://localhost:3000',
    trustedOrigins: configData.server?.trusted_origins || [
      process.env.BASE_URL || 'http://localhost:3000',
      'http://localhost:5173',
      'http://localhost:8080',
    ],
    databaseUrl: configData.database?.url || secretsData.database?.url || process.env.DATABASE_URL || 'postgresql://localhost/dexorder',

    // Authentication configuration
    authSecret: secretsData.auth?.secret || process.env.AUTH_SECRET || 'change-me-in-production',

    // LLM provider API keys and model configuration
    providerConfig: {
      deepinfraApiKey: secretsData.llm_providers?.deepinfra_api_key || process.env.DEEPINFRA_API_KEY,
      defaultModel: {
        provider: configData.defaults?.model_provider || 'deepinfra',
        model: configData.defaults?.model || 'zai-org/GLM-5',
      },
      licenseModels: {
        free: {
          default: configData.license_models?.free?.default || 'zai-org/GLM-5',
          cost_optimized: configData.license_models?.free?.cost_optimized || 'zai-org/GLM-5',
          complex: configData.license_models?.free?.complex || 'zai-org/GLM-5',
          allowed_models: configData.license_models?.free?.allowed_models || ['zai-org/GLM-5'],
        },
        pro: {
          default: configData.license_models?.pro?.default || 'zai-org/GLM-5',
          cost_optimized: configData.license_models?.pro?.cost_optimized || 'zai-org/GLM-5',
          complex: configData.license_models?.pro?.complex || 'zai-org/GLM-5',
          blocked_models: configData.license_models?.pro?.blocked_models || ['Qwen/Qwen3-235B-A22B-Instruct-2507'],
        },
        enterprise: {
          default: configData.license_models?.enterprise?.default || 'zai-org/GLM-5',
          cost_optimized: configData.license_models?.enterprise?.cost_optimized || 'zai-org/GLM-5',
          complex: configData.license_models?.enterprise?.complex || 'Qwen/Qwen3-235B-A22B-Instruct-2507',
        },
      },
    },

    telegramBotToken: secretsData.telegram?.bot_token || process.env.TELEGRAM_BOT_TOKEN || '',

    // Email service configuration
    emailServiceKey: secretsData.email?.service_key || process.env.EMAIL_SERVICE_KEY,
    emailFromAddress: configData.email?.from_address || process.env.EMAIL_FROM_ADDRESS || 'noreply@dexorder.com',

    // Push notification service configuration
    pushServiceKey: secretsData.push?.service_key || process.env.PUSH_SERVICE_KEY,

    // Event router configuration
    eventRouterBind: configData.events?.router_bind || process.env.EVENT_ROUTER_BIND || 'tcp://*:5571',

    // Redis configuration (for harness memory layer)
    redisUrl: configData.redis?.url || process.env.REDIS_URL || 'redis://localhost:6379',

    // Conversation history limit: number of prior turns loaded as LLM context and flushed to Iceberg
    conversationHistoryLimit: configData.agent?.conversation_history_limit || parseInt(process.env.CONVERSATION_HISTORY_LIMIT || '20'),

    // Iceberg configuration (for durable storage)
    iceberg: {
      catalogUri: configData.iceberg?.catalog_uri || process.env.ICEBERG_CATALOG_URI || 'http://iceberg-catalog:8181',
      namespace: configData.iceberg?.namespace || process.env.ICEBERG_NAMESPACE || 'gateway',
      ohlcCatalogUri: configData.iceberg?.ohlc_catalog_uri || process.env.ICEBERG_OHLC_CATALOG_URI,
      ohlcNamespace: configData.iceberg?.ohlc_namespace || process.env.ICEBERG_OHLC_NAMESPACE || 'trading',
      s3Endpoint: configData.iceberg?.s3_endpoint || process.env.S3_ENDPOINT,
      s3AccessKey: secretsData.iceberg?.s3_access_key || process.env.S3_ACCESS_KEY,
      s3SecretKey: secretsData.iceberg?.s3_secret_key || process.env.S3_SECRET_KEY,
      conversationsBucket: configData.iceberg?.conversations_bucket || process.env.CONVERSATIONS_S3_BUCKET,
    },

    // Relay configuration (for historical data)
    relay: {
      requestEndpoint: configData.relay?.request_endpoint || process.env.RELAY_REQUEST_ENDPOINT || 'tcp://relay:5559',
      notificationEndpoint: configData.relay?.notification_endpoint || process.env.RELAY_NOTIFICATION_ENDPOINT || 'tcp://relay:5558',
    },

    // Kubernetes configuration
    kubernetes: {
      namespace: configData.kubernetes?.namespace || process.env.KUBERNETES_NAMESPACE || 'sandbox',
      serviceNamespace: configData.kubernetes?.service_namespace || process.env.KUBERNETES_SERVICE_NAMESPACE || 'default',
      inCluster: configData.kubernetes?.in_cluster ?? (process.env.KUBERNETES_IN_CLUSTER === 'true'),
      context: configData.kubernetes?.context || process.env.KUBERNETES_CONTEXT,
      sandboxImage: configData.kubernetes?.sandbox_image || process.env.SANDBOX_IMAGE || 'ghcr.io/dexorder/sandbox:latest',
      sidecarImage: configData.kubernetes?.sidecar_image || process.env.SIDECAR_IMAGE || 'ghcr.io/dexorder/lifecycle-sidecar:latest',
      storageClass: configData.kubernetes?.storage_class || process.env.SANDBOX_STORAGE_CLASS || '',
      imagePullPolicy: configData.kubernetes?.image_pull_policy || process.env.IMAGE_PULL_POLICY || 'Always',
    },

    // Search API keys
    tavilyApiKey: secretsData.search?.tavily_api_key || process.env.TAVILY_API_KEY,
  };
}

const config = loadConfig();

const app = Fastify({
  logger: {
    level: config.logLevel,
    transport: {
      target: 'pino-pretty',
      options: {
        colorize: true,
        translateTime: 'HH:MM:ss Z',
        ignore: 'pid,hostname',
      },
    },
  },
});

// Validate LLM provider is configured
if (!config.providerConfig.deepinfraApiKey) {
  app.log.error('DEEPINFRA_API_KEY is required');
  process.exit(1);
}

// Register plugins
await app.register(cors, {
  origin: config.corsOrigin,
});

await app.register(websocket, {
  options: {
    maxPayload: 1024 * 1024, // 1MB
  },
});

// Initialize services
const userService = new UserService(config.databaseUrl);

// Initialize Better Auth
let betterAuth;
try {
  app.log.info({ databaseUrl: config.databaseUrl.replace(/:[^:@]+@/, ':***@') }, 'Initializing Better Auth');
  betterAuth = await createBetterAuth({
    databaseUrl: config.databaseUrl,
    secret: config.authSecret,
    baseUrl: config.baseUrl,
    trustedOrigins: config.trustedOrigins,
    logger: app.log,
  });
  app.log.info('Better Auth initialized successfully');
} catch (error: any) {
  app.log.error({ error, message: error.message, stack: error.stack }, 'Failed to initialize Better Auth');
  throw error;
}

// Initialize Auth Service
const authService = new AuthService({
  auth: betterAuth,
  pool: userService.getPool(),
  logger: app.log,
});

// Connect UserService with AuthService for JWT verification
userService.setAuthService(authService);

// Initialize Redis client (for harness memory layer)
const redis = new Redis(config.redisUrl, {
  maxRetriesPerRequest: 3,
  connectTimeout: 10000, // 10 seconds
  retryStrategy: (times) => {
    if (times > 5) {
      app.log.error('Redis connection failed after 5 retries');
      return null; // Stop retrying
    }
    const delay = Math.min(times * 50, 2000);
    return delay;
  },
  lazyConnect: true,
});

// Initialize Iceberg client (for durable storage)
// const icebergClient = new IcebergClient(config.iceberg, app.log);

// Create metadata update callback that will be wired up when SymbolIndexService initializes
// This ensures we don't miss notifications sent before the service is ready
let symbolIndexService: SymbolIndexService | undefined;
const onMetadataUpdate = async () => {
  if (symbolIndexService) {
    app.log.info('Reloading symbol metadata from Iceberg');
    await symbolIndexService.initialize();
    app.log.info({ stats: symbolIndexService.getStats() }, 'Symbol metadata reloaded');
  } else {
    app.log.warn('Received METADATA_UPDATE before SymbolIndexService initialized, ignoring');
  }
};

// Initialize ZMQ Relay client (for historical data)
// Pass onMetadataUpdate callback so it's registered before connection
const zmqRelayClient = new ZMQRelayClient({
  relayRequestEndpoint: config.relay.requestEndpoint,
  relayNotificationEndpoint: config.relay.notificationEndpoint,
  onMetadataUpdate,
}, app.log);

app.log.info({
  redis: config.redisUrl,
  iceberg: config.iceberg.catalogUri,
  relay: config.relay.requestEndpoint,
}, 'Harness storage clients configured');

// Initialize Kubernetes client and container manager
const k8sClient = new KubernetesClient({
  namespace: config.kubernetes.namespace,
  inCluster: config.kubernetes.inCluster,
  context: config.kubernetes.context,
  logger: app.log,
});

const containerManager = new ContainerManager({
  k8sClient,
  userService,
  sandboxImage: config.kubernetes.sandboxImage,
  sidecarImage: config.kubernetes.sidecarImage,
  storageClass: config.kubernetes.storageClass,
  imagePullPolicy: config.kubernetes.imagePullPolicy,
  namespace: config.kubernetes.namespace,
  serviceNamespace: config.kubernetes.serviceNamespace,
  logger: app.log,
});
app.log.debug('Container manager initialized');

const authenticator = new Authenticator({
  userService,
  containerManager,
  logger: app.log,
});
app.log.debug('Authenticator initialized');

// Initialize event system
const sessionRegistry = new SessionRegistry();
app.log.debug('Session registry initialized');

const deliveryService = new DeliveryService({
  telegramBotToken: config.telegramBotToken,
  emailServiceKey: config.emailServiceKey,
  emailFromAddress: config.emailFromAddress,
  pushServiceKey: config.pushServiceKey,
  logger: app.log,
});
app.log.debug('Delivery service initialized');

const eventSubscriber = new EventSubscriber(sessionRegistry, app.log);
app.log.debug('Event subscriber initialized');

const eventRouter = new EventRouter({
  sessions: sessionRegistry,
  delivery: deliveryService,
  logger: app.log,
  bindEndpoint: config.eventRouterBind,
});
app.log.debug('Event router initialized');

// Initialize shared Iceberg client (used by both OHLC service and conversation store)
const icebergClient = new IcebergClient(config.iceberg, app.log);
app.log.debug('Iceberg client initialized');

// Initialize OHLC service (optional - only if relay is available)
let ohlcService: OHLCService | undefined;
try {
  ohlcService = new OHLCService({
    icebergClient,
    relayClient: zmqRelayClient,
    logger: app.log,
  });
  app.log.info('OHLC service initialized');
} catch (error) {
  app.log.warn({ error }, 'Failed to initialize OHLC service - historical data will not be available');
}

// Initialize conversation store (Redis hot path + Iceberg cold path)
const conversationStore = new ConversationStore(redis, app.log, icebergClient);
app.log.debug('Conversation store initialized');

const blobStore = new BlobStore(icebergClient, app.log);
const conversationService = new ConversationService(conversationStore, blobStore, app.log);
app.log.debug('Blob store and conversation service initialized');

// Harness factory: captures infrastructure deps; channel handlers stay infrastructure-free
function createHarness(sessionConfig: HarnessSessionConfig): AgentHarness {
  return new AgentHarness({
    ...sessionConfig,
    providerConfig: config.providerConfig,
    conversationStore,
    blobStore,
    historyLimit: config.conversationHistoryLimit,
  });
}

// Symbol Index Service will be initialized after server starts
// (declared above near ZMQ client initialization)

// Initialize channel handlers
const websocketHandler = new WebSocketHandler({
  authenticator,
  containerManager,
  sessionRegistry,
  eventSubscriber,
  createHarness,
  ohlcService,  // Optional
  symbolIndexService,  // Optional
  conversationService,  // Optional - for history replay on reconnect
});
app.log.debug('WebSocket handler initialized');

const telegramHandler = new TelegramHandler({
  authenticator,
  telegramBotToken: config.telegramBotToken,
  createHarness,
});
app.log.debug('Telegram handler initialized');

// Initialize auth routes
app.log.debug('Initializing auth routes...');
const authRoutes = new AuthRoutes({
  authService,
  betterAuth,
  containerManager,
  userService,
});

// Register routes
app.log.debug('Registering auth routes...');
try {
  authRoutes.register(app);
  app.log.debug('Auth routes registered successfully');
} catch (error: any) {
  app.log.error({ error, message: error.message, stack: error.stack }, 'Failed to register auth routes');
  throw error;
}

app.log.debug('Registering websocket handler...');
websocketHandler.register(app);
app.log.debug('Registering telegram handler...');
telegramHandler.register(app);

// Register symbol routes (service may not be ready yet, but routes will handle this)
app.log.debug('Registering symbol routes...');
const getSymbolService = () => symbolIndexService;
const symbolRoutes = new SymbolRoutes({ getSymbolIndexService: getSymbolService });
symbolRoutes.register(app);

// Register admin routes
new AdminRoutes(containerManager, userService).register(app);

app.log.debug('All routes registered');

// Health check
app.get('/health', async () => {
  const health: any = {
    status: 'ok',
    timestamp: new Date().toISOString(),
    activeSessions: sessionRegistry.size(),
    eventSubscriptions: eventSubscriber.getSubscriptionCount(),
    processedEvents: eventRouter.getProcessedEventCount(),
  };

  return health;
});

// Graceful shutdown
const shutdown = async () => {
  app.log.info('Shutting down gracefully...');
  try {
    // Flush all active sessions to Iceberg before shutdown
    await websocketHandler.endAllSessions();
    await telegramHandler.endAllSessions();

    // Stop event system first
    await eventSubscriber.stop();
    await eventRouter.stop();

    // Close ZMQ relay client
    if (zmqRelayClient.isConnected()) {
      await zmqRelayClient.close();
    }

    // Disconnect Redis
    redis.disconnect();

    await userService.close();
    await app.close();
    app.log.info('Shutdown complete');
    process.exit(0);
  } catch (error) {
    app.log.error({ error }, 'Error during shutdown');
    process.exit(1);
  }
};

process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);

// Start server
try {
  app.log.debug('Starting server initialization...');

  // Connect to Redis
  app.log.debug('Connecting to Redis...');
  await redis.connect();
  app.log.info('Redis connected');

  // Connect to ZMQ Relay
  app.log.debug('Connecting to ZMQ Relay...');
  try {
    await zmqRelayClient.connect();
    app.log.info('ZMQ Relay connected');
  } catch (error) {
    app.log.warn({ error }, 'ZMQ Relay connection failed - historical data will not be available');
  }

  // Initialize tool registry
  app.log.debug('Initializing tool registry...');
  try {
    const toolRegistry = initializeToolRegistry(app.log, {
      // Use getter functions to support lazy initialization
      ohlcService: () => ohlcService,
      symbolIndexService: () => symbolIndexService,
      workspaceManager: undefined, // Will be set per-session
      tavilyApiKey: config.tavilyApiKey,
    });

    // Register agent tool configurations
    // Main agent: platform tools + user's general MCP tools
    toolRegistry.registerAgentTools({
      agentName: 'main',
      platformTools: ['SymbolLookup', 'GetChartData'],
      mcpTools: ['PythonList', 'PythonDelete', 'BacktestStrategy', 'ListActiveStrategies'],
    });

    app.log.info(
      {
        agents: toolRegistry.getRegisteredAgents(),
        configs: toolRegistry.getRegisteredAgents().map(name => ({
          name,
          config: toolRegistry.getAgentToolConfig(name),
        })),
      },
      'Tool registry initialized'
    );
  } catch (error) {
    app.log.error({ error }, 'Failed to initialize tool registry');
    // Non-fatal - continue without tools
  }

  // Start event system
  app.log.debug('Starting event subscriber...');
  await eventSubscriber.start();
  app.log.debug('Starting event router...');
  await eventRouter.start();
  app.log.debug('Event system started');

  app.log.debug('Starting Fastify server...');
  await app.listen({
    port: config.port,
    host: config.host,
  });

  app.log.info(
    {
      port: config.port,
      host: config.host,
      eventRouterBind: config.eventRouterBind,
      redis: config.redisUrl,
    },
    'Gateway server started'
  );

  // Initialize Symbol Index Service (after server is running)
  // This is done asynchronously to not block server startup
  // The onMetadataUpdate callback is already registered with zmqRelayClient
  (async () => {
    try {
      const icebergClient = new IcebergClient(config.iceberg, app.log);
      const indexService = new SymbolIndexService({
        icebergClient,
        logger: app.log,
      });

      // Assign to module-level variable so onMetadataUpdate callback can use it
      symbolIndexService = indexService;

      // Update websocket handler's config so it can use the service
      (websocketHandler as any).config.symbolIndexService = indexService;

      // Retry until we get at least some symbol metadata
      while (true) {
        await indexService.initialize();
        const stats = indexService.getStats();
        if (stats.symbolCount > 0) {
          app.log.info({ stats }, 'Symbol index service initialized');
          break;
        }
        app.log.warn('Symbol index has no metadata yet, retrying in 5 seconds...');
        await new Promise(resolve => setTimeout(resolve, 5000));
      }
    } catch (error) {
      app.log.warn({ error }, 'Failed to initialize symbol index service - symbol search will not be available');
    }
  })();
} catch (error) {
  app.log.error({ error }, 'Failed to start server');
  process.exit(1);
}
