/**
 * Batch Messaging System
 * Aggregates multiple small messages into larger batches for efficient transmission.
 *
 * Features:
 * - Automatic batching of messages within a time window
 * - Configurable batch size and flush intervals
 * - Priority-based message ordering
 * - Compression for large payloads
 * - Statistics tracking
 */

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

export type MessagePriority = 'high' | 'normal' | 'low';

export interface QueuedMessage {
  type: string;
  data: Record<string, unknown>;
  priority: MessagePriority;
  timestamp: number;
}

export interface BatchedPayload {
  type: 'batch';
  messages: Array<{ type: string; data: Record<string, unknown> }>;
  count: number;
  timestamp: number;
}

export interface BatchStats {
  totalMessages: number;
  batchesSent: number;
  averageBatchSize: number;
  messagesDropped: number;
  bytesTransmitted: number;
  compressionRatio: number;
}

export interface BatchOptions {
  maxBatchSize?: number;
  flushIntervalMs?: number;
  maxQueueSize?: number;
  compressionThreshold?: number;
  onFlush?: (playerId: string, payload: BatchedPayload) => void;
}

// ============================================================================
// MessageBatcher Class
// ============================================================================

/**
 * Batches messages for a single player/connection.
 * Messages are accumulated and flushed either when the batch size is reached
 * or when the flush interval elapses.
 */
export class MessageBatcher {
  private messageQueue: QueuedMessage[] = [];
  private maxBatchSize: number;
  private flushIntervalMs: number;
  private maxQueueSize: number;
  private compressionThreshold: number;
  private flushTimer: NodeJS.Timeout | null = null;
  private onFlush: ((payload: BatchedPayload) => void) | null;

  // Statistics
  private totalMessages: number = 0;
  private batchesSent: number = 0;
  private messagesDropped: number = 0;
  private bytesTransmitted: number = 0;
  private uncompressedBytes: number = 0;

  /**
   * Create a new message batcher.
   *
   * @param maxBatchSize - Max messages per batch (default: 50)
   * @param flushIntervalMs - Time before auto-flush (default: 100ms)
   * @param maxQueueSize - Max queued messages before dropping (default: 500)
   */
  constructor(options: BatchOptions = {}) {
    this.maxBatchSize = options.maxBatchSize ?? 50;
    this.flushIntervalMs = options.flushIntervalMs ?? 100;
    this.maxQueueSize = options.maxQueueSize ?? 500;
    this.compressionThreshold = options.compressionThreshold ?? 1024;
    this.onFlush = null;
  }

  /**
   * Set the flush callback.
   */
  setFlushCallback(callback: (payload: BatchedPayload) => void): void {
    this.onFlush = callback;
  }

  /**
   * Queue a message for batching.
   *
   * @param type - Message type
   * @param data - Message payload
   * @param priority - Message priority (high messages are sent first)
   * @returns true if queued, false if dropped due to queue size
   */
  enqueue(type: string, data: Record<string, unknown>, priority: MessagePriority = 'normal'): boolean {
    // Check queue size limit
    if (this.messageQueue.length >= this.maxQueueSize) {
      // Drop lowest priority messages first
      if (priority === 'low') {
        this.messagesDropped++;
        return false;
      }

      // Remove oldest low priority message if exists
      const lowPriorityIndex = this.messageQueue.findIndex((m) => m.priority === 'low');
      if (lowPriorityIndex !== -1) {
        this.messageQueue.splice(lowPriorityIndex, 1);
        this.messagesDropped++;
      } else {
        // No low priority messages, drop if not high priority
        if (priority !== 'high') {
          this.messagesDropped++;
          return false;
        }
        // Remove oldest normal priority message
        const normalPriorityIndex = this.messageQueue.findIndex((m) => m.priority === 'normal');
        if (normalPriorityIndex !== -1) {
          this.messageQueue.splice(normalPriorityIndex, 1);
          this.messagesDropped++;
        }
      }
    }

    // Add message to queue
    this.messageQueue.push({
      type,
      data,
      priority,
      timestamp: Date.now(),
    });

    this.totalMessages++;

    // Start flush timer if not already running
    if (!this.flushTimer) {
      this.flushTimer = setTimeout(() => this.flush(), this.flushIntervalMs);
    }

    // Check if we should flush immediately
    if (this.messageQueue.length >= this.maxBatchSize || priority === 'high') {
      this.flush();
    }

    return true;
  }

  /**
   * Queue a high-priority message (flushes immediately).
   */
  enqueueHighPriority(type: string, data: Record<string, unknown>): boolean {
    return this.enqueue(type, data, 'high');
  }

