/**
 * Offline Event Queue System
 * Manages event queuing and replay for offline players.
 *
 * Ported from Python: ws/performance/offline/queue.py
 */

import { Player } from '../../models/Player.js';
import { getJobManager } from './jobs.js';

// ============================================================================
// Types
// ============================================================================

export interface QueuedEvent {
  type: string;
  id: string;
  message?: string;
  data?: Record<string, unknown>;
  queuedAt: number; // Unix timestamp in milliseconds
}

export interface OfflineQueueStats {
  offlinePlayers: number;
  totalQueuedEvents: number;
  maxQueueSize: number;
  maxOfflineHours: number;
}

export interface ReconnectInfo {
  offlineDurationSeconds: number | null;
  offlineDurationHours: number;
  queuedEvents: QueuedEvent[];
  eventCount: number;
}

// ============================================================================
// OfflineEventQueue Class
// ============================================================================

/**
 * Manages queuing of events that occur while a player is offline.
 * Events are stored and replayed when the player reconnects.
 */
export class OfflineEventQueue {
  private maxQueueSize: number;
  private maxOfflineHours: number;
  private queues: Map<string, QueuedEvent[]>; // playerId -> list of events
  private offlineSince: Map<string, number>; // playerId -> timestamp

  /**
   * Initialize the offline event queue.
   *
   * @param maxQueueSize - Maximum events to queue per player (default: 1000)
   * @param maxOfflineHours - Maximum hours to keep offline events (default: 168 = 7 days)
   */
  constructor(maxQueueSize = 1000, maxOfflineHours = 168) {
    this.maxQueueSize = maxQueueSize;
    this.maxOfflineHours = maxOfflineHours;
    this.queues = new Map();
    this.offlineSince = new Map();
  }

  /**
   * Mark a player as offline and start queuing events.
   *
   * @param playerId - Player's unique identifier
   */
  markOffline(playerId: string): void {
    if (!this.offlineSince.has(playerId)) {
      this.offlineSince.set(playerId, Date.now());
      if (!this.queues.has(playerId)) {
        this.queues.set(playerId, []);
      }
      console.log(`[OfflineQueue] Player ${playerId} marked offline`);
    }
  }

  /**
   * Mark a player as online and return queued events.
   *
   * @param playerId - Player's unique identifier
   * @returns List of queued events to replay
   */
  markOnline(playerId: string): QueuedEvent[] {
    if (this.offlineSince.has(playerId)) {
      const offlineSince = this.offlineSince.get(playerId)!;
      const offlineDuration = Date.now() - offlineSince;
      this.offlineSince.delete(playerId);

      const events = this.queues.get(playerId) ?? [];
      this.queues.delete(playerId);

      console.log(
        `[OfflineQueue] Player ${playerId} back online after ` +
        `${(offlineDuration / 3600000).toFixed(1)} hours, replaying ${events.length} events`
      );
      return events;
    }
    return [];
  }

  /**
   * Check if a player is currently marked as offline.
   *
   * @param playerId - Player's unique identifier
   * @returns True if player is offline
   */
  isOffline(playerId: string): boolean {
    return this.offlineSince.has(playerId);
  }

  /**
   * Queue an event for an offline player.
   *
   * @param playerId - Player's unique identifier
   * @param event - Event data to queue
   * @returns True if queued, false if player is online or queue full
   */
  queueEvent(playerId: string, event: Omit<QueuedEvent, 'queuedAt'>): boolean {
    // Only queue if player is marked offline
    if (!this.offlineSince.has(playerId)) {
      return false;
    }

    // Check if we've exceeded max offline time
    const offlineSince = this.offlineSince.get(playerId)!;
    const offlineDuration = Date.now() - offlineSince;
    const maxOfflineMs = this.maxOfflineHours * 3600000;

    if (offlineDuration > maxOfflineMs) {
      console.warn(
        `[OfflineQueue] Player ${playerId} offline too long ` +
        `(${(offlineDuration / 3600000).toFixed(1)}h), discarding events`
      );
      this.clearQueue(playerId);
      return false;
    }

    // Initialize queue if needed
    if (!this.queues.has(playerId)) {
      this.queues.set(playerId, []);
    }

    const queue = this.queues.get(playerId)!;

    // Check queue size limit
    if (queue.length >= this.maxQueueSize) {
      console.warn(
        `[OfflineQueue] Queue full for player ${playerId}, discarding oldest event`
      );
      queue.shift(); // Remove oldest event
    }

    // Add event with timestamp
    const queuedEvent: QueuedEvent = {
      ...event,
      queuedAt: Date.now(),
    };
    queue.push(queuedEvent);
    console.debug(`[OfflineQueue] Queued event for offline player ${playerId}`);
    return true;
  }

