import type { Socket } from "socket.io";
import { Server as SocketIOServer } from "socket.io";
import type { Server as HTTPServer } from "http";
import { drizzle } from "drizzle-orm/postgres-js";
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "~/database/schema";
import { eq, and, asc } from "drizzle-orm";
import { logger } from "./logger";

// Lazy-initialized DB for socket-level validation queries (team ownership checks)
let _socketDb: PostgresJsDatabase<typeof schema> | null = null;
function getSocketDb(): PostgresJsDatabase<typeof schema> {
  if (!_socketDb) {
    const url = process.env.DATABASE_URL;
    if (!url) throw new Error("DATABASE_URL is required");
    _socketDb = drizzle(postgres(url), { schema });
  }
  return _socketDb;
}

// Socket event types
interface ServerToClientEvents {
  "test-message": (data: {
    originalMessage: unknown;
    serverResponse: string;
    serverTimestamp: string;
    socketId: string;
  }) => void;
  "pick-made": (data: unknown) => void;
  "draft-started": (data: { seasonId: string; currentPickNumber: number }) => void;
  "draft-completed": () => void;
  "timer-update": (data: {
    seasonId: string;
    teamId: string;
    timeRemaining: number;
    currentPickNumber: number;
  }) => void;
  "autodraft-updated": (data: {
    teamId: string;
    isEnabled: boolean;
    mode: "next_pick" | "while_on";
    queueOnly: boolean;
    source?: "commissioner" | "user";
  }) => void;
  "team-connected": (data: { teamId: string }) => void;
  "team-disconnected": (data: { teamId: string }) => void;
  "connected-teams-list": (data: { teamIds: string[] }) => void;
  "draft-paused": (data: { seasonId: string; paused: boolean }) => void;
  "draft-resumed": (data: { seasonId: string; paused: boolean }) => void;
  "participant-removed-from-queues": (data: { participantId: string }) => void;
  "queue-updated": (data: { queue: Array<{ id: string; teamId: string; seasonId: string; participantId: string; queuePosition: number }> }) => void;
  "draft-state-sync": (data: {
    currentPickNumber: number;
    isPaused: boolean;
    status: string;
    picks: Array<{
      id: string;
      pickNumber: number;
      round: number;
      pickInRound: number;
      timeUsed: number;
      team: unknown;
      participant: unknown;
      sport: unknown;
    }>;
    timers: Array<{
      teamId: string;
      timeRemaining: number;
    }>;
    queue?: Array<{
      id: string;
      teamId: string;
      seasonId: string;
      participantId: string;
      queuePosition: number;
    }>;
  }) => void;
}

interface ClientToServerEvents {
  "join-draft": (seasonId: string, teamId?: string) => void;
  "leave-draft": (seasonId: string) => void;
  "test-event": (data: unknown) => void;
}

// Global type augmentation
declare global {
  var __socketIO: SocketIOServer | undefined;
}

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

// Track connected teams per season (in-memory, single-instance only).
// If the server restarts or runs as multiple instances this map resets.
// For multi-instance deployments, replace with a Redis-backed Socket.IO adapter.
const connectedTeams = new Map<string, Set<string>>(); // seasonId -> Set<teamId>

/**
 * Initialize Socket.IO server
 */