  /**
   * Flush all queued messages immediately.
   *
   * @returns The batched payload, or null if queue is empty
   */
  flush(): BatchedPayload | null {
    // Clear flush timer
    if (this.flushTimer) {
      clearTimeout(this.flushTimer);
      this.flushTimer = null;
    }

    if (this.messageQueue.length === 0) {
      return null;
    }

    // Sort by priority (high first) then by timestamp
    const priorityOrder: Record<MessagePriority, number> = {
      high: 0,
      normal: 1,
      low: 2,
    };

    this.messageQueue.sort((a, b) => {
      const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
      if (priorityDiff !== 0) return priorityDiff;
      return a.timestamp - b.timestamp;
    });

    // Create batched payload
    const messages = this.messageQueue.map((m) => ({
      type: m.type,
      data: m.data,
    }));

    const payload: BatchedPayload = {
      type: 'batch',
      messages,
      count: messages.length,
      timestamp: Date.now(),
    };

    // Track statistics
    const payloadStr = JSON.stringify(payload);
    this.uncompressedBytes += payloadStr.length;
    this.bytesTransmitted += payloadStr.length;
    this.batchesSent++;

    // Clear queue
    this.messageQueue = [];

    // Call flush callback
    if (this.onFlush) {
      this.onFlush(payload);
    }

    return payload;
  }

  /**
   * Get the number of queued messages.
   */
  get queueSize(): number {
    return this.messageQueue.length;
  }

  /**
   * Get batcher statistics.
   */
  getStats(): BatchStats {
    return {
      totalMessages: this.totalMessages,
      batchesSent: this.batchesSent,
      averageBatchSize: this.batchesSent > 0 ? this.totalMessages / this.batchesSent : 0,
      messagesDropped: this.messagesDropped,
      bytesTransmitted: this.bytesTransmitted,
      compressionRatio:
        this.uncompressedBytes > 0 ? this.bytesTransmitted / this.uncompressedBytes : 1,
    };
  }

  /**
   * Reset statistics.
   */
  resetStats(): void {
    this.totalMessages = 0;
    this.batchesSent = 0;
    this.messagesDropped = 0;
    this.bytesTransmitted = 0;
    this.uncompressedBytes = 0;
  }

  /**
   * Clear all queued messages without sending.
   */
  clear(): void {
    if (this.flushTimer) {
      clearTimeout(this.flushTimer);
      this.flushTimer = null;
    }
    this.messageQueue = [];
  }

  /**
   * Destroy the batcher, clearing all timers.
   */
  destroy(): void {
    this.clear();
  }
}

// ============================================================================
// BatchMessageManager Class
// ============================================================================

/**
 * Manages message batchers for multiple players.
 */
export class BatchMessageManager {
  private batchers: Map<string, MessageBatcher> = new Map();
  private options: BatchOptions;
  private sendCallback: ((playerId: string, payload: BatchedPayload) => void) | null = null;

  constructor(options: BatchOptions = {}) {
    this.options = options;
  }

  /**
   * Set the callback for sending batched messages.
   */
  setSendCallback(callback: (playerId: string, payload: BatchedPayload) => void): void {
    this.sendCallback = callback;
  }

  /**
   * Get or create a batcher for a player.
   */
  private getBatcher(playerId: string): MessageBatcher {
    let batcher = this.batchers.get(playerId);
    if (!batcher) {
      batcher = new MessageBatcher(this.options);
      batcher.setFlushCallback((payload) => {
        if (this.sendCallback) {
          this.sendCallback(playerId, payload);
        }
      });
      this.batchers.set(playerId, batcher);
    }
    return batcher;
  }

  /**
   * Queue a message for a player.
   */
  queueMessage(
    playerId: string,
    type: string,
    data: Record<string, unknown>,
    priority: MessagePriority = 'normal'
  ): boolean {
    const batcher = this.getBatcher(playerId);
    return batcher.enqueue(type, data, priority);
  }

  /**
   * Queue a high-priority message for a player.
   */
  queueHighPriority(playerId: string, type: string, data: Record<string, unknown>): boolean {
    const batcher = this.getBatcher(playerId);
    return batcher.enqueueHighPriority(type, data);
  }

  /**
   * Flush messages for a specific player.
   */
  flushPlayer(playerId: string): BatchedPayload | null {
    const batcher = this.batchers.get(playerId);
    if (!batcher) return null;
    return batcher.flush();
  }

  /**
   * Flush messages for all players.
   */
  flushAll(): Map<string, BatchedPayload> {
    const results = new Map<string, BatchedPayload>();
    for (const [playerId, batcher] of this.batchers.entries()) {
      const payload = batcher.flush();
      if (payload) {
        results.set(playerId, payload);
      }
    }
    return results;
  }

  /**
   * Remove a player's batcher (on disconnect).
   */
  removePlayer(playerId: string): void {
    const batcher = this.batchers.get(playerId);
    if (batcher) {
      batcher.destroy();
      this.batchers.delete(playerId);
    }
  }

  /**
   * Get statistics for a player.
   */
  getPlayerStats(playerId: string): BatchStats | null {
    const batcher = this.batchers.get(playerId);
    return batcher?.getStats() ?? null;
  }

