import { createHttpTerminator } from 'http-terminator';
import { setTimeout as setTimeoutAsync } from 'timers/promises';
import Sentry from '../external-services/sentry.js';
import { getAsyncStorage, logger, runWithAsyncStorage } from '../server-utils/logger.js';
import { SocketEvent } from '../server-utils/socketMessageHandler.js';

let shuttingDown = false;
const shutdownCallbacks: (() => any)[] = [];

const gracePeriod = 1000 * (Number(process.env.SHUTDOWN_TIMEOUT) || 25);

export function registerShutdownHandler(func: () => any): void {
  shutdownCallbacks.push(func);
}

export function registerHTTPServerForTermination(server) {
  const terminator = createHttpTerminator({ server });
  registerShutdownHandler(() => terminator.terminate());
}

export function installMessageHandlerWithShutdownSemaphore(server, messageHandler) {
  const terminator = createHttpTerminator({ server: server.httpServer });
  const activeSocketConnections = new Map();

  // See https://github.com/socketio/socket.io/issues/1602
  registerShutdownHandler(async () => {
    await Promise.all(Array.from(activeSocketConnections.values()));
    console.log('all websocket requests completed');
    for (const sock of activeSocketConnections.keys()) {
      try {
        sock.disconnect();
      } catch (e) {
        console.log('exception disconnecting websocket:');
        console.error(e);
      }
    }
    terminator.terminate();
  });

  server.on('connection', (socket) => {
    if (shuttingDown) {
      logger.info('got socket connection while shutting down - immediate destroy');
      socket.destroy();
      return;
    }
    logger.info('got socket connection');
    const asyncStorage = getAsyncStorage();
    activeSocketConnections.set(socket, false);
    socket.on('error', (error) => {
      Sentry.captureException(error);
    });
    socket.on('disconnect', (reason) => {
      runWithAsyncStorage(asyncStorage, () => {
        logger.info('websocket disconnecting for reason', reason);
        activeSocketConnections.delete(socket);
      });
    });
    socket.on('message', (message) => {
      runWithAsyncStorage(asyncStorage, async () => {
        if (shuttingDown) {
          socket.emit(SocketEvent.Final, { error: true, message: 'Server shutting down' });
          return;
        }
        let finished;
        try {
          activeSocketConnections.set(
            socket,
            new Promise((resolve) => {
              finished = resolve;
            })
          );
          await messageHandler(socket, message);
        } finally {
          finished(null);
        }
      });
    });
  });
}

export async function gracefulShutdown() {
  if (shuttingDown) {
    return;
  }
  shuttingDown = true;
  const res = await Promise.race([
    Promise.all(shutdownCallbacks.map(async (f) => await f())),
    setTimeoutAsync(gracePeriod, 'timeout'),
  ]);
  if (res === 'timeout') {
    console.log('timed out waiting for graceful shutdown callbacks');
    process.exit(1);
  }
  console.log('all shutdown callbacks completed');
  process.exit();
}

function handleSignal(signal) {
  if (shuttingDown) {
    console.log('got signal', signal, 'during shutdown - exit immediately');
    process.exit();
  }
  console.log('got signal', signal, '- shutting down');
  gracefulShutdown();
}

function handleUncaughtException(e) {
  console.error(e);
  Sentry.captureException(e);
  process.exitCode = 1;
  console.log('shutting down on uncaught exception');
  gracefulShutdown();
}

function handleUnhandledRejection(e) {
  // In several places we intentionally do not await a promise, expecting it to
  // complete work after a request has been responded to. Rejection of such promises
  // should not cause the server to crash.
  console.log('unhandled rejection');
  console.error(e);
  Sentry.captureException(e);
}

export function installPlatformEventHandlers() {
  process.on('SIGTERM', handleSignal);
  process.on('SIGINT', handleSignal);
  process.on('uncaughtException', handleUncaughtException);
  process.on('unhandledRejection', handleUnhandledRejection);
}
