import * as schema from "~/database/schema";
import { eq, and, asc, sql, lte, isNotNull } from "drizzle-orm";
import type { InferSelectModel } from "drizzle-orm";
import { getSocketIO } from "./socket";
import { executeAutoPick, calculatePickInfo } from "~/models/draft-utils";
import { isInOvernightWindow, getOvernightResumeUTC } from "~/lib/overnight-pause";
import { logger } from "./logger";
import { db } from "./db";
import { startDraft } from "~/services/draft-autostart";

let timerInterval: NodeJS.Timeout | null = null;
let timerTickRunning = false;

// Draft slots never change during an active draft — cache them to avoid
// a redundant query on every tick.
const draftSlotsCache = new Map<string, { teamId: string; draftOrder: number }[]>();

// Team timezone cache for per_user overnight pause mode (seasonId → teamId → timezone).
// Populated lazily; evicted when the season leaves active drafting.
const teamTimezoneCache = new Map<string, Map<string, string | null>>();

async function getTeamTimezone(seasonId: string, teamId: string): Promise<string | null> {
  let seasonMap = teamTimezoneCache.get(seasonId);
  if (!seasonMap) {
    try {
      const rows = await db
        .select({ teamId: schema.teams.id, timezone: schema.users.timezone })
        .from(schema.teams)
        .leftJoin(schema.users, sql`${schema.teams.ownerId}::uuid = ${schema.users.id}`)
        .where(eq(schema.teams.seasonId, seasonId));
      seasonMap = new Map(rows.map((r) => [r.teamId, r.timezone || null]));
    } catch (err) {
      // ownerId column is varchar; if any row still holds a non-UUID value
      // (pre-migration remnant) the ::uuid cast throws. Degrade gracefully.
      logger.error("[Timer] getTeamTimezone cast failed, skipping overnight check:", err);
      seasonMap = new Map();
    }
    teamTimezoneCache.set(seasonId, seasonMap);
  }
  return seasonMap.get(teamId) ?? null;
}

/**
 * Determine whether the current team's pick is in an overnight pause window.
 * Returns { active: false } or { active: true, resumesAtUTC: number }.
 */
async function checkOvernightPause(
  season: InferSelectModel<typeof schema.seasons>,
  currentTeamId: string
): Promise<{ active: boolean; resumesAtUTC?: number }> {
  const mode = season.overnightPauseMode;
  const start = season.overnightPauseStart;
  const end = season.overnightPauseEnd;

  if (mode === "none" || !start || !end) return { active: false };

  let tz: string | null = null;
  if (mode === "league") {
    tz = season.overnightPauseTimezone || null;
  } else {
    // per_user: use the team owner's timezone, fall back to league timezone
    tz = await getTeamTimezone(season.id, currentTeamId);
    if (!tz) tz = season.overnightPauseTimezone || null;
  }

  if (!tz) return { active: false };

  if (isInOvernightWindow(tz, start, end)) {
    const resumesAt = getOvernightResumeUTC(tz, end);
    return { active: true, resumesAtUTC: resumesAt.getTime() };
  }
  return { active: false };
}

/**
 * Start the draft timer system
 * Runs every second to update all active draft timers
 */
export function startDraftTimerSystem(): void {
  if (timerInterval) {
    logger.log("[Timer] Timer system already running");
    return;
  }

  timerInterval = setInterval(async () => {
    // Skip this tick if the previous one is still running (e.g. long autodraft chain)
    // to prevent concurrent ticks from racing on the same pick slot.
    if (timerTickRunning) return;
    timerTickRunning = true;
    try {
      await updateDraftTimers();
    } catch (error) {
      logger.error("[Timer] Error updating draft timers:", error);
    } finally {
      timerTickRunning = false;
    }
  }, 1000);

  logger.log("[Timer] Draft timer system started");
}

/**
 * Stop the draft timer system
 */
export function stopDraftTimerSystem(): void {
  if (timerInterval) {
    clearInterval(timerInterval);
    timerInterval = null;
    logger.log("[Timer] Draft timer system stopped");
  }
}

