import { Server } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import type { Server as HTTPServer } from 'node:http';
import { and, eq } from 'drizzle-orm';

import { redis } from '@/lib/redis';
import { auth } from '@/lib/auth';
import { db } from '@/lib/db';
import { userProfiles, userPortRoles } from '@/lib/db/schema/users';
import { berths } from '@/lib/db/schema/berths';
import { clients } from '@/lib/db/schema/clients';
import { interests } from '@/lib/db/schema/interests';
import { logger } from '@/lib/logger';
import type { ServerToClientEvents, ClientToServerEvents } from './events';

let io: Server<ClientToServerEvents, ServerToClientEvents> | null = null;

/**
 * Returns true if the user is a super-admin OR holds a userPortRoles row
 * for the given portId. The Socket.IO auth middleware uses this to decide
 * whether to honour a client-supplied `auth.portId` - the prior code
 * trusted whatever the client passed and thereby joined the socket to a
 * foreign tenant's broadcast room.
 */
async function userCanAccessPort(userId: string, portId: string): Promise<boolean> {
  const profile = await db.query.userProfiles.findFirst({
    where: eq(userProfiles.userId, userId),
  });
  if (profile?.isSuperAdmin) return true;
  const role = await db.query.userPortRoles.findFirst({
    where: and(eq(userPortRoles.userId, userId), eq(userPortRoles.portId, portId)),
  });
  return Boolean(role);
}

/**
 * Verify the user can join an entity-scoped room. Each entity type's own
 * tenant column is checked - if the user can access the entity's port,
 * they may subscribe to that entity's room.
 */
async function userCanJoinEntity(
  userId: string,
  type: 'berth' | 'client' | 'interest',
  id: string,
): Promise<boolean> {
  const profile = await db.query.userProfiles.findFirst({
    where: eq(userProfiles.userId, userId),
  });
  if (profile?.isSuperAdmin) return true;

  let entityPortId: string | null = null;
  if (type === 'berth') {
    const row = await db.query.berths.findFirst({ where: eq(berths.id, id) });
    entityPortId = row?.portId ?? null;
  } else if (type === 'client') {
    const row = await db.query.clients.findFirst({ where: eq(clients.id, id) });
    entityPortId = row?.portId ?? null;
  } else if (type === 'interest') {
    const row = await db.query.interests.findFirst({ where: eq(interests.id, id) });
    entityPortId = row?.portId ?? null;
  }
  if (!entityPortId) return false;

  const role = await db.query.userPortRoles.findFirst({
    where: and(eq(userPortRoles.userId, userId), eq(userPortRoles.portId, entityPortId)),
  });
  return Boolean(role);
}

export function initSocketServer(
  httpServer: HTTPServer,
): Server<ClientToServerEvents, ServerToClientEvents> {
  const pubClient = redis.duplicate();
  const subClient = redis.duplicate();

  io = new Server<ClientToServerEvents, ServerToClientEvents>(httpServer, {
    path: '/socket.io/',
    adapter: createAdapter(pubClient, subClient),
    cors: {
      origin: process.env.APP_URL,
      credentials: true,
    },
    connectionStateRecovery: { maxDisconnectionDuration: 2 * 60 * 1000 },
    maxHttpBufferSize: 1e6, // 1MB message limit
  });

  // Auth middleware - validate session cookie + verify the user actually
  // holds a role in the requested port. The handshake's auth.portId is
  // user-supplied; we MUST cross-check it against userPortRoles or any
  // authenticated user could subscribe to a foreign tenant's broadcasts.
  io.use(async (socket, next) => {
    try {
      const cookie = socket.handshake.headers.cookie;
      if (!cookie) return next(new Error('Authentication required'));

      // Parse session from cookie
      const session = await auth.api.getSession({
        headers: new Headers({ cookie }),
      });
      if (!session?.user) return next(new Error('Invalid session'));

      // Enforce max 10 connections per user
      const userSockets = await io!.in(`user:${session.user.id}`).fetchSockets();
      if (userSockets.length >= 10) {
        return next(new Error('Maximum connections reached'));
      }

      const requestedPortId = socket.handshake.auth.portId as string | undefined;
      if (requestedPortId) {
        const allowed = await userCanAccessPort(session.user.id, requestedPortId);
        if (!allowed) {
          return next(new Error('No access to this port'));
        }
      }

      socket.data = {
        userId: session.user.id,
        portId: requestedPortId,
      };
      next();
    } catch {
      next(new Error('Authentication failed'));
    }
  });

  // Connection handler
  io.on('connection', (socket) => {
    const { userId, portId } = socket.data as { userId: string; portId: string | undefined };
    logger.debug({ userId, portId }, 'Socket connected');

    // Auto-join personal and port rooms
    socket.join(`user:${userId}`);
    if (portId) socket.join(`port:${portId}`);

    // Entity-level room management - verify the user can access the
    // entity's port before joining. Without this, any authenticated user
    // could subscribe to a foreign-tenant entity's broadcast (note
    // previews, signer emails, etc.) by guessing or harvesting an id.
    socket.on('join:entity', async ({ type, id }) => {
      try {
        const ok = await userCanJoinEntity(userId, type, id);
        if (ok) socket.join(`${type}:${id}`);
        else logger.warn({ userId, type, id }, 'Socket denied join:entity');
      } catch (err) {
        logger.warn({ err, userId, type, id }, 'join:entity check failed');
      }
    });
    socket.on('leave:entity', ({ type, id }) => {
      socket.leave(`${type}:${id}`);
    });

    // NOTE: there is no manual idle-disconnect timer here. The previous
    // implementation forcibly disconnected every socket after 30s of
    // client-emitted activity, which (a) would have torn down every
    // dashboard connection in production every half minute (the server-
    // push events don't reset the timer because `onAny` fires only on
    // inbound messages from the client) and (b) was redundant — Socket.IO's
    // built-in pingTimeout / pingInterval handles dead-transport detection
    // at the protocol level.

    socket.on('disconnect', () => {
      logger.debug({ userId }, 'Socket disconnected');
    });
  });

  return io;
}

export function getIO(): Server<ClientToServerEvents, ServerToClientEvents> {
  if (!io) throw new Error('Socket.io not initialized');
  return io;
}

/**
 * Idempotent shutdown for graceful-stop on SIGTERM/SIGINT — closes the
 * Socket.io server and (if present) its Redis adapter pub/sub clients so
 * the process can exit cleanly.
 */
export async function closeSocketServer(): Promise<void> {
  if (!io) return;
  await new Promise<void>((resolve) => io!.close(() => resolve()));
  io = null;
}

/**
 * Emit an event to a specific room. Used by service layer after mutations.
 */
export function emitToRoom<E extends keyof ServerToClientEvents>(
  room: string,
  event: E,
  ...args: Parameters<ServerToClientEvents[E]>
): void {
  if (!io) return;
  io.to(room).emit(event, ...args);
}
