import { useLoaderData, Link, useRevalidator } from "react-router";
import { useAuth } from "@clerk/react-router";
import { eq, and, asc, desc, inArray } from "drizzle-orm";
import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { useDraftSocket } from "~/hooks/useDraftSocket";
import { logger } from "~/lib/logger";
import { useCallback, useEffect, useState, useMemo, useRef } from "react";
import { Button } from "~/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "~/components/ui/dialog";
import { getAuth } from "@clerk/react-router/server";
import { getTeamQueue } from "~/models/draft-queue";
import { buildOwnerMap } from "~/lib/owner-map";
import { DraftSidebar } from "~/components/DraftSidebar";
import { TeamRosterView } from "~/components/draft/TeamRosterView";
import { DraftSummaryView } from "~/components/draft/DraftSummaryView";
import { QueueSection } from "~/components/draft/QueueSection";
import { AvailableParticipantsSection } from "~/components/draft/AvailableParticipantsSection";
import { SidebarRecentPicks } from "~/components/draft/SidebarRecentPicks";
import { DraftGridSection } from "~/components/draft/DraftGridSection";
import { ConnectionOverlay } from "~/components/draft/ConnectionOverlay";
import { AuthRecoveryOverlay } from "~/components/draft/AuthRecoveryOverlay";
import { ParticipantSelectionDialog } from "~/components/draft/ParticipantSelectionDialog";
import { calculateDraftEligibility } from "~/lib/draft-eligibility";
import { getTeamForPick } from "~/lib/draft-order";
import { useDraftNotifications } from "~/hooks/useDraftNotifications";
import { useMediaQuery } from "~/hooks/useMediaQuery";
import { NotificationSettings } from "~/components/NotificationSettings";
import { AutodraftSettings, getAutodraftLabel } from "~/components/AutodraftSettings";
import { toast } from "sonner";
import { formatClockTime, getTimerColorClass } from "~/lib/draft-timer";
import { Users, LayoutGrid, ListChecks, Settings, ListOrdered } from "lucide-react";
import type { Route } from "./+types/$leagueId.draft.$seasonId";

export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
  return [{ title: `Draft — ${data?.season?.league?.name ?? "League"} - Brackt` }];
}

type QueueItem = typeof schema.draftQueue.$inferSelect;

type AutodraftStatusEntry = {
  isEnabled: boolean;
  mode: "next_pick" | "while_on";
  queueOnly: boolean;
};

const MOBILE_TABS_BASE = [
  { id: "available" as const, label: "Available", Icon: Users },
  { id: "board" as const, label: "Board", Icon: LayoutGrid },
  { id: "teams" as const, label: "Teams", Icon: ListChecks },
  { id: "controls" as const, label: "Controls", Icon: Settings },
];

const QUEUE_TAB = { id: "queue" as const, label: "Queue", Icon: ListOrdered };

export async function loader(args: Route.LoaderArgs) {
  const { params } = args;
  const { seasonId, leagueId } = params;
  const { userId } = await getAuth(args);

  if (!seasonId) {
    throw new Response("Season ID is required", { status: 400 });
  }

  const db = database();

  // Get season details
  const season = await db.query.seasons.findFirst({
    where: eq(schema.seasons.id, seasonId),
    with: {
      league: true,
      teams: true,
    },
  });

  if (!season) {
    throw new Response("Season not found", { status: 404 });
  }

  // Resolve user's team and commissioner status (null userId means neither)
  const userTeam = userId
    ? season.teams.find((team) => team.ownerId === userId)
    : undefined;

  const isCommissioner = userId
    ? await db.query.commissioners.findFirst({
        where: and(
          eq(schema.commissioners.leagueId, leagueId),
          eq(schema.commissioners.userId, userId)
        ),
      })
    : null;

  // Access control: if draft board is not public, require authentication and membership
  if (!season.league.isPublicDraftBoard) {
    if (!userId) {
      throw new Response("You must be logged in to view the draft room", {
        status: 401,
      });
    }
    if (!userTeam && !isCommissioner) {
      throw new Response("You do not have access to this draft room", {
        status: 403,
      });
    }
  }

  // Get draft slots (draft order) - using select instead of query builder
  const draftSlots = await db
    .select({
      id: schema.draftSlots.id,
      draftOrder: schema.draftSlots.draftOrder,
      team: schema.teams,
    })
    .from(schema.draftSlots)
    .innerJoin(schema.teams, eq(schema.draftSlots.teamId, schema.teams.id))
    .where(eq(schema.draftSlots.seasonId, seasonId))
    .orderBy(asc(schema.draftSlots.draftOrder));

  // Get all draft picks - using select instead of query builder
  const draftPicks = await 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));

  // Get sports seasons for this season
  const seasonSports = await db.query.seasonSports.findMany({
    where: eq(schema.seasonSports.seasonId, seasonId),
  });

  const sportsSeasonIds = seasonSports.map((ss) => ss.sportsSeasonId);

  // Get ALL participants (both drafted and undrafted) - sorted by EV desc, then name
  // Filtering will be done on the client side based on the "Show Drafted" toggle
  const availableParticipants = sportsSeasonIds.length > 0
    ? await db
        .select({
          id: schema.participants.id,
          name: schema.participants.name,
          sport: schema.sports,
        })
        .from(schema.participants)
        .innerJoin(
          schema.sportsSeasons,
          eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id)
        )
        .innerJoin(
          schema.sports,
          eq(schema.sportsSeasons.sportId, schema.sports.id)
        )
        .where(
          sportsSeasonIds.length === 1
            ? eq(schema.participants.sportsSeasonId, sportsSeasonIds[0])
            : inArray(schema.participants.sportsSeasonId, sportsSeasonIds)
        )
        .orderBy(
          desc(schema.participants.expectedValue),
          asc(schema.participants.name)
        )
    : [];

  // Load user's team queue if they have a team
  const userQueue = userTeam
    ? await getTeamQueue(userTeam.id).catch((err) => {
        logger.error("[DraftLoader] Failed to load queue, defaulting to empty:", err);
        return [];
      })
    : [];

  // Load draft timers for all teams
  const timers = await db.query.draftTimers.findMany({
    where: eq(schema.draftTimers.seasonId, seasonId),
  });

  // Load autodraft settings for all teams
  const autodraftSettings = await db.query.autodraftSettings.findMany({
    where: eq(schema.autodraftSettings.seasonId, seasonId),
  });

  // Derive user's autodraft settings from the already-loaded list
  const userAutodraftSettings = userTeam
    ? (autodraftSettings.find((s) => s.teamId === userTeam.id) ?? null)
    : null;

  const ownerMap = await buildOwnerMap(draftSlots);

  return {
    season,
    draftSlots,
    draftPicks,
    availableParticipants,
    userTeam,
    userQueue,
    timers,
    autodraftSettings,
    userAutodraftSettings,
    isCommissioner: !!isCommissioner,
    currentUserId: userId,
    ownerMap,
  };
}