async function checkAndAutoStartDrafts(): Promise<void> {
  const io = getSocketIO();
  const seasonsToStart = await db.query.seasons.findMany({
    where: and(
      eq(schema.seasons.status, "pre_draft"),
      eq(schema.seasons.autoStartDraft, true),
      isNotNull(schema.seasons.draftDateTime),
      lte(schema.seasons.draftDateTime, new Date())
    ),
  });

  for (const season of seasonsToStart) {
    logger.log(`[Timer] Auto-starting draft for season ${season.id}`);
    const result = await startDraft({
      seasonId: season.id,
      actorUserId: "system",
      actorDisplayName: "System (auto-start)",
      db,
      io,
    });
    if (result.success) continue;
    if (result.error === "Draft already started or completed") continue;

    logger.error(`[Timer] Auto-start failed for season ${season.id}: ${result.error}`);

    if (result.error === "No draft slots found for this season") {
      // Disable auto-start to stop the retry loop. The commissioner will see
      // the setting is now off and can re-enable it once the order is set.
      await db
        .update(schema.seasons)
        .set({ autoStartDraft: false })
        .where(eq(schema.seasons.id, season.id));
      logger.log(`[Timer] Disabled autoStartDraft for season ${season.id} — no draft order set`);
    }
  }
}

/**
 * Update all active draft timers
 * Called every second by the timer interval
 */