  /**
   * Get aggregated statistics for all players.
   */
  getAllStats(): BatchStats {
    const aggregate: BatchStats = {
      totalMessages: 0,
      batchesSent: 0,
      averageBatchSize: 0,
      messagesDropped: 0,
      bytesTransmitted: 0,
      compressionRatio: 1,
    };

    let totalUncompressed = 0;

    for (const batcher of this.batchers.values()) {
      const stats = batcher.getStats();
      aggregate.totalMessages += stats.totalMessages;
      aggregate.batchesSent += stats.batchesSent;
      aggregate.messagesDropped += stats.messagesDropped;
      aggregate.bytesTransmitted += stats.bytesTransmitted;
      totalUncompressed += stats.bytesTransmitted / stats.compressionRatio;
    }

    aggregate.averageBatchSize =
      aggregate.batchesSent > 0 ? aggregate.totalMessages / aggregate.batchesSent : 0;
    aggregate.compressionRatio =
      totalUncompressed > 0 ? aggregate.bytesTransmitted / totalUncompressed : 1;

    return aggregate;
  }

  /**
   * Get the number of active batchers.
   */
  get playerCount(): number {
    return this.batchers.size;
  }

  /**
   * Clear all batchers.
   */
  clear(): void {
    for (const batcher of this.batchers.values()) {
      batcher.destroy();
    }
    this.batchers.clear();
  }
}

// ============================================================================
// Message Deduplication
// ============================================================================

/**
 * Deduplicates messages within a batch.
 * For messages of the same type, keeps only the most recent one.
 */
export function deduplicateBatch(
  messages: Array<{ type: string; data: Record<string, unknown> }>,
  deduplicateTypes: string[] = ['u', 'playerUpdate', 'statsUpdate']
): Array<{ type: string; data: Record<string, unknown> }> {
  const seen = new Map<string, number>();
  const result: Array<{ type: string; data: Record<string, unknown> }> = [];

  // Process in reverse to keep the latest occurrence
  for (let i = messages.length - 1; i >= 0; i--) {
    const msg = messages[i];

    if (deduplicateTypes.includes(msg.type)) {
      if (!seen.has(msg.type)) {
        seen.set(msg.type, i);
        result.unshift(msg);
      }
    } else {
      result.unshift(msg);
    }
  }

  return result;
}

/**
 * Merges consecutive messages of the same type.
 * Useful for update messages where only the final state matters.
 */
export function mergeBatch(
  messages: Array<{ type: string; data: Record<string, unknown> }>,
  mergeTypes: string[] = ['u']
): Array<{ type: string; data: Record<string, unknown> }> {
  if (messages.length <= 1) return messages;

  const result: Array<{ type: string; data: Record<string, unknown> }> = [];
  let currentMerge: { type: string; data: Record<string, unknown> } | null = null;

  for (const msg of messages) {
    if (mergeTypes.includes(msg.type)) {
      if (currentMerge && currentMerge.type === msg.type) {
        // Merge data (later values override earlier ones)
        currentMerge.data = { ...currentMerge.data, ...msg.data };
      } else {
        if (currentMerge) {
          result.push(currentMerge);
        }
        currentMerge = { type: msg.type, data: { ...msg.data } };
      }
    } else {
      if (currentMerge) {
        result.push(currentMerge);
        currentMerge = null;
      }
      result.push(msg);
    }
  }

  if (currentMerge) {
    result.push(currentMerge);
  }

  return result;
}

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

let batchManager: BatchMessageManager | null = null;

/**
 * Get the global batch message manager.
 */
export function getBatchMessageManager(options?: BatchOptions): BatchMessageManager {
  if (!batchManager) {
    batchManager = new BatchMessageManager({
      maxBatchSize: 50,
      flushIntervalMs: 100,
      maxQueueSize: 500,
      ...options,
    });
  }
  return batchManager;
}

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

/**
 * Send a message through the batching system.
 *
 * @param playerId - Player to send to
 * @param type - Message type
 * @param data - Message data
 * @param priority - Message priority
 */
export function sendBatchedMessage(
  playerId: string,
  type: string,
  data: Record<string, unknown>,
  priority: MessagePriority = 'normal'
): boolean {
  return getBatchMessageManager().queueMessage(playerId, type, data, priority);
}

/**
 * Send a high-priority message (bypasses batching).
 */
export function sendImmediateMessage(
  playerId: string,
  type: string,
  data: Record<string, unknown>
): boolean {
  return getBatchMessageManager().queueHighPriority(playerId, type, data);
}

/**
 * Flush all pending batches for a player.
 */
export function flushPlayerMessages(playerId: string): BatchedPayload | null {
  return getBatchMessageManager().flushPlayer(playerId);
}

/**
 * Log batch messaging statistics.
 */
export function logBatchStats(): void {
  const manager = getBatchMessageManager();
  const stats = manager.getAllStats();
  console.log(
    `[BatchMessaging] Stats: ` +
    `players=${manager.playerCount}, ` +
    `messages=${stats.totalMessages}, ` +
    `batches=${stats.batchesSent}, ` +
    `avgSize=${stats.averageBatchSize.toFixed(1)}, ` +
    `dropped=${stats.messagesDropped}, ` +
    `bytes=${stats.bytesTransmitted}`
  );
}

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

export const batchMessagingService = {
  MessageBatcher,
  BatchMessageManager,
  getBatchMessageManager,
  sendBatchedMessage,
  sendImmediateMessage,
  flushPlayerMessages,
  deduplicateBatch,
  mergeBatch,
  logBatchStats,
};
