/**
 * Entry point: HTTP + WebSocket on one port, with a graceful shutdown that
 * drains sockets before closing the database and Redis connections.
 */
import { createServer } from 'node:http';
import { env } from './config/env.js';
import { createApp } from './http/app.js';
import { createSocketServer } from './realtime/io.js';
import { pool } from './db/pool.js';
import { redis, redisHealth, redisPub, redisSub } from './redis/client.js';
import { rebuildQueue } from './modules/conversations/queue.js';
import { autoMigrate, seedAdmin } from './db/bootstrap.js';
import { logger } from './utils/logger.js';
import { connectionHint, describeError } from './utils/describeError.js';

/**
 * ioredis runs with `maxRetriesPerRequest: null` because the Socket.IO Redis
 * adapter requires it - but that also means `ping()` queues forever rather
 * than rejecting when Redis is unreachable. Without a bound here, a
 * misconfigured REDIS_URL makes the server hang silently at boot instead of
 * reporting the problem, so every startup probe gets a deadline.
 */
async function withTimeout<T>(work: Promise<T>, ms: number, label: string): Promise<T> {
  let timer: ReturnType<typeof setTimeout> | undefined;
  const deadline = new Promise<never>((_, reject) => {
    timer = setTimeout(() => reject(new Error(`${label} did not respond within ${ms / 1000}s`)), ms);
  });
  try {
    return await Promise.race([work, deadline]);
  } finally {
    clearTimeout(timer);
  }
}

const STARTUP_PROBE_MS = 15_000;

async function main() {
  // Fail fast if the dependencies aren't reachable - a half-up server that
  // accepts chats it cannot persist is worse than one that refuses to boot.
  // Each is checked separately so the error names the one that is actually down.
  try {
    await withTimeout(pool.query('SELECT 1'), STARTUP_PROBE_MS, 'Postgres');
  } catch (err) {
    throw new Error(`Postgres is unreachable. ${connectionHint(err, 'DATABASE_URL')}`);
  }
  try {
    await withTimeout(redis.ping(), STARTUP_PROBE_MS, 'Redis');
  } catch (err) {
    // Prefer the underlying socket error over the bare timeout message.
    throw new Error(`Redis is unreachable. ${connectionHint(redisHealth.lastError ?? err, 'REDIS_URL')}`);
  }
  logger.info('dependencies reachable', { database: 'ok', redis: 'ok' });

  // Optional self-setup for hosts with no shell access.
  await autoMigrate();
  await seedAdmin();

  // Self-heal the Redis queue mirror from Postgres on every boot.
  await rebuildQueue();

  const app = createApp();
  const httpServer = createServer(app);
  const io = createSocketServer(httpServer);

  httpServer.listen(env.PORT, () => {
    logger.info(`server listening on http://localhost:${env.PORT}`, {
      env: env.NODE_ENV,
      storage: env.STORAGE_DRIVER,
      cors: env.corsOrigins,
    });
  });

  const shutdown = async (signal: string) => {
    logger.info(`${signal} received, shutting down`);
    const timer = setTimeout(() => {
      logger.warn('graceful shutdown timed out, forcing exit');
      process.exit(1);
    }, 10_000);

    io.close();
    httpServer.close();
    await Promise.allSettled([pool.end(), redis.quit(), redisPub.quit(), redisSub.quit()]);
    clearTimeout(timer);
    logger.info('shutdown complete');
    process.exit(0);
  };

  process.on('SIGINT', () => void shutdown('SIGINT'));
  process.on('SIGTERM', () => void shutdown('SIGTERM'));
  process.on('unhandledRejection', (reason) => logger.error('unhandled rejection', { reason: String(reason) }));
  process.on('uncaughtException', (err) => {
    logger.error('uncaught exception', { err: describeError(err), stack: err.stack });
    process.exit(1);
  });
}

main().catch((err) => {
  logger.error('failed to start server', { err: describeError(err) });
  if (!env.isProd && err?.stack) console.error(err.stack);
  process.exit(1);
});