async function updateDraftTimers(): Promise<void> {
  await checkAndAutoStartDrafts();

  const io = getSocketIO();

  // Get all active drafts
  const activeDrafts = await db.query.seasons.findMany({
    where: eq(schema.seasons.status, "draft"),
  });

  if (activeDrafts.length === 0) {
    draftSlotsCache.clear();
    return;
  }

  // Evict cache entries for seasons no longer actively drafting.
  const activeIds = new Set(activeDrafts.map((s) => s.id));
  for (const cachedId of draftSlotsCache.keys()) {
    if (!activeIds.has(cachedId)) draftSlotsCache.delete(cachedId);
  }
  for (const cachedId of teamTimezoneCache.keys()) {
    if (!activeIds.has(cachedId)) teamTimezoneCache.delete(cachedId);
  }

  for (const season of activeDrafts) {
    // Skip if draft is paused
    if (season.draftPaused) {
      continue;
    }

    const currentPickNumber = season.currentPickNumber ?? 1;

    // Draft slots never change during an active draft — use the cache.
    let draftSlots = draftSlotsCache.get(season.id);
    if (!draftSlots) {
      draftSlots = await db.query.draftSlots.findMany({
        where: eq(schema.draftSlots.seasonId, season.id),
        orderBy: asc(schema.draftSlots.draftOrder),
      });
      draftSlotsCache.set(season.id, draftSlots);
    }

    const totalTeams = draftSlots.length;
    if (totalTeams === 0) continue;

    const { pickInRound } = calculatePickInfo(currentPickNumber, totalTeams);
    const currentDraftSlot = draftSlots.find(
      (slot) => slot.draftOrder === pickInRound
    );

    if (!currentDraftSlot) {
      continue;
    }

    const currentTeamId = currentDraftSlot.teamId;

    // Get current team's timer
    const timer = await db.query.draftTimers.findFirst({
      where: and(
        eq(schema.draftTimers.seasonId, season.id),
        eq(schema.draftTimers.teamId, currentTeamId)
      ),
    });

    if (!timer) {
      logger.warn(
        `[Timer] No timer found for team ${currentTeamId} in season ${season.id}, creating with initial time`
      );
      // Standard mode seeds with the per-pick time; chess clock seeds with the full bank.
      const initialTime = season.draftTimerMode === "standard"
        ? (season.draftIncrementTime || 30)
        : (season.draftInitialTime || 120);
      await db
        .insert(schema.draftTimers)
        .values({
          seasonId: season.id,
          teamId: currentTeamId,
          timeRemaining: initialTime,
        });

      // Emit timer update so clients are aware of the new timer
      io.to(`draft-${season.id}`).emit("timer-update", {
        seasonId: season.id,
        teamId: currentTeamId,
        timeRemaining: initialTime,
        currentPickNumber,
      });

      // Continue processing with the newly created timer on the next tick
      continue;
    }

    // Fetch autodraft settings once — used both for while_on bypass and timer-expiry path.
    const autodraftSettings = await db.query.autodraftSettings.findFirst({
      where: and(
        eq(schema.autodraftSettings.seasonId, season.id),
        eq(schema.autodraftSettings.teamId, currentTeamId)
      ),
    });
    const shouldAutodraft = autodraftSettings?.isEnabled ?? false;
    // while_on means "pick immediately when it's my turn" — bypass the countdown.
    const isWhileOn = shouldAutodraft && autodraftSettings?.mode === "while_on";

    // Overnight pause: freeze timer for non-autodraft teams in their overnight window.
    const overnightPause = await checkOvernightPause(season, currentTeamId);
    const isOvernightFreeze = overnightPause.active && !shouldAutodraft;

    // Trigger pick when: timer expired  OR  team is in while_on autodraft mode.
    // The chain picks consecutive while_on teams after each pick, but it is capped at
    // totalTeams iterations. The while_on bypass here fills the gap: the timer picks
    // the next while_on team within one tick (≤1 s) rather than waiting for the full
    // countdown to expire.  If the chain already made this pick, executeAutoPick
    // returns "Pick already made" which triggerAutoPick treats as a non-fatal no-op.
    if (timer.timeRemaining <= 0 || isWhileOn) {
      if (timer.timeRemaining <= 0) {
        logger.log(
          `[Timer] ⚠️  Timer expired for team ${currentTeamId} in season ${season.id} (pick ${currentPickNumber})`
        );
      } else {
        logger.log(
          `[Timer] ⚡ while_on autodraft — immediate pick for team ${currentTeamId} in season ${season.id} (pick ${currentPickNumber})`
        );
        // Emit 0 so clients see the timer hit zero before the pick-made event arrives.
        io.to(`draft-${season.id}`).emit("timer-update", {
          seasonId: season.id,
          teamId: currentTeamId,
          timeRemaining: 0,
          currentPickNumber,
          overnightPauseActive: false,
        });
      }

      const success = await triggerAutoPick(season.id, currentTeamId, currentPickNumber, shouldAutodraft ? (autodraftSettings ?? null) : null);

      if (!success) {
        logger.error(`[Timer] Pausing draft ${season.id} — auto-pick failed for team ${currentTeamId} pick ${currentPickNumber}`);
        await db.update(schema.seasons).set({ draftPaused: true }).where(eq(schema.seasons.id, season.id));
        io.to(`draft-${season.id}`).emit("draft-paused", { seasonId: season.id, paused: true });
      }

      continue;
    }

    // Overnight freeze: skip decrement, notify clients to display the pause indicator.
    if (isOvernightFreeze) {
      io.to(`draft-${season.id}`).emit("timer-update", {
        seasonId: season.id,
        teamId: currentTeamId,
        timeRemaining: timer.timeRemaining,
        currentPickNumber,
        overnightPauseActive: true,
        resumesAtUTC: overnightPause.resumesAtUTC,
      });
      continue;
    }

    // Atomically decrement timer (race-condition safe: uses DB-level update
    // so concurrent increments from pick handlers are never overwritten)
    const [updatedTimer] = await db
      .update(schema.draftTimers)
      .set({
        timeRemaining: sql`GREATEST(${schema.draftTimers.timeRemaining} - 1, 0)`,
        updatedAt: new Date(),
      })
      .where(eq(schema.draftTimers.id, timer.id))
      .returning();

    const newTimeRemaining = updatedTimer?.timeRemaining ?? 0;

    // Emit timer update to all clients in the draft room
    io.to(`draft-${season.id}`).emit("timer-update", {
      seasonId: season.id,
      teamId: currentTeamId,
      timeRemaining: newTimeRemaining,
      currentPickNumber,
      overnightPauseActive: false,
    });

    // If timer just hit 0, trigger auto-pick (next_pick mode or no autodraft)
    if (newTimeRemaining === 0) {
      const success = await triggerAutoPick(season.id, currentTeamId, currentPickNumber, shouldAutodraft ? (autodraftSettings ?? null) : null);

      if (!success) {
        logger.error(`[Timer] Pausing draft ${season.id} — auto-pick failed for team ${currentTeamId} pick ${currentPickNumber}`);
        await db.update(schema.seasons).set({ draftPaused: true }).where(eq(schema.seasons.id, season.id));
        io.to(`draft-${season.id}`).emit("draft-paused", { seasonId: season.id, paused: true });
      }
    }
  }
}

/**
 * Trigger an automatic pick when timer expires.
 * Returns true if the pick succeeded (or was already made by another path),
 * false if a real failure occurred that requires commissioner intervention.
 */
async function triggerAutoPick(
  seasonId: string,
  teamId: string,
  pickNumber: number,
  autodraftSettings: InferSelectModel<typeof schema.autodraftSettings> | null
): Promise<boolean> {
  try {
    const result = await executeAutoPick({
      seasonId,
      teamId,
      pickNumber,
      triggeredBy: "timer",
      autodraftSettings,
      db,
    });

    if (!result.success) {
      // A race condition where the pick was already made is not a real failure
      if (result.error === "Pick already made") {
        return true;
      }
      logger.error(`[Timer] Auto-pick failed: ${result.error}`);
      return false;
    }

    return true;
  } catch (error) {
    logger.error("[Timer] Error in triggerAutoPick:", error);
    return false;
  }
}