  /**
   * Get how long a player has been offline in seconds.
   *
   * @param playerId - Player's unique identifier
   * @returns Offline duration in seconds, or null if online
   */
  getOfflineDuration(playerId: string): number | null {
    if (this.offlineSince.has(playerId)) {
      return (Date.now() - this.offlineSince.get(playerId)!) / 1000;
    }
    return null;
  }

  /**
   * Clear all queued events for a player.
   *
   * @param playerId - Player's unique identifier
   */
  clearQueue(playerId: string): void {
    this.queues.delete(playerId);
    console.log(`[OfflineQueue] Cleared event queue for player ${playerId}`);
  }

  /**
   * Get number of queued events for a player.
   *
   * @param playerId - Player's unique identifier
   * @returns Number of queued events
   */
  getQueueSize(playerId: string): number {
    return this.queues.get(playerId)?.length ?? 0;
  }

  /**
   * Get queue statistics.
   *
   * @returns Dictionary with queue stats
   */
  getStats(): OfflineQueueStats {
    let totalEvents = 0;
    for (const queue of this.queues.values()) {
      totalEvents += queue.length;
    }

    return {
      offlinePlayers: this.offlineSince.size,
      totalQueuedEvents: totalEvents,
      maxQueueSize: this.maxQueueSize,
      maxOfflineHours: this.maxOfflineHours,
    };
  }

  /**
   * Remove queues for players offline longer than maxOfflineHours.
   *
   * @returns Number of expired queues removed
   */
  cleanupExpired(): number {
    const currentTime = Date.now();
    const maxOfflineMs = this.maxOfflineHours * 3600000;
    const expired: string[] = [];

    for (const [playerId, offlineSince] of this.offlineSince.entries()) {
      if (currentTime - offlineSince > maxOfflineMs) {
        expired.push(playerId);
      }
    }

    for (const playerId of expired) {
      console.log(`[OfflineQueue] Removing expired queue for player ${playerId}`);
      this.offlineSince.delete(playerId);
      this.queues.delete(playerId);
    }

    return expired.length;
  }

  /**
   * Get all offline player IDs.
   *
   * @returns Array of offline player IDs
   */
  getOfflinePlayerIds(): string[] {
    return Array.from(this.offlineSince.keys());
  }
}

// ============================================================================
// Global Queue Instance
// ============================================================================

let offlineQueue: OfflineEventQueue | null = null;

/**
 * Get the global offline queue instance.
 */
export function getOfflineQueue(): OfflineEventQueue {
  if (!offlineQueue) {
    offlineQueue = new OfflineEventQueue(1000, 168); // 1000 events, 7 days
  }
  return offlineQueue;
}

// ============================================================================
// Helper Functions
// ============================================================================

/**
 * Handle player disconnection - start queuing events.
 *
 * @param playerId - Player's unique identifier
 */
export function handlePlayerDisconnect(playerId: string): void {
  getOfflineQueue().markOffline(playerId);
}

/**
 * Handle player reconnection - return queued events and stats.
 *
 * @param playerId - Player's unique identifier
 * @returns Dictionary with offline duration and queued events
 */
export function handlePlayerReconnect(playerId: string): ReconnectInfo {
  const queue = getOfflineQueue();
  const offlineDuration = queue.getOfflineDuration(playerId);
  const events = queue.markOnline(playerId);

  return {
    offlineDurationSeconds: offlineDuration,
    offlineDurationHours: offlineDuration ? offlineDuration / 3600 : 0,
    queuedEvents: events,
    eventCount: events.length,
  };
}