export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer {
  if (io) {
    logger.log("Socket.IO already initialized");
    return io;
  }

  // Create typed Socket.IO server
  io = new SocketIOServer<ClientToServerEvents, ServerToClientEvents>(httpServer, {
    // Faster dead-connection detection than the defaults (20s/25s).
    // pingTimeout: how long to wait for a pong before declaring the connection dead.
    // pingInterval: how often to send a ping.
    // Together these detect a crashed server in ~15s instead of ~45s.
    // Trade-off: more heartbeat traffic at scale; revisit if server load becomes a concern.
    pingTimeout: 5000,
    pingInterval: 10000,
    cors: process.env.NODE_ENV === "production" && process.env.APP_URL
      ? {
          origin: process.env.APP_URL,
          credentials: true,
        }
      : undefined,
  });

  // Connection handling
  io.on("connection", (socket: Socket<ClientToServerEvents, ServerToClientEvents>) => {
    logger.log("Client connected:", socket.id);

    // Store team ID for this socket
    let currentTeamId: string | undefined;
    let currentSeasonId: string | undefined;

    socket.on("join-draft", async (seasonId: string, teamId?: string) => {
      if (!seasonId) {
        logger.error("No seasonId provided for join-draft");
        return;
      }
      socket.join(`draft-${seasonId}`);
      currentSeasonId = seasonId;
      logger.log(`Socket ${socket.id} joined draft-${seasonId}`);

      // If teamId provided, validate it belongs to this season before tracking
      if (teamId) {
        try {
          const db = getSocketDb();
          const team = await db.query.teams.findFirst({
            where: and(eq(schema.teams.id, teamId), eq(schema.teams.seasonId, seasonId)),
          });
          if (!team) {
            logger.warn(`[Socket] join-draft rejected: team ${teamId} does not belong to season ${seasonId}`);
            return;
          }
        } catch (err) {
          logger.error("[Socket] join-draft team validation failed:", err);
          return;
        }

        currentTeamId = teamId;
        socket.join(`team-${teamId}`);

        // Initialize the season's connected teams set if it doesn't exist
        if (!connectedTeams.has(seasonId)) {
          connectedTeams.set(seasonId, new Set());
        }

        // Get current connected teams for this season
        const seasonConnectedTeams = connectedTeams.get(seasonId) ?? new Set<string>();
        if (!connectedTeams.has(seasonId)) connectedTeams.set(seasonId, seasonConnectedTeams);

        // Send the list of already-connected teams to this socket
        socket.emit("connected-teams-list", { teamIds: Array.from(seasonConnectedTeams) });

        // Add this team to the connected teams set
        seasonConnectedTeams.add(teamId);

        // Broadcast to everyone else that this team connected
        io?.to(`draft-${seasonId}`).emit("team-connected", { teamId });
        logger.log(`Team ${teamId} connected to draft-${seasonId}. Total connected: ${seasonConnectedTeams.size}`);
      }

      // Send full draft state to the joining client so it can sync immediately.
      // This is critical for mobile reconnection: the client may have been in the
      // background for minutes/hours and missed many picks. The revalidate() path
      // (HTTP loader re-fetch) covers this too, but it can fail due to expired auth
      // tokens or flaky mobile networks. This socket-based sync provides an
      // additional, more reliable path since the socket is already connected.
      try {
        const db = getSocketDb();
        const [seasonData, picks, timerRows, queueItems] = await Promise.all([
          db.query.seasons.findFirst({
            where: eq(schema.seasons.id, seasonId),
          }),
          db
            .select({
              id: schema.draftPicks.id,
              pickNumber: schema.draftPicks.pickNumber,
              round: schema.draftPicks.round,
              pickInRound: schema.draftPicks.pickInRound,
              timeUsed: schema.draftPicks.timeUsed,
              team: schema.teams,
              participant: schema.participants,
              sport: schema.sports,
            })
            .from(schema.draftPicks)
            .innerJoin(schema.teams, eq(schema.draftPicks.teamId, schema.teams.id))
            .innerJoin(
              schema.participants,
              eq(schema.draftPicks.participantId, schema.participants.id)
            )
            .innerJoin(
              schema.sportsSeasons,
              eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id)
            )
            .innerJoin(
              schema.sports,
              eq(schema.sportsSeasons.sportId, schema.sports.id)
            )
            .where(eq(schema.draftPicks.seasonId, seasonId))
            .orderBy(asc(schema.draftPicks.pickNumber)),
          db.query.draftTimers.findMany({
            where: eq(schema.draftTimers.seasonId, seasonId),
          }),
          teamId
            ? db.query.draftQueue.findMany({
                where: and(
                  eq(schema.draftQueue.teamId, teamId),
                  eq(schema.draftQueue.seasonId, seasonId)
                ),
                orderBy: asc(schema.draftQueue.queuePosition),
              })
            : Promise.resolve([]),
        ]);

        if (seasonData) {
          socket.emit("draft-state-sync", {
            currentPickNumber: seasonData.currentPickNumber || 1,
            isPaused: seasonData.draftPaused || false,
            status: seasonData.status,
            picks,
            timers: timerRows.map((t) => ({
              teamId: t.teamId,
              timeRemaining: t.timeRemaining,
            })),
            queue: teamId ? queueItems : undefined,
          });
        }
      } catch (err) {
        logger.error("[Socket] draft-state-sync query failed:", err);
        // Non-fatal — the client will fall back to HTTP revalidation
      }
    });

    socket.on("leave-draft", (seasonId: string) => {
      if (!seasonId) return;
      socket.leave(`draft-${seasonId}`);
      logger.log(`Socket ${socket.id} left draft-${seasonId}`);

      // Emit disconnection event if team was tracked
      if (currentTeamId) {
        socket.leave(`team-${currentTeamId}`);

        // Remove team from connected teams tracking
        const seasonConnectedTeams = connectedTeams.get(seasonId);
        if (seasonConnectedTeams) {
          seasonConnectedTeams.delete(currentTeamId);
          logger.log(`Team ${currentTeamId} removed from tracking. Remaining: ${seasonConnectedTeams.size}`);

          // Clean up empty sets
          if (seasonConnectedTeams.size === 0) {
            connectedTeams.delete(seasonId);
          }
        }

        io?.to(`draft-${seasonId}`).emit("team-disconnected", { teamId: currentTeamId });
        logger.log(`Team ${currentTeamId} disconnected from draft-${seasonId}`);
      }
    });

    socket.on("test-event", (data: unknown) => {
      logger.log("📨 Received test-event from client:", socket.id, data);
      
      socket.emit("test-message", {
        originalMessage: data,
        serverResponse: "Hello from server!",
        serverTimestamp: new Date().toISOString(),
        socketId: socket.id,
      });
      
      logger.log("✅ Sent test-message response to client:", socket.id);
    });

    socket.on("disconnect", () => {
      logger.log("Client disconnected:", socket.id);

      // Emit disconnection event if team was tracked
      if (currentTeamId && currentSeasonId) {
        // Remove team from connected teams tracking
        const seasonConnectedTeams = connectedTeams.get(currentSeasonId);
        if (seasonConnectedTeams) {
          seasonConnectedTeams.delete(currentTeamId);
          logger.log(`Team ${currentTeamId} removed from tracking on disconnect. Remaining: ${seasonConnectedTeams.size}`);

          // Clean up empty sets
          if (seasonConnectedTeams.size === 0) {
            connectedTeams.delete(currentSeasonId);
          }
        }

        io?.to(`draft-${currentSeasonId}`).emit("team-disconnected", { teamId: currentTeamId });
        logger.log(`Team ${currentTeamId} disconnected from draft-${currentSeasonId}`);
      }
    });
  });

  // Store globally for route handlers
  global.__socketIO = io;

  logger.log("Socket.IO initialized");

  // Start the draft timer system (async import but don't await)
  import("./timer").then(({ startDraftTimerSystem }) => {
    startDraftTimerSystem();
  }).catch((error) => {
    logger.error("Failed to start timer system:", error);
  });

  // Start the daily snapshot system
  import("./snapshots").then(({ startSnapshotSystem }) => {
    startSnapshotSystem();
  }).catch((error) => {
    logger.error("Failed to start snapshot system:", error);
  });

  return io;
}

/**
 * Get the Socket.IO server instance
 */
export function getSocketIO(): SocketIOServer {
  const instance = io || global.__socketIO;
  if (!instance) {
    throw new Error("Socket.IO not initialized. Call initializeSocketIO first.");
  }
  return instance;
}
