import { createServer } from "http";
import { Server as SocketIOServer } from "socket.io";
import type { IncomingMessage, ServerResponse } from "http";
import app, { sessionMiddleware } from "./app";
import { logger } from "./lib/logger";
import { ensureSystemSettingsBootstrap } from "./lib/setupService";

const rawPort = process.env["PORT"];

if (!rawPort) {
  throw new Error(
    "PORT environment variable is required but was not provided.",
  );
}

const port = Number(rawPort);

if (Number.isNaN(port) || port <= 0) {
  throw new Error(`Invalid PORT value: "${rawPort}"`);
}

const httpServer = createServer(app);

export const io = new SocketIOServer(httpServer, {
  path: "/api/socket.io",
  // #632: Detect dead connections faster. Defaults are 25s/20s which
  // means a silently-dropped WebSocket (Tailscale NAT timeout, iPad
  // PWA suspension, proxy hiccup) takes up to ~45s before the client
  // notices and reconnects — long enough that recipients miss the
  // realtime order push entirely. Tightening to 10s/5s puts the
  // detection cycle at ~15s, well inside the "is this app even
  // working?" window users complain about.
  pingInterval: 10000,
  pingTimeout: 5000,
  cors: {
    origin: process.env.ALLOWED_ORIGINS
      ? process.env.ALLOWED_ORIGINS.split(",").map((o) => o.trim())
      : true,
    methods: ["GET", "POST"],
    credentials: true,
  },
});

io.engine.use(
  (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => {
    sessionMiddleware(req as Parameters<typeof sessionMiddleware>[0], res as Parameters<typeof sessionMiddleware>[1], next);
  },
);

io.use((socket, next) => {
  const req = socket.request as IncomingMessage & { session?: { userId?: number } };
  const userId = req.session?.userId;
  if (!userId) {
    return next(new Error("Unauthorized: no session"));
  }
  socket.data.userId = userId;
  next();
});

io.on("connection", (socket) => {
  const userId = socket.data.userId as number;
  logger.info({ socketId: socket.id, userId }, "Socket connected");

  socket.join(`user:${userId}`);

  // #632: Client foreground-recovery probe. The web client emits this
  // on `visibilitychange` to verify the WebSocket isn't half-open
  // (silently dead behind Tailscale/NAT) — we ack immediately so a
  // genuinely-alive connection completes the round-trip, and a dead
  // one times out on the client side and force-cycles the transport.
  socket.on("client_health_probe", (ack?: () => void) => {
    if (typeof ack === "function") ack();
  });

  socket.on("disconnect", () => {
    logger.info({ socketId: socket.id }, "Socket disconnected");
  });
});

// Run install-state bootstrap before accepting traffic so the wizard
// gate is always consistent with the DB on the first request, even
// when the schema was applied without running the seed script.
ensureSystemSettingsBootstrap()
  .catch((err) => {
    logger.error({ err }, "ensureSystemSettingsBootstrap failed (continuing)");
  })
  .finally(() => {
    httpServer.listen(port, () => {
      logger.info({ port }, "Server listening");
      // Fire-and-forget background tick: every 60s, scan today's
      // meetings and push the 5-minute reminder to attendees whose
      // alert hasn't been pushed yet. See #558.
      import("./lib/executive-meeting-scheduler.js")
        .then((m) => m.startExecutiveMeetingScheduler())
        .catch((err) => {
          logger.error({ err }, "Failed to start executive-meeting scheduler");
        });
    });
  });