/**
 * Replay queued events to a player after reconnection.
 *
 * @param playerId - Player's unique identifier
 * @param sendEvent - Function to send each event to the player
 * @returns Number of events replayed
 */
export async function replayEvents(
  playerId: string,
  sendEvent: (event: QueuedEvent) => Promise<void>
): Promise<number> {
  const reconnectInfo = handlePlayerReconnect(playerId);
  const events = reconnectInfo.queuedEvents;

  console.log(
    `[OfflineQueue] Replaying ${events.length} events for player ${playerId} ` +
    `(offline for ${reconnectInfo.offlineDurationHours.toFixed(1)} hours)`
  );

  for (const event of events) {
    try {
      await sendEvent(event);
    } catch (error) {
      console.error(
        `[OfflineQueue] Error replaying event ${event.id} for player ${playerId}:`,
        error
      );
    }
  }

  return events.length;
}

// ============================================================================
// Background Job: Iterate Offline Games
// ============================================================================

export interface IterateGamesOptions {
  loadGames: () => Promise<Array<{ playerId: string }>>;
  loadGameAsync: (playerId: string) => Promise<Player | null>;
  saveGameAsync: (player: Player) => Promise<void>;
  processGameTick: (player: Player) => Promise<void>;
}

/**
 * Iterate through all saved games and process one tick for disconnected players.
 *
 * This function loads all games from the database and runs offline simulation
 * for players who are disconnected. Used for background game progression.
 *
 * Ported from Python: ws/game_loop/loop_manager.py -> iterateGames()
 */
export async function iterateGames(options: IterateGamesOptions): Promise<void> {
  const { loadGames, loadGameAsync, saveGameAsync, processGameTick } = options;
  const queue = getOfflineQueue();

  try {
    const games = await loadGames();

    if (!games || games.length === 0) {
      return;
    }

    for (const game of games) {
      // Only process players marked as offline
      if (queue.isOffline(game.playerId)) {
        try {
          const player = await loadGameAsync(game.playerId);

          if (player && player.c?.status === 'alive') {
            console.log(
              `[OfflineQueue] Iterating game for ${player.c.firstname} ${player.c.lastname} ` +
              `at ${player.minuteOfHour}min`
            );

            // Process one game tick
            await processGameTick(player);

            // Update offline stats
            player.connection = 'disconnected';
            player.controller = 'inactive';
            player.offlineStats = player.offlineStats ?? { minutesOffline: 0 };
            player.offlineStats.minutesOffline += 1;

            // Save the game
            await saveGameAsync(player);
          }
        } catch (error) {
          console.error(
            `[OfflineQueue] Error processing offline game ${game.playerId}:`,
            error
          );
        }
      }
    }
  } catch (error) {
    console.error('[OfflineQueue] Error iterating games:', error);
  }
}

// ============================================================================
// Background Job Registration
// ============================================================================

/**
 * Register offline queue maintenance jobs.
 *
 * @param intervalMs - Interval for cleanup job (default: 1 hour)
 */
export function registerOfflineQueueJobs(intervalMs = 3600000): void {
  const manager = getJobManager();

  // Cleanup expired offline queues every hour
  manager.register(
    'offline_queue_cleanup',
    intervalMs,
    () => {
      const queue = getOfflineQueue();
      const expired = queue.cleanupExpired();
      if (expired > 0) {
        console.log(`[Job] Cleaned up ${expired} expired offline queues`);
      }
      const stats = queue.getStats();
      console.log(
        `[Job] Offline queue stats: ${stats.offlinePlayers} offline players, ` +
        `${stats.totalQueuedEvents} queued events`
      );
    },
    false
  );
}

// ============================================================================
// Exports
// ============================================================================

export const offlineQueueService = {
  OfflineEventQueue,
  getOfflineQueue,
  handlePlayerDisconnect,
  handlePlayerReconnect,
  replayEvents,
  iterateGames,
  registerOfflineQueueJobs,
};