export default function DraftRoom() {
  const {
    season,
    draftSlots,
    draftPicks,
    availableParticipants,
    userTeam,
    userQueue,
    timers,
    autodraftSettings,
    userAutodraftSettings,
    isCommissioner,
    currentUserId,
    ownerMap,
  } = useLoaderData<typeof loader>();
  const { revalidate, state: revalidatorState } = useRevalidator();
  const { userId: clerkUserId, getToken } = useAuth();
  const { isConnected, connectionError, isReconnecting, reconnectCount, socketVersion, on, off } = useDraftSocket(season.id, userTeam?.id);
  const {
    permissionState: notificationsPermission,
    enabled: notificationsEnabled,
    setEnabled: setNotificationsEnabled,
    mode: notificationsMode,
    setMode: setNotificationsMode,
    sendNotification,
  } = useDraftNotifications(season.id, currentUserId ?? "");

  // Use refs so the socket effect doesn't re-register all handlers when the user
  // changes notification settings (which would change the sendNotification reference).
  const sendNotificationRef = useRef(sendNotification);
  useEffect(() => {
    sendNotificationRef.current = sendNotification;
  }, [sendNotification]);
  const notificationsModeRef = useRef(notificationsMode);
  useEffect(() => {
    notificationsModeRef.current = notificationsMode;
  }, [notificationsMode]);

  // Track current autodraft state for detecting server-initiated auto-disable
  const [userAutodraft, setUserAutodraft] = useState({
    isEnabled: userAutodraftSettings?.isEnabled || false,
    mode: (userAutodraftSettings?.mode || "next_pick") as "next_pick" | "while_on",
    queueOnly: userAutodraftSettings?.queueOnly || false,
  });
  const userAutodraftRef = useRef(userAutodraft);
  useEffect(() => {
    userAutodraftRef.current = userAutodraft;
  }, [userAutodraft]);

  // Re-fetch loader data after each reconnect so stale picks/timers are refreshed.
  // Schedule a delayed retry so that if the first attempt fails (common on mobile
  // where the network may not be fully restored yet), we get another chance.
  const revalidationRetryRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  useEffect(() => {
    if (reconnectCount > 0) {
      revalidate();
      // Clear any pending retry from a previous reconnect
      if (revalidationRetryRef.current) {
        clearTimeout(revalidationRetryRef.current);
      }
      revalidationRetryRef.current = setTimeout(() => {
        revalidate();
        revalidationRetryRef.current = null;
      }, 3000);
    }
    return () => {
      if (revalidationRetryRef.current) {
        clearTimeout(revalidationRetryRef.current);
      }
    };
  }, [reconnectCount, revalidate]);

  // Guard against Clerk JWT expiry race: if the loader ran without a userId
  // (expired short-lived JWT) but the Clerk client SDK has a valid session,
  // revalidate so the loader re-runs with a fresh token and returns correct
  // userTeam/userQueue data. This can happen repeatedly on mobile when the
  // user backgrounds and foregrounds the app, so we don't limit it to a
  // single attempt. Instead we debounce to avoid rapid-fire revalidations.
  //
  // Cap at 3 attempts to avoid an infinite loop if the server consistently
  // fails to resolve a userId (e.g. clock skew, misconfigured Clerk keys).
  // After 3 failures we fall through to the authDegraded overlay.
  const authRecoveryTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
  const authRecoveryAttemptsRef = useRef(0);
  const MAX_AUTH_RECOVERY_ATTEMPTS = 3;
  useEffect(() => {
    if (clerkUserId && !currentUserId) {
      if (authRecoveryAttemptsRef.current >= MAX_AUTH_RECOVERY_ATTEMPTS) {
        setAuthDegraded(true);
        return;
      }
      // Small debounce so we don't fire multiple times in quick succession
      // (e.g. visibility change + socket reconnect both updating deps).
      clearTimeout(authRecoveryTimerRef.current);
      authRecoveryTimerRef.current = setTimeout(() => {
        authRecoveryAttemptsRef.current += 1;
        revalidate();
      }, 100);
    } else if (currentUserId) {
      // Auth recovered — reset the counter for the next background cycle.
      authRecoveryAttemptsRef.current = 0;
    }
    return () => clearTimeout(authRecoveryTimerRef.current);
  }, [clerkUserId, currentUserId, revalidate]);

  // Auth session protection: proactively refresh the Clerk JWT to prevent
  // silent logout during long-running draft sessions.
  const [authDegraded, setAuthDegraded] = useState(false);

  // Fetch wrapper that detects 401 responses and shows the auth recovery
  // overlay instead of letting each handler deal with it individually.
  // Returns null on 401 so callers can bail with `if (!response) return;`.
  const authFetch = useCallback(async (url: string, init?: RequestInit): Promise<Response | null> => {
    const response = await fetch(url, init);
    if (response.status === 401) {
      setAuthDegraded(true);
      return null;
    }
    return response;
  }, []);

  // Refresh the Clerk JWT when the tab becomes visible again. Browsers
  // throttle/suspend intervals in background tabs, so the SDK's automatic
  // 60-second refresh may not have run. Getting a fresh token here prevents
  // the next user action from hitting a 401. After a successful refresh we
  // also revalidate the loader so it re-runs with the fresh token — without
  // this, userTeam/userQueue stay stale from the previous (possibly
  // unauthenticated) loader run.
  useEffect(() => {
    // Nothing to do once auth is fully degraded — avoid re-registering a
    // listener that would immediately exit on every future visibility change.
    if (authDegraded) return;

    let aborted = false;
    let inFlight = false;

    const handleVisibilityChange = async () => {
      // Ignore hide events, and skip if another invocation is already running
      // (rapid tab switching) or this effect has been cleaned up.
      if (document.visibilityState !== "visible" || !clerkUserId || inFlight) return;
      inFlight = true;
      try {
        const token = await getToken({ skipCache: true });
        if (aborted) return;
        if (token !== null) {
          // Token refreshed successfully. Only revalidate if the loader is
          // missing auth (currentUserId is null) — if the loader already has
          // a valid user there's no stale state to fix and the extra round
          // trip just adds unnecessary server load.
          if (!currentUserId) {
            revalidate();
          }
        } else {
          // Retry after a longer delay — mobile networks may take a few
          // seconds to fully wake up after background suspension.
          await new Promise((r) => setTimeout(r, 2500));
          if (aborted) return;
          const retryToken = await getToken({ skipCache: true });
          if (aborted) return;
          if (retryToken !== null) {
            revalidate();
          } else {
            setAuthDegraded(true);
          }
        }
      } catch {
        try {
          await new Promise((r) => setTimeout(r, 2500));
          if (aborted) return;
          const retryToken = await getToken({ skipCache: true });
          if (aborted) return;
          if (retryToken !== null) {
            revalidate();
          } else {
            setAuthDegraded(true);
          }
        } catch {
          if (!aborted) setAuthDegraded(true);
        }
      } finally {
        inFlight = false;
      }
    };
    document.addEventListener("visibilitychange", handleVisibilityChange);
    return () => {
      aborted = true;
      document.removeEventListener("visibilitychange", handleVisibilityChange);
    };
  }, [clerkUserId, currentUserId, getToken, authDegraded, revalidate]);

  // Track revalidation lifecycle to sync local state from fresh loader data.
  //
  // The problem: `picks` and `currentPick` are local state initialized once
  // from useLoaderData(). When revalidate() runs after a reconnect, React
  // Router re-fetches `draftPicks`/`season` — but those updates never flow
  // back into the local useState copies automatically.
  //
  // Race condition: a `pick-made` socket event that arrives *during* the
  // revalidation window would normally be applied to the stale prev state and
  // then overwritten when we sync from the DB snapshot. We prevent that by
  // buffering live picks while revalidation is in flight and merging them
  // (deduplicated by ID) after the DB snapshot lands.
  const revalidatorStateRef = useRef(revalidatorState);
  const isRevalidatingRef = useRef(false);
  const pendingPicksDuringRevalidationRef = useRef<(typeof draftPicks)[number][]>([]);
  // Track the draftPicks reference when revalidation starts. If it hasn't
  // changed by the time revalidation completes, the loader didn't return new
  // data (e.g. network failure) and we should NOT overwrite state that may
  // have been freshly set by draft-state-sync.
  const draftPicksAtRevalidationStartRef = useRef(draftPicks);
  // Same pattern for userQueue: only sync from loader if the reference changed,
  // meaning the fetch actually returned fresh data. Prevents overwriting
  // socket-applied queue changes (queue-updated, participant-removed-from-queues)
  // with stale loader data when a revalidation fetch fails.
  const userQueueAtRevalidationStartRef = useRef(userQueue);
  // Tracks how many queue mutations are in-flight from this window. While > 0,
  // incoming queue-updated socket events are ignored so that the server echo
  // doesn't clobber a more-recent optimistic update (e.g. rapid drag-reorders).
  // The initiating window uses the HTTP response as the authoritative state;
  // other windows use the socket event.
  const pendingQueueMutationsRef = useRef(0);

  useEffect(() => {
    const prev = revalidatorStateRef.current;
    revalidatorStateRef.current = revalidatorState;

    if (prev !== "loading" && revalidatorState === "loading") {
      // Revalidation just started — buffer any live picks to avoid loss
      isRevalidatingRef.current = true;
      pendingPicksDuringRevalidationRef.current = [];
      draftPicksAtRevalidationStartRef.current = draftPicks;
      userQueueAtRevalidationStartRef.current = userQueue;
    } else if (prev === "loading" && revalidatorState === "idle") {
      // Revalidation just completed — merge DB snapshot with buffered picks
      isRevalidatingRef.current = false;

      // If draftPicks is the same reference as when revalidation started, the
      // loader didn't actually update (likely a failed fetch). Skip the sync
      // to avoid overwriting fresh draft-state-sync data with stale state.
      if (draftPicks === draftPicksAtRevalidationStartRef.current) {
        pendingPicksDuringRevalidationRef.current = [];
        return;
      }

      const dbPickIds = new Set(draftPicks.map((p) => p.id));
      const missedPicks = pendingPicksDuringRevalidationRef.current.filter(
        (p) => !dbPickIds.has(p.id)
      );
      pendingPicksDuringRevalidationRef.current = [];

      setPicks([...draftPicks, ...missedPicks]);
      setCurrentPick(season.currentPickNumber || 1);
      setIsPaused(season.draftPaused || false);
      setIsDraftComplete(
        season.status === "active" || season.status === "completed"
      );
      // Sync the queue if the loader returned fresh data for it. Using reference
      // equality (same pattern as draftPicks above): drizzle always returns a new
      // array on a successful fetch, so a changed reference means the fetch
      // succeeded and we can trust the value. A same reference means the fetch
      // failed and we must NOT overwrite socket-applied changes (queue-updated,
      // participant-removed-from-queues) with stale data.
      //
      // Guard: skip if the loader ran unauthenticated (currentUserId null) to avoid
      // overwriting with an empty userQueue returned for an expired JWT session.
      if (currentUserId && userQueue !== userQueueAtRevalidationStartRef.current) {
        setQueue(userQueue);
      }
      // Sync timers — the server-side timer state may have changed while
      // we were disconnected (time bank adjustments, new timers, etc.)
      if (timers.length > 0) {
        setTeamTimers((currentTimers) => {
          const updated = { ...currentTimers };
          timers.forEach((timer) => {
            updated[timer.teamId] = timer.timeRemaining;
          });
          return updated;
        });
      }
      // Sync autodraft settings — teams may have toggled autodraft while
      // we were away.
      setAutodraftStatus(() => {
        const status: Record<string, AutodraftStatusEntry> = {};
        autodraftSettings.forEach((setting) => {
          status[setting.teamId] = {
            isEnabled: setting.isEnabled,
            mode: setting.mode,
            queueOnly: setting.queueOnly,
          };
        });
        return status;
      });
    }
  }, [revalidatorState, draftPicks, season, userQueue, timers, autodraftSettings, currentUserId]);

  const [picks, setPicks] = useState(draftPicks);
  const [currentPick, setCurrentPick] = useState(season.currentPickNumber || 1);
  const [searchQuery, setSearchQuery] = useState("");
  const [hideDrafted, setHideDrafted] = useState(true);
  const [hideIneligible, setHideIneligible] = useState(true);
  const [hideCompletedSports, setHideCompletedSports] = useState(false);
  const [sportFilters, setSportFilters] = useState<string[]>([]);
  const [queue, setQueue] = useState<QueueItem[]>(userQueue);
  const [isPaused, setIsPaused] = useState(season.draftPaused || false);
  const [isDraftComplete, setIsDraftComplete] = useState(
    season.status === "active" || season.status === "completed"
  );

  // Sidebar and tabs state
  const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
    if (typeof window === "undefined") return false;
    const stored = localStorage.getItem("draftSidebarCollapsed");
    return stored ? JSON.parse(stored) : false;
  });
  const [activeTab, setActiveTab] = useState<"participants" | "board" | "rosters" | "summary">("board");
  const [mobileTab, setMobileTab] = useState<"available" | "queue" | "board" | "teams" | "controls">(
    !userTeam && isCommissioner ? "board" : "available"
  );
  const [teamsSubTab, setTeamsSubTab] = useState<"rosters" | "summary">("rosters");
  const isMobile = useMediaQuery("(max-width: 767px)");

  // Track autodraft status for all teams
  const [autodraftStatus, setAutodraftStatus] = useState<Record<string, AutodraftStatusEntry>>(() => {
    const status: Record<string, AutodraftStatusEntry> = {};
    autodraftSettings.forEach((setting) => {
      status[setting.teamId] = {
        isEnabled: setting.isEnabled,
        mode: setting.mode,
        queueOnly: setting.queueOnly,
      };
    });
    return status;
  });

  // Track connected teams
  const [connectedTeams, setConnectedTeams] = useState<Set<string>>(new Set());

  const [teamTimers, setTeamTimers] = useState<Record<string, number>>(() => {
    // Initialize with timers from loader data, or use initial time if draft hasn't started
    const timersMap: Record<string, number> = {};
    const initialTime = season.draftInitialTime || 120;
    
    if (timers.length > 0) {
      // Draft has started, use actual timers
      timers.forEach((timer) => {
        timersMap[timer.teamId] = timer.timeRemaining;
      });
    } else {
      // Draft hasn't started, show initial time for all teams
      draftSlots.forEach((slot) => {
        timersMap[slot.team.id] = initialTime;
      });
    }
    
    return timersMap;
  });
  const [forcePickDialogOpen, setForcePickDialogOpen] = useState(false);
  const [selectedPickSlot, setSelectedPickSlot] = useState<{
    pickNumber: number;
    teamId: string;
  } | null>(null);
  // Replace pick dialog state
  const [replacePickDialogOpen, setReplacePickDialogOpen] = useState(false);
  const [replacePickSlot, setReplacePickSlot] = useState<{
    pickNumber: number;
    teamId: string;
    oldParticipantId: string;
  } | null>(null);

  // Rollback confirm dialog state
  const [rollbackConfirmOpen, setRollbackConfirmOpen] = useState(false);
  const [rollbackPickNumber, setRollbackPickNumber] = useState<number | null>(null);
  const [isRollingBack, setIsRollingBack] = useState(false);

  // Time bank adjustment dialog state
  const [timeBankDialogOpen, setTimeBankDialogOpen] = useState(false);
  const [timeBankTeamId, setTimeBankTeamId] = useState<string | null>(null);
  const [commissionerAutodraftDialogOpen, setCommissionerAutodraftDialogOpen] = useState(false);
  const [commissionerAutodraftTeamId, setCommissionerAutodraftTeamId] = useState<string | null>(null);
  const [timeBankAmount, setTimeBankAmount] = useState("1");
  const [timeBankUnit, setTimeBankUnit] = useState<"seconds" | "minutes" | "hours">("minutes");
  const [timeBankDirection, setTimeBankDirection] = useState<"add" | "remove">("add");
  const [isAdjustingTimeBank, setIsAdjustingTimeBank] = useState(false);

  // Shared transforms for eligibility calculations
  const transformedPicks = useMemo(
    () =>
      picks.map((p) => ({
        teamId: p.team.id,
        participant: {
          id: p.participant.id,
          sport: { id: p.sport.id, name: p.sport.name },
        },
      })),
    [picks]
  );

  const transformedParticipants = useMemo(
    () =>
      availableParticipants.map((p) => ({
        id: p.id,
        sport: { id: p.sport.id, name: p.sport.name },
      })),
    [availableParticipants]
  );

  const seasonSportsData = useMemo(() => {
    const sportsMap = new Map<string, { id: string; name: string }>();
    availableParticipants.forEach((p) => {
      if (!sportsMap.has(p.sport.id)) {
        sportsMap.set(p.sport.id, { id: p.sport.id, name: p.sport.name });
      }
    });
    return Array.from(sportsMap.values()).toSorted((a, b) =>
      a.name.localeCompare(b.name)
    );
  }, [availableParticipants]);

  // Calculate draft eligibility for current user's team
  const eligibility = useMemo(() => {
    if (!userTeam) return null;
    const userTeamPicks = transformedPicks.filter(
      (p) => p.teamId === userTeam.id
    );
    return calculateDraftEligibility(
      userTeam.id,
      userTeamPicks,
      transformedPicks,
      transformedParticipants,
      seasonSportsData,
      season.draftRounds,
      season.teams
    );
  }, [userTeam, transformedPicks, transformedParticipants, seasonSportsData, season]);

  // Calculate eligibility for force manual pick dialog (selected team)
  const forcePickEligibility = useMemo(() => {
    if (!selectedPickSlot) return null;
    const selectedTeamPicks = transformedPicks.filter(
      (p) => p.teamId === selectedPickSlot.teamId
    );
    return calculateDraftEligibility(
      selectedPickSlot.teamId,
      selectedTeamPicks,
      transformedPicks,
      transformedParticipants,
      seasonSportsData,
      season.draftRounds,
      season.teams
    );
  }, [selectedPickSlot, transformedPicks, transformedParticipants, seasonSportsData, season]);

  // Calculate eligibility for replace pick dialog — exclude the old pick so its slot is free
  const replacePickEligibility = useMemo(() => {
    if (!replacePickSlot) return null;
    const allPicksExcluding = transformedPicks.filter(
      (p) => p.participant.id !== replacePickSlot.oldParticipantId
    );
    const selectedTeamPicks = allPicksExcluding.filter(
      (p) => p.teamId === replacePickSlot.teamId
    );
    return calculateDraftEligibility(
      replacePickSlot.teamId,
      selectedTeamPicks,
      allPicksExcluding,
      transformedParticipants,
      seasonSportsData,
      season.draftRounds,
      season.teams
    );
  }, [replacePickSlot, transformedPicks, transformedParticipants, seasonSportsData, season]);

  // Listen for new picks from other users
  useEffect(() => {
    type PickMadePayload = { pick: (typeof draftPicks)[number] & { team?: { id?: string; name?: string }; participant?: { name?: string } }; nextPickNumber: number; isDraftComplete?: boolean };
    const handlePickMade = (data: PickMadePayload) => {
      if (isRevalidatingRef.current) {
        // Buffer this pick; the revalidation-complete handler will merge it
        // into the DB snapshot so it isn't silently dropped.
        // Don't update currentPick either — the sync effect will set it from
        // fresh season.currentPickNumber once loading → idle, keeping the
        // pick counter and the picks list in sync with each other.
        pendingPicksDuringRevalidationRef.current.push(data.pick);
      } else {
        setPicks((prev) => [...prev, data.pick]);
        setCurrentPick(data.nextPickNumber);
      }

      // No meaningful "next turn" notification once the draft is over
      if (data.isDraftComplete) return;

      const notifTitle = `${season.league.name} Draft`;
      const nextSlot = getTeamForPick(data.nextPickNumber, draftSlots);
      const isNowMyTurn = !!(
        userTeam && nextSlot && nextSlot.team.id === userTeam.id
      );

      if (isNowMyTurn) {
        sendNotificationRef.current(notifTitle, `It's your turn to pick!`);
      } else if (
        notificationsModeRef.current === "all_picks" &&
        data.pick?.team?.id !== userTeam?.id
      ) {
        // Only notify about other teams' picks, not the user's own
        const pickerName = data.pick?.team?.name || "A team";
        const participantName = data.pick?.participant?.name || "a participant";
        sendNotificationRef.current(
          notifTitle,
          `${pickerName} picked ${participantName}`
        );
      }
    };

    const handleTimerUpdate = (data: { teamId: string; timeRemaining: number; currentPickNumber: number | null }) => {
      // Bail out without creating a new object reference if nothing changed.
      // Without this guard the spread `{ ...prev }` always returns a new object,
      // causing a full re-render of the DraftRoom tree on every 1-second tick.
      setTeamTimers((prev) => {
        if (prev[data.teamId] === data.timeRemaining) return prev;
        return { ...prev, [data.teamId]: data.timeRemaining };
      });
      // Also sync the current pick number from the server. This is critical
      // for mobile reconnection: timer-update events arrive every second, so
      // this ensures currentPick is correct within 1s of reconnecting — even
      // if the HTTP revalidation hasn't completed yet.
      // React's useState already bails out when the new value equals the old
      // one (Object.is comparison on primitives), so no manual guard needed.
      if (data.currentPickNumber !== null) {
        setCurrentPick(data.currentPickNumber);
      }
    };

    const handleDraftPaused = () => {
      setIsPaused(true);
    };

    const handleDraftResumed = () => {
      setIsPaused(false);
    };

    const handleDraftCompleted = () => {
      setIsDraftComplete(true);
    };

    const handleAutodraftUpdated = (data: {
      teamId: string;
      isEnabled: boolean;
      mode: "next_pick" | "while_on";
      queueOnly: boolean;
      source?: "commissioner" | "user";
    }) => {
      setAutodraftStatus((prev) => ({
        ...prev,
        [data.teamId]: { isEnabled: data.isEnabled, mode: data.mode, queueOnly: data.queueOnly },
      }));

      // Update user's local state if it's their team
      if (userTeam && data.teamId === userTeam.id) {
        if (data.source === "commissioner") {
          if (!data.isEnabled) {
            toast.info("Commissioner turned off your autodraft");
          } else {
            toast.info(`Commissioner changed your autodraft to "${getAutodraftLabel(data.isEnabled, data.mode, data.queueOnly)}"`);
          }
        } else {
          // Detect server-side auto-disable: was enabled with queueOnly, now disabled
          const prev = userAutodraftRef.current;
          if (!data.isEnabled && prev.isEnabled && prev.queueOnly && prev.mode === "while_on") {
            toast.info("Autodraft disabled — your queue is empty");
          }
        }

        setUserAutodraft({
          isEnabled: data.isEnabled,
          mode: data.mode,
          queueOnly: data.queueOnly,
        });
      }
    };

    const handleTeamConnected = (data: { teamId: string }) => {
      setConnectedTeams((prev) => new Set(prev).add(data.teamId));
    };

    const handleTeamDisconnected = (data: { teamId: string }) => {
      setConnectedTeams((prev) => {
        const newSet = new Set(prev);
        newSet.delete(data.teamId);
        return newSet;
      });
    };

    const handleConnectedTeamsList = (data: { teamIds: string[] }) => {
      setConnectedTeams(new Set(data.teamIds));
    };

    const handleParticipantRemovedFromQueues = (data: { participantId: string }) => {
      setQueue((prev) =>
        prev.filter((item) => item.participantId !== data.participantId)
      );
    };

    const handleQueueEligibilityPruned = (data: { teamId: string; removedParticipantIds: string[] }) => {
      if (data.teamId !== userTeam?.id) return;
      const removed = new Set(data.removedParticipantIds);
      setQueue((prev) => prev.filter((item) => !removed.has(item.participantId)));
    };

    // Fired when another window/session changes the user's queue (add/remove/reorder/clear).
    // Only received by this team's own sockets (emitted to team-${teamId} private room).
    // Ignored while this window has in-flight mutations to prevent the server echo
    // from clobbering a more-recent optimistic update.
    const handleQueueUpdated = (data: { queue: QueueItem[] }) => {
      if (pendingQueueMutationsRef.current > 0) return;
      setQueue(data.queue);
    };

    const handlePickReplaced = (data: { pickNumber: number; pick: (typeof draftPicks)[number] }) => {
      setPicks((prev) =>
        prev.map((p) => (p.pickNumber === data.pickNumber ? data.pick : p))
      );
    };

    const handleDraftRolledBack = (data: { pickNumber: number }) => {
      setPicks((prev) =>
        prev.filter((p) => p.pickNumber < data.pickNumber)
      );
      setCurrentPick(data.pickNumber);
      setIsDraftComplete(false);
      setIsPaused(false);
    };

    // Full state sync sent by the server on join-draft. Provides an immediate,
    // socket-based path to bring the client up to date — critical for mobile
    // reconnection where the HTTP revalidation may be delayed or fail entirely.
    const handleDraftStateSync = (data: { picks: (typeof draftPicks)[number][]; currentPickNumber: number; isPaused: boolean; status: string; timers?: Array<{ teamId: string; timeRemaining: number }>; queue?: QueueItem[] }) => {
      // If a revalidation is in-flight, let it complete and handle the merge.
      // The revalidation path already buffers picks and deduplicates — layering
      // this on top would risk conflicts. But if NOT revalidating, apply the
      // server snapshot directly.
      if (!isRevalidatingRef.current) {
        setPicks(data.picks);
        setCurrentPick(data.currentPickNumber);
        setIsPaused(data.isPaused);
        setIsDraftComplete(
          data.status === "active" || data.status === "completed"
        );
        if (data.timers) {
          setTeamTimers((prev) => {
            const updated = { ...prev };
            data.timers?.forEach((t) => {
              updated[t.teamId] = t.timeRemaining;
            });
            return updated;
          });
        }
        if (data.queue) {
          setQueue(data.queue);
        }
      }
    };

    on("pick-made", handlePickMade);
    on("timer-update", handleTimerUpdate);
    on("draft-paused", handleDraftPaused);
    on("draft-resumed", handleDraftResumed);
    on("draft-completed", handleDraftCompleted);
    on("autodraft-updated", handleAutodraftUpdated);
    on("team-connected", handleTeamConnected);
    on("team-disconnected", handleTeamDisconnected);
    on("connected-teams-list", handleConnectedTeamsList);
    on("participant-removed-from-queues", handleParticipantRemovedFromQueues);
    on("queue-eligibility-pruned", handleQueueEligibilityPruned);
    on("queue-updated", handleQueueUpdated);
    on("pick-replaced", handlePickReplaced);
    on("draft-rolled-back", handleDraftRolledBack);
    on("draft-state-sync", handleDraftStateSync);

    return () => {
      off("pick-made", handlePickMade);
      off("timer-update", handleTimerUpdate);
      off("draft-paused", handleDraftPaused);
      off("draft-resumed", handleDraftResumed);
      off("draft-completed", handleDraftCompleted);
      off("autodraft-updated", handleAutodraftUpdated);
      off("team-connected", handleTeamConnected);
      off("team-disconnected", handleTeamDisconnected);
      off("connected-teams-list", handleConnectedTeamsList);
      off("participant-removed-from-queues", handleParticipantRemovedFromQueues);
      off("queue-eligibility-pruned", handleQueueEligibilityPruned);
      off("queue-updated", handleQueueUpdated);
      off("pick-replaced", handlePickReplaced);
      off("draft-rolled-back", handleDraftRolledBack);
      off("draft-state-sync", handleDraftStateSync);
    };
  // `on`/`off` are stable useCallback refs. `socketVersion` increments whenever
  // useDraftSocket creates a new socket instance, ensuring this effect re-runs
  // and re-registers all handlers on the fresh socket.
  // `draftSlots`, `userTeam`, and `season.league.name` are intentionally omitted:
  // they come from useLoaderData and are immutable for the lifetime of the page load.
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [on, off, socketVersion]);

  // Persist sidebar collapsed state
  useEffect(() => {
    if (typeof window !== "undefined") {
      localStorage.setItem("draftSidebarCollapsed", JSON.stringify(sidebarCollapsed));
    }
  }, [sidebarCollapsed]);

  // Queue handlers
  const handleAddToQueue = useCallback(async (participantId: string) => {
    if (!userTeam) return;

    // Check if participant is already in queue (client-side)
    const alreadyInQueue = queue.some((item) => item.participantId === participantId);
    if (alreadyInQueue) {
      toast.error("This participant is already in your queue");
      return;
    }

    // Optimistic update - create a temporary queue item
    const now = new Date();
    const tempQueueItem: QueueItem = {
      id: `temp-${Date.now()}`,
      participantId,
      queuePosition: queue.length + 1,
      seasonId: season.id,
      teamId: userTeam.id,
      createdAt: now,
      updatedAt: now,
    };

    // Immediately update the UI
    setQueue((prev) => [...prev, tempQueueItem]);

    const formData = new FormData();
    formData.append("seasonId", season.id);
    formData.append("teamId", userTeam.id);
    formData.append("participantId", participantId);

    pendingQueueMutationsRef.current++;
    try {
      const response = await authFetch("/api/queue/add", {
        method: "POST",
        body: formData,
      });
      if (!response) { setQueue((prev) => prev.filter((item) => item.id !== tempQueueItem.id)); return; }

      if (response.ok) {
        const data = await response.json();
        // Replace temp item with real item from server
        setQueue((prev) =>
          prev.map((item) =>
            item.id === tempQueueItem.id ? data.queueItem : item
          )
        );
      } else {
        // Revert the optimistic update on error
        setQueue((prev) => prev.filter((item) => item.id !== tempQueueItem.id));
        const error = await response.json();
        toast.error(error.error || "Failed to add to queue");
      }
    } catch {
      // Revert the optimistic update on network error
      setQueue((prev) => prev.filter((item) => item.id !== tempQueueItem.id));
      toast.error("Network error - failed to add to queue");
    } finally {
      pendingQueueMutationsRef.current--;
    }
  // queue is read directly (not via functional updater) for the duplicate check and
  // optimistic position, so it must be a dep. This is fine: AvailableParticipantsSection
  // already re-renders when queue changes (it receives queue as a prop).
  }, [userTeam, queue, season.id, authFetch]);

  const handleRemoveFromQueue = useCallback(async (queueId: string) => {
    if (!userTeam) return;

    // Optimistically remove the item immediately
    let previousQueue: QueueItem[] = [];
    setQueue((prev) => {
      previousQueue = prev;
      return prev.filter((item) => item.id !== queueId);
    });

    const formData = new FormData();
    formData.append("queueId", queueId);
    formData.append("teamId", userTeam.id);

    pendingQueueMutationsRef.current++;
    try {
      const response = await authFetch("/api/queue/remove", {
        method: "POST",
        body: formData,
      });
      if (!response) { setQueue(previousQueue); return; }

      if (response.ok) {
        const data = await response.json();
        setQueue(data.queue);
      } else {
        setQueue(previousQueue);
        const error = await response.json();
        toast.error(error.error || "Failed to remove from queue");
      }
    } catch {
      setQueue(previousQueue);
      toast.error("Network error - failed to remove from queue");
    } finally {
      pendingQueueMutationsRef.current--;
    }
  }, [userTeam, authFetch]);

  const handleReorderQueue = useCallback(async (participantIds: string[]) => {
    if (!userTeam) return;

    // Optimistically reorder the queue immediately
    let previousQueue: QueueItem[] = [];
    setQueue((prev) => {
      previousQueue = prev;
      return participantIds
        .map((pid) => prev.find((item) => item.participantId === pid))
        .filter((item): item is QueueItem => item !== undefined);
    });

    const formData = new FormData();
    formData.append("teamId", userTeam.id);
    formData.append("seasonId", season.id);
    formData.append("participantIds", JSON.stringify(participantIds));

    pendingQueueMutationsRef.current++;
    try {
      const response = await authFetch("/api/queue/reorder", {
        method: "POST",
        body: formData,
      });
      if (!response) { setQueue(previousQueue); return; }

      if (response.ok) {
        const data = await response.json();
        setQueue(data.queue);
      } else {
        const error = await response.json();
        setQueue(previousQueue);
        toast.error(error.error || "Failed to reorder queue");
      }
    } catch {
      setQueue(previousQueue);
      toast.error("Network error - failed to reorder queue");
    } finally {
      pendingQueueMutationsRef.current--;
    }
  }, [userTeam, season.id, authFetch]);

  const handleStartDraft = async () => {
    try {
      const formData = new FormData();
      formData.append("seasonId", season.id);

      const response = await authFetch("/api/draft/start", {
        method: "POST",
        body: formData,
      });
      if (!response) return;

      if (response.ok) {
        revalidate();
      } else {
        const error = await response.json();
        toast.error(error.error || "Failed to start draft");
      }
    } catch {
      toast.error("Network error - failed to start draft");
    }
  };

  const handlePauseDraft = async () => {
    const formData = new FormData();
    formData.append("seasonId", season.id);

    try {
      const response = await authFetch("/api/draft/pause", {
        method: "POST",
        body: formData,
      });
      if (!response) return;

      if (response.ok) {
        setIsPaused(true);
      } else {
        const error = await response.json();
        toast.error(error.error || "Failed to pause draft");
      }
    } catch {
      toast.error("Network error - failed to pause draft");
    }
  };

  const handleResumeDraft = async () => {
    const formData = new FormData();
    formData.append("seasonId", season.id);

    try {
      const response = await authFetch("/api/draft/resume", {
        method: "POST",
        body: formData,
      });
      if (!response) return;

      if (response.ok) {
        setIsPaused(false);
      } else {
        const error = await response.json();
        toast.error(error.error || "Failed to resume draft");
      }
    } catch {
      toast.error("Network error - failed to resume draft");
    }
  };

  const handleMakePick = useCallback(async (participantId: string) => {
    try {
      const formData = new FormData();
      formData.append("seasonId", season.id);
      formData.append("participantId", participantId);

      const response = await authFetch("/api/draft/make-pick", {
        method: "POST",
        body: formData,
      });
      if (!response) return;

      if (!response.ok) {
        const error = await response.json();
        toast.error(error.error || "Failed to make pick");
      }
    } catch {
      toast.error("Network error - failed to make pick");
    }
  }, [season.id, authFetch]);

  const handleForceAutopick = useCallback(async (pickNumber: number, teamId: string) => {
    try {
      const formData = new FormData();
      formData.append("seasonId", season.id);
      formData.append("teamId", teamId);
      formData.append("pickNumber", pickNumber.toString());

      const response = await authFetch("/api/draft/force-autopick", {
        method: "POST",
        body: formData,
      });
      if (!response) return;

      if (!response.ok) {
        const error = await response.json();
        toast.error(error.error || "Failed to force autopick");
      }
    } catch {
      toast.error("Network error - failed to force autopick");
    }
  }, [season.id, authFetch]);

  const handleForceManualPick = async (participantId: string) => {
    if (!selectedPickSlot) return;

    try {
      const formData = new FormData();
      formData.append("seasonId", season.id);
      formData.append("teamId", selectedPickSlot.teamId);
      formData.append("participantId", participantId);
      formData.append("pickNumber", selectedPickSlot.pickNumber.toString());

      const response = await authFetch("/api/draft/force-manual-pick", {
        method: "POST",
        body: formData,
      });
      if (!response) return;

      if (response.ok) {
        setForcePickDialogOpen(false);
        setSelectedPickSlot(null);
      } else {
        const error = await response.json();
        toast.error(error.error || "Failed to force manual pick");
      }
    } catch {
      toast.error("Network error - failed to force manual pick");
    }
  };

  const handleReplacePickOpen = useCallback((pickNumber: number, teamId: string) => {
    const pick = picks.find((p) => p.pickNumber === pickNumber);
    if (!pick) {
      toast.error("Pick not found — try refreshing the page");
      return;
    }
    setReplacePickSlot({ pickNumber, teamId, oldParticipantId: pick.participant.id });
    setReplacePickDialogOpen(true);
  // picks is read directly to find the pick being replaced.
  }, [picks]);

  const handleReplacePick = async (participantId: string) => {
    if (!replacePickSlot) return;

    try {
      const formData = new FormData();
      formData.append("seasonId", season.id);
      formData.append("teamId", replacePickSlot.teamId);
      formData.append("participantId", participantId);
      formData.append("pickNumber", replacePickSlot.pickNumber.toString());

      const response = await authFetch("/api/draft/replace-pick", {
        method: "POST",
        body: formData,
      });
      if (!response) return;

      if (response.ok) {
        setReplacePickDialogOpen(false);
        setReplacePickSlot(null);
      } else {
        const error = await response.json();
        toast.error(error.error || "Failed to replace pick");
      }
    } catch {
      toast.error("Network error - failed to replace pick");
    }
  };

  const handleRollbackToPick = useCallback((pickNumber: number) => {
    setRollbackPickNumber(pickNumber);
    setRollbackConfirmOpen(true);
  }, []);

  const handleConfirmRollback = async () => {
    if (!rollbackPickNumber || isRollingBack) return;

    setIsRollingBack(true);
    try {
      const formData = new FormData();
      formData.append("seasonId", season.id);
      formData.append("pickNumber", rollbackPickNumber.toString());

      const response = await authFetch("/api/draft/rollback", {
        method: "POST",
        body: formData,
      });

      setIsRollingBack(false);
      if (!response) return;

      if (response.ok) {
        setRollbackConfirmOpen(false);
        setRollbackPickNumber(null);
      } else {
        const error = await response.json();
        toast.error(error.error || "Failed to roll back draft");
      }
    } catch {
      setIsRollingBack(false);
      toast.error("Network error - failed to roll back draft");
    }
  };

  const handleAdjustTimeBankOpen = useCallback((teamId: string) => {
    setTimeBankTeamId(teamId);
    setTimeBankAmount("1");
    setTimeBankUnit("minutes");
    setTimeBankDirection("add");
    setTimeBankDialogOpen(true);
  }, []);

  const handleSetAutodraftOpen = useCallback((teamId: string) => {
    setCommissionerAutodraftTeamId(teamId);
    setCommissionerAutodraftDialogOpen(true);
  }, []);

  const handleCommissionerAutodraftUpdate = useCallback(() => {
    setCommissionerAutodraftDialogOpen(false);
    setCommissionerAutodraftTeamId(null);
  }, []);

  const handleConfirmAdjustTimeBank = async () => {
    if (!timeBankTeamId || isAdjustingTimeBank) return;

    const amount = parseFloat(timeBankAmount);
    if (isNaN(amount) || amount <= 0) {
      toast.error("Please enter a valid positive amount");
      return;
    }

    const unitMultipliers = { seconds: 1, minutes: 60, hours: 3600 };
    const totalSeconds = Math.round(amount * unitMultipliers[timeBankUnit]);

    if (totalSeconds === 0) {
      toast.error("Adjustment rounds to 0 seconds — please enter a larger value");
      return;
    }

    const adjustment = timeBankDirection === "add" ? totalSeconds : -totalSeconds;

    setIsAdjustingTimeBank(true);
    try {
      const formData = new FormData();
      formData.append("seasonId", season.id);
      formData.append("teamId", timeBankTeamId);
      formData.append("adjustment", adjustment.toString());

      const response = await authFetch("/api/draft/adjust-time-bank", {
        method: "POST",
        body: formData,
      });
      if (!response) return;

      if (response.ok) {
        const data = await response.json();
        setTeamTimers((prev) => ({ ...prev, [timeBankTeamId]: data.timeRemaining }));
        const teamName = draftSlots.find((s) => s.team.id === timeBankTeamId)?.team.name;
        toast.success(`Time bank adjusted for ${teamName}`);
        setTimeBankDialogOpen(false);
        setTimeBankTeamId(null);
      } else {
        const error = await response.json();
        toast.error(error.error || "Failed to adjust time bank");
      }
    } catch {
      toast.error("Network error — failed to adjust time bank");
    } finally {
      setIsAdjustingTimeBank(false);
    }
  };

  const handleForceManualPickOpen = useCallback((pickNumber: number, teamId: string) => {
    setSelectedPickSlot({ pickNumber, teamId });
    setForcePickDialogOpen(true);
  }, []);

  // Calculate current round
  const currentRound = draftSlots.length > 0 ? Math.ceil(currentPick / draftSlots.length) : 1;

  // Determine whose turn it is
  const currentDraftSlot = useMemo(
    () => getTeamForPick(currentPick, draftSlots),
    [currentPick, draftSlots]
  );
  const isMyTurn = !!(
    userTeam && currentDraftSlot && currentDraftSlot.team.id === userTeam.id
  );
  const canPick = isMyTurn && season.status === "draft" && !isPaused; // Only team owner on their turn when draft is active

  const currentDraftSlotOwnerName = currentDraftSlot ? ownerMap[currentDraftSlot.team.id] : undefined;
  const currentClockTime = currentDraftSlot ? teamTimers[currentDraftSlot.team.id] : undefined;
  const currentClockColor = getTimerColorClass(currentClockTime);

  // Generate snake draft grid structure
  const draftGrid = useMemo(() => {
    const teamCount = draftSlots.length;
    const rounds = season.draftRounds;
    const grid: Array<
      Array<{
        pickNumber: number;
        round: number;
        pickInRound: number;
        teamId: string;
        pick?: (typeof draftPicks)[number];
      }>
    > = [];

    for (let round = 1; round <= rounds; round++) {
      const roundPicks = [];
      const isOddRound = round % 2 === 1;

      for (let i = 0; i < teamCount; i++) {
        const pickInRound = i + 1;
        const teamIndex = isOddRound ? i : teamCount - 1 - i;
        const pickNumber = (round - 1) * teamCount + pickInRound;
        const teamId = draftSlots[teamIndex]?.team.id;

        const pick = picks.find((p) => p.pickNumber === pickNumber);

        roundPicks.push({ pickNumber, round, pickInRound, teamId, pick });
      }
      grid.push(roundPicks);
    }

    return grid;
  }, [picks, draftSlots, season.draftRounds]);

  // Get drafted participant IDs for filtering
  const draftedParticipantIds = useMemo(
    () => new Set(picks.map((p) => p.participant.id)),
    [picks]
  );

  // Sport IDs where the current user's team has already made at least one pick
  const userDraftedSportIds = useMemo(() => {
    if (!userTeam) return new Set<string>();
    return new Set(
      picks
        .filter((p) => p.team.id === userTeam.id)
        .map((p) => p.sport.id)
    );
  }, [picks, userTeam]);

  // Sport names where the current user's team has already made at least one pick
  const userDraftedSportNames = useMemo(() => {
    if (!userTeam) return new Set<string>();
    return new Set(
      picks
        .filter((p) => p.team.id === userTeam.id)
        .map((p) => p.sport.name)
    );
  }, [picks, userTeam]);

  // Get unique sports for filter dropdown
  const uniqueSports = useMemo(
    () =>
      Array.from(
        new Set(availableParticipants.map((p) => p.sport.name))
      ).sort(),
    [availableParticipants]
  );

  // Filter participants based on search, sport, drafted status, and eligibility
  const filteredParticipants = useMemo(() => {
    const sportFilterSet = new Set(sportFilters);
    return availableParticipants.filter((participant) => {
      // Drafted filter - hide drafted participants by default
      if (hideDrafted && draftedParticipantIds.has(participant.id)) {
        return false;
      }
      // Eligibility filter - hide ineligible participants by default (if user has a team)
      if (hideIneligible && eligibility) {
        const isEligible = eligibility.eligibleSportIds.has(participant.sport.id);
        if (!isEligible) {
          return false;
        }
      }
      // Hide completed sports filter - hide participants from sports already drafted by user's team
      if (hideCompletedSports && userDraftedSportIds.has(participant.sport.id)) {
        return false;
      }
      // Search filter
      if (
        searchQuery &&
        !participant.name.toLowerCase().includes(searchQuery.toLowerCase())
      ) {
        return false;
      }
      // Sport filter
      if (sportFilterSet.size > 0 && !sportFilterSet.has(participant.sport.name)) {
        return false;
      }
      return true;
    });
  }, [availableParticipants, hideDrafted, hideIneligible, hideCompletedSports, eligibility, draftedParticipantIds, userDraftedSportIds, searchQuery, sportFilters]);

  // Shared component props — defined once to avoid duplication between desktop and mobile layouts
  const handleHideCompletedSportsChange = useCallback((hide: boolean) => {
    setHideCompletedSports(hide);
    if (hide) {
      setSportFilters((prev) => prev.filter((s) => !userDraftedSportNames.has(s)));
    }
  }, [userDraftedSportNames]);

  const availableParticipantsSectionProps = {
    participants: filteredParticipants,
    searchQuery,
    sportFilters,
    hideDrafted,
    hideIneligible,
    uniqueSports,
    draftedParticipantIds,
    queue,
    eligibility,
    canPick,
    hasTeam: !!userTeam,
    onSearchChange: setSearchQuery,
    onSportFiltersChange: setSportFilters,
    onHideDraftedChange: setHideDrafted,
    onHideIneligibleChange: setHideIneligible,
    hideCompletedSports,
    userDraftedSportNames,
    onHideCompletedSportsChange: handleHideCompletedSportsChange,
    onMakePick: handleMakePick,
    onAddToQueue: handleAddToQueue,
    onRemoveFromQueue: handleRemoveFromQueue,
  };

  const draftGridSectionProps = {
    draftSlots,
    draftGrid,
    currentPick,
    teamTimers,
    autodraftStatus,
    connectedTeams,
    isCommissioner,
    ownerMap,
    onAdjustTimeBankOpen: isCommissioner ? handleAdjustTimeBankOpen : undefined,
    onSetAutodraftOpen: isCommissioner ? handleSetAutodraftOpen : undefined,
    onForceAutopick: handleForceAutopick,
    onForceManualPickOpen: handleForceManualPickOpen,
    onReplacePick: isCommissioner ? handleReplacePickOpen : undefined,
    onRollbackToPick: isCommissioner && !isDraftComplete ? handleRollbackToPick : undefined,
  };

  const summaryViewProps = {
    draftSlots,
    picks,
    sports: seasonSportsData,
    ownerMap,
    totalRounds: season.draftRounds,
  };

  const rosterViewProps = {
    draftSlots,
    picks,
    sports: seasonSportsData,
    ownerMap,
  };

  const handleAutodraftUpdate = useCallback(
    (isEnabled: boolean, mode: "next_pick" | "while_on", queueOnly: boolean) => {
      setUserAutodraft({ isEnabled, mode, queueOnly });
    },
    []
  );

  const queueSectionProps = userTeam
    ? {
        queue,
        availableParticipants,
        seasonId: season.id,
        teamId: userTeam.id,
        isMyTurn,
        canPick,
        userAutodraft,
        onRemoveFromQueue: handleRemoveFromQueue,
        onAutodraftUpdate: handleAutodraftUpdate,
        onReorder: handleReorderQueue,
        onMakePick: handleMakePick,
      }
    : null;

  const mobileTabs = userTeam
    ? [MOBILE_TABS_BASE[0], QUEUE_TAB, ...MOBILE_TABS_BASE.slice(1)]
    : MOBILE_TABS_BASE;
  const mobileColCount = mobileTabs.length;

  return (
    <div className="h-dvh bg-background flex flex-col overflow-hidden">
      {/* Draft Completion Banner */}
      {isDraftComplete && (
        <div className="bg-emerald-500/20 border-b border-emerald-500/30 text-emerald-400 px-4 py-3 text-center font-semibold flex-shrink-0">
          🎉 Draft Complete! The season is now active.
          {season.league.isPublicDraftBoard && (
            <>
              {" "}
              <a
                href={`/leagues/${season.leagueId}/draft-board/${season.id}`}
                className="underline hover:text-emerald-300"
              >
                View Draft Board
              </a>
            </>
          )}
        </div>
      )}

      {/* Standalone Header - No Main Nav */}
      <div className="border-b bg-card flex-shrink-0">
        <div className="w-full px-4 py-4">
          <div className="flex items-center justify-between">
            <div className="flex items-center gap-3">
              {/* Mobile menu button - only show if user has team and sidebar is collapsed */}
              {userTeam && sidebarCollapsed && (
                <Button
                  variant="ghost"
                  size="icon"
                  onClick={() => setSidebarCollapsed(false)}
                  className="lg:hidden"
                  aria-label="Open sidebar"
                >
                  <svg
                    xmlns="http://www.w3.org/2000/svg"
                    width="24"
                    height="24"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth="2"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  >
                    <line x1="3" y1="12" x2="21" y2="12" />
                    <line x1="3" y1="6" x2="21" y2="6" />
                    <line x1="3" y1="18" x2="21" y2="18" />
                  </svg>
                </Button>
              )}
              <div>
                <h1 className="text-lg md:text-3xl font-bold">
                  {season.league.name} - {season.year} Draft
                </h1>
                <div className="flex gap-4 text-sm text-muted-foreground mt-1">
                  <span>Round: {currentRound}</span>
                  <span>Pick: {currentPick}</span>
                  <span className="capitalize">
                    {season.status.replace("_", " ")}
                  </span>
                </div>
              </div>
            </div>
            <div className="flex items-center gap-3">
              {/* Commissioner Controls - hidden on mobile (shown in Controls tab) */}
              {isCommissioner && season.status === "pre_draft" && (
                <Button className="hidden md:flex" onClick={handleStartDraft}>Start Draft</Button>
              )}

              {isCommissioner && season.status === "draft" && !isDraftComplete && (
                <div className="hidden md:flex">
                  {isPaused ? (
                    <Button onClick={handleResumeDraft} variant="default">
                      Resume Draft
                    </Button>
                  ) : (
                    <Button onClick={handlePauseDraft} variant="outline">
                      Pause Draft
                    </Button>
                  )}
                </div>
              )}

              <div className="flex items-center gap-2">
                <div
                  className={`w-3 h-3 rounded-full ${
                    isConnected ? "bg-emerald-500" : "bg-coral-accent"
                  }`}
                />
                <span className="hidden sm:inline text-sm font-medium">
                  {isConnected ? "Connected" : "Disconnected"}
                </span>
              </div>
              <Button variant="outline" asChild className="hidden md:flex">
                <Link to={`/leagues/${season.leagueId}`}>Exit Draft Room</Link>
              </Button>
            </div>
          </div>
        </div>
      </div>

      {/* Mobile On Clock bar */}
      {season.status === "draft" && !isDraftComplete && currentDraftSlot && (
        <div className={`md:hidden flex-shrink-0 flex items-center justify-between px-4 py-2 border-b ${
          isMyTurn
            ? "bg-electric/25 border-electric"
            : "bg-muted/30"
        }`}>
          <div>
            <div className={`text-xs font-bold uppercase tracking-wider ${isMyTurn ? "text-electric" : "text-muted-foreground"}`}>
              {isMyTurn ? "Your Turn!" : "On the Clock"}
            </div>
            <div className="text-sm font-bold">
              {currentDraftSlotOwnerName ?? currentDraftSlot.team.name}
            </div>
            {currentDraftSlotOwnerName && (
              <div className="text-xs text-muted-foreground">{currentDraftSlot.team.name}</div>
            )}
          </div>
          {isPaused ? (
            <span className="text-sm font-bold text-amber-accent">Paused</span>
          ) : (
            <span className={`text-xl font-mono font-bold tabular-nums ${currentClockColor}`}>
              {formatClockTime(currentClockTime)}
            </span>
          )}
        </div>
      )}

      {/* Main Content — single layout tree based on isMobile to avoid duplicate component instances */}
      <div className="flex-1 overflow-hidden">
        {isMobile ? (
          <div className="flex h-full overflow-hidden flex-col">
            {mobileTab === "available" && (
              <AvailableParticipantsSection {...availableParticipantsSectionProps} />
            )}
            {mobileTab === "queue" && (
              <div className="h-full overflow-y-auto">
                {queueSectionProps ? (
                  <QueueSection {...queueSectionProps} />
                ) : (
                  <p className="text-muted-foreground text-sm text-center py-8 px-4">
                    Join a team to manage your queue
                  </p>
                )}
              </div>
            )}
            {mobileTab === "board" && (
              <DraftGridSection {...draftGridSectionProps} />
            )}
            {mobileTab === "teams" && (
              <div className="flex flex-col h-full overflow-hidden">
                <div className="flex-shrink-0 flex gap-1 p-2 border-b">
                  <button
                    onClick={() => setTeamsSubTab("rosters")}
                    className={`flex-1 py-1.5 text-sm font-medium rounded transition-colors ${
                      teamsSubTab === "rosters"
                        ? "bg-accent text-accent-foreground"
                        : "text-muted-foreground hover:text-foreground"
                    }`}
                  >
                    Roster
                  </button>
                  <button
                    onClick={() => setTeamsSubTab("summary")}
                    className={`flex-1 py-1.5 text-sm font-medium rounded transition-colors ${
                      teamsSubTab === "summary"
                        ? "bg-accent text-accent-foreground"
                        : "text-muted-foreground hover:text-foreground"
                    }`}
                  >
                    Summary
                  </button>
                </div>
                <div className="flex-1 overflow-hidden">
                  {teamsSubTab === "rosters" ? (
                    <TeamRosterView {...rosterViewProps} />
                  ) : (
                    <DraftSummaryView {...summaryViewProps} />
                  )}
                </div>
              </div>
            )}
            {mobileTab === "controls" && (
              <div className="h-full overflow-y-auto p-4 space-y-6">
                {isCommissioner && season.status === "pre_draft" && (
                  <Button className="w-full min-h-[48px]" onClick={handleStartDraft}>
                    Start Draft
                  </Button>
                )}
                {isCommissioner && season.status === "draft" && !isDraftComplete && (
                  isPaused ? (
                    <Button className="w-full min-h-[48px]" onClick={handleResumeDraft}>
                      Resume Draft
                    </Button>
                  ) : (
                    <Button className="w-full min-h-[48px]" variant="outline" onClick={handlePauseDraft}>
                      Pause Draft
                    </Button>
                  )
                )}

                <section>
                  <h2 className="font-semibold text-sm mb-2">Recent Picks</h2>
                  <SidebarRecentPicks picks={picks} />
                </section>

                <NotificationSettings
                  enabled={notificationsEnabled}
                  onEnabledChange={setNotificationsEnabled}
                  mode={notificationsMode}
                  onModeChange={setNotificationsMode}
                  permissionState={notificationsPermission}
                />

                <Button variant="outline" asChild className="w-full min-h-[48px]">
                  <Link to={`/leagues/${season.leagueId}`}>Exit Draft Room</Link>
                </Button>
              </div>
            )}
          </div>
        ) : (
          <div className="flex h-full overflow-hidden">
            {userTeam && queueSectionProps && (
              <DraftSidebar
                collapsed={sidebarCollapsed}
                onCollapsedChange={setSidebarCollapsed}
                queueSection={<QueueSection {...queueSectionProps} />}
                recentPicksSection={<SidebarRecentPicks picks={picks} />}
                settingsSection={
                  <NotificationSettings
                    enabled={notificationsEnabled}
                    onEnabledChange={setNotificationsEnabled}
                    mode={notificationsMode}
                    onModeChange={setNotificationsMode}
                    permissionState={notificationsPermission}
                  />
                }
              />
            )}

            <div className="flex-1 overflow-hidden">
              <Tabs
                value={activeTab}
                onValueChange={(value) =>
                  setActiveTab(value as "participants" | "board" | "rosters" | "summary")
                }
                className="h-full flex flex-col"
              >
                <div className={`mt-4 transition-all duration-300 ${
                  isMyTurn && season.status === "draft" && !isDraftComplete
                    ? "bg-electric/25 border-y-2 border-electric shadow-[0_0_24px_0_rgb(0_200_255_/_0.18)]"
                    : ""
                }`}>
                  <div className="flex items-center gap-3 px-4 py-2">
                    <TabsList>
                      <TabsTrigger value="participants">Available Participants</TabsTrigger>
                      <TabsTrigger value="board">Draft Board</TabsTrigger>
                      <TabsTrigger value="rosters">Rosters</TabsTrigger>
                      <TabsTrigger value="summary">Summary</TabsTrigger>
                    </TabsList>

                    {season.status === "draft" && !isDraftComplete && currentDraftSlot && (
                      <div className={`ml-auto flex items-center gap-2.5 rounded-lg px-3 py-1.5 flex-shrink-0 transition-all ${
                        isMyTurn
                          ? "bg-electric/30 border-2 border-electric shadow-md shadow-electric/30"
                          : "border border-border/50"
                      }`}>
                        <div className="leading-tight">
                          <div className={`text-xs font-bold uppercase tracking-wider ${isMyTurn ? "text-electric" : "text-muted-foreground"}`}>
                            {isMyTurn ? "Your Turn!" : "On the Clock"}
                          </div>
                          <div className={`text-sm font-bold ${isMyTurn ? "text-foreground" : "text-muted-foreground"}`}>
                            {currentDraftSlotOwnerName ?? currentDraftSlot.team.name}
                          </div>
                          {currentDraftSlotOwnerName && (
                            <div className="text-xs text-muted-foreground">{currentDraftSlot.team.name}</div>
                          )}
                        </div>
                        {isPaused ? (
                          <span className="text-sm font-bold text-amber-accent">Paused</span>
                        ) : (
                          <span className={`text-2xl font-mono font-bold tabular-nums ${currentClockColor}`}>
                            {formatClockTime(currentClockTime)}
                          </span>
                        )}
                      </div>
                    )}
                  </div>
                </div>

                <TabsContent value="participants" className="flex-1 overflow-hidden m-0">
                  <AvailableParticipantsSection {...availableParticipantsSectionProps} />
                </TabsContent>

                <TabsContent value="board" className="flex-1 overflow-hidden m-0">
                  <DraftGridSection {...draftGridSectionProps} />
                </TabsContent>

                <TabsContent value="rosters" className="flex-1 overflow-hidden m-0">
                  <TeamRosterView {...rosterViewProps} />
                </TabsContent>

                <TabsContent value="summary" className="flex-1 overflow-hidden m-0">
                  <DraftSummaryView {...summaryViewProps} />
                </TabsContent>
              </Tabs>
            </div>
          </div>
        )}
      </div>

      {/* Mobile Bottom Nav */}
      <nav className={`md:hidden flex-shrink-0 border-t bg-card grid`} style={{ gridTemplateColumns: `repeat(${mobileColCount}, 1fr)` }}>
        {mobileTabs.map((tab) => {
          const showTurnIndicator =
            (tab.id === "available" || tab.id === "queue") &&
            isMyTurn &&
            season.status === "draft" &&
            !isDraftComplete;
          return (
            <button
              key={tab.id}
              onClick={() => setMobileTab(tab.id as typeof mobileTab)}
              className={`relative flex flex-col items-center justify-center py-2 gap-0.5 min-h-[56px] transition-colors ${
                mobileTab === tab.id ? "text-electric" : "text-muted-foreground"
              }`}
            >
              <div className="relative">
                <tab.Icon className="h-5 w-5" />
                {showTurnIndicator && (
                  <span className="absolute -top-1 -right-1 w-2 h-2 rounded-full bg-electric" />
                )}
              </div>
              <span className="text-[10px] font-medium">{tab.label}</span>
            </button>
          );
        })}
      </nav>

      <ParticipantSelectionDialog
        open={forcePickDialogOpen && !!selectedPickSlot}
        onOpenChange={(open) => {
          setForcePickDialogOpen(open);
          if (!open) setSelectedPickSlot(null);
        }}
        title="Force Manual Pick"
        description={`Select a participant to draft for Pick #${selectedPickSlot?.pickNumber}`}
        participants={availableParticipants}
        draftedParticipantIds={draftedParticipantIds}
        eligibility={forcePickEligibility}
        uniqueSports={uniqueSports}
        onSelect={handleForceManualPick}
      />

      <ParticipantSelectionDialog
        open={replacePickDialogOpen && !!replacePickSlot}
        onOpenChange={(open) => {
          setReplacePickDialogOpen(open);
          if (!open) setReplacePickSlot(null);
        }}
        title="Replace Pick"
        description={`Select a participant to replace the current pick at slot #${replacePickSlot?.pickNumber}`}
        participants={availableParticipants}
        draftedParticipantIds={draftedParticipantIds}
        allowParticipantId={replacePickSlot?.oldParticipantId}
        currentParticipantId={replacePickSlot?.oldParticipantId}
        eligibility={replacePickEligibility}
        uniqueSports={uniqueSports}
        onSelect={handleReplacePick}
      />

      {/* Rollback Confirmation Dialog */}
      <Dialog
        open={rollbackConfirmOpen}
        onOpenChange={(open) => {
          setRollbackConfirmOpen(open);
          if (!open) setRollbackPickNumber(null);
        }}
      >
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Roll Back Draft</DialogTitle>
            <DialogDescription>
              {rollbackPickNumber !== null && (() => {
                const count = picks.filter((p) => p.pickNumber >= rollbackPickNumber).length;
                return `This will erase ${count} pick${count === 1 ? "" : "s"} (pick #${rollbackPickNumber} through the most recent) and return the draft to pick #${rollbackPickNumber}. This cannot be undone.`;
              })()}
            </DialogDescription>
          </DialogHeader>
          <DialogFooter>
            <Button variant="outline" onClick={() => setRollbackConfirmOpen(false)}>
              Cancel
            </Button>
            <Button variant="destructive" onClick={handleConfirmRollback} disabled={isRollingBack}>
              {isRollingBack ? "Rolling Back..." : "Roll Back"}
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>

      {/* Time Bank Adjustment Dialog */}
      <Dialog
        open={timeBankDialogOpen}
        onOpenChange={(open) => {
          setTimeBankDialogOpen(open);
          if (!open) setTimeBankTeamId(null);
        }}
      >
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Adjust Time Bank</DialogTitle>
            <DialogDescription>
              {timeBankTeamId
                ? `Adjusting time for ${draftSlots.find((s) => s.team.id === timeBankTeamId)?.team.name}`
                : "Adjust a team's time bank"}
            </DialogDescription>
          </DialogHeader>

          <div className="flex flex-col gap-4 py-2">
            <div className="flex gap-2">
              <button
                type="button"
                onClick={() => setTimeBankDirection("add")}
                className={`flex-1 py-2 rounded-md border text-sm font-medium transition-colors ${
                  timeBankDirection === "add"
                    ? "bg-emerald-500/20 border-emerald-500 text-emerald-400"
                    : "border-border text-muted-foreground hover:bg-muted"
                }`}
              >
                Add Time
              </button>
              <button
                type="button"
                onClick={() => setTimeBankDirection("remove")}
                className={`flex-1 py-2 rounded-md border text-sm font-medium transition-colors ${
                  timeBankDirection === "remove"
                    ? "bg-destructive/20 border-destructive text-destructive"
                    : "border-border text-muted-foreground hover:bg-muted"
                }`}
              >
                Remove Time
              </button>
            </div>

            <div className="flex gap-2">
              <input
                type="number"
                min="0.001"
                step="any"
                value={timeBankAmount}
                onChange={(e) => setTimeBankAmount(e.target.value)}
                className="flex-1 px-3 py-2 border rounded-md bg-background text-foreground text-base md:text-sm"
                placeholder="Amount"
              />
              <select
                value={timeBankUnit}
                onChange={(e) =>
                  setTimeBankUnit(e.target.value as "seconds" | "minutes" | "hours")
                }
                className="px-3 py-2 border rounded-md bg-background text-foreground text-base md:text-sm"
              >
                <option value="seconds">Seconds</option>
                <option value="minutes">Minutes</option>
                <option value="hours">Hours</option>
              </select>
            </div>
          </div>

          <DialogFooter>
            <Button variant="outline" onClick={() => setTimeBankDialogOpen(false)}>
              Cancel
            </Button>
            <Button
              onClick={handleConfirmAdjustTimeBank}
              disabled={isAdjustingTimeBank}
              variant={timeBankDirection === "remove" ? "destructive" : "default"}
            >
              {isAdjustingTimeBank
                ? "Adjusting..."
                : timeBankDirection === "add"
                  ? "Add Time"
                  : "Remove Time"}
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>

      {/* Commissioner Autodraft Override Dialog */}
      <Dialog
        open={commissionerAutodraftDialogOpen}
        onOpenChange={(open) => {
          setCommissionerAutodraftDialogOpen(open);
          if (!open) setCommissionerAutodraftTeamId(null);
        }}
      >
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Set Autodraft</DialogTitle>
            <DialogDescription>
              {commissionerAutodraftTeamId
                ? `Change autodraft for ${draftSlots.find((s) => s.team.id === commissionerAutodraftTeamId)?.team.name ?? "this team"}`
                : "Set a team's autodraft settings"}
            </DialogDescription>
          </DialogHeader>
          {commissionerAutodraftTeamId && (
            <AutodraftSettings
              seasonId={season.id}
              teamId={commissionerAutodraftTeamId}
              isEnabled={autodraftStatus[commissionerAutodraftTeamId]?.isEnabled ?? false}
              mode={autodraftStatus[commissionerAutodraftTeamId]?.mode ?? "next_pick"}
              queueOnly={autodraftStatus[commissionerAutodraftTeamId]?.queueOnly ?? false}
              isMyTurn={false}
              onUpdate={handleCommissionerAutodraftUpdate}
            />
          )}
        </DialogContent>
      </Dialog>

      {/* Connection Overlay - blocks interaction until socket connects */}
      <ConnectionOverlay
        isConnected={isConnected}
        isReconnecting={isReconnecting}
        connectionError={connectionError}
      />

      {/* Auth Recovery Overlay - blocks interaction when session expires */}
      <AuthRecoveryOverlay isAuthDegraded={authDegraded} />
    </div>
  );
}
