/**
 * Slot Game Engine - RNG & Outcome Calculation
 * Server-side random number generation and spin outcome calculation
 */

import crypto from 'crypto';
import type { GameConfig, SpinResult, WinLine, Symbol, BonusTriggered } from './types.ts';

export class RNGEngine {
  /**
   * Generate cryptographically secure random number
   */
  static generateRandomNumber(min: number = 0, max: number = 1): number {
    const range = max - min;
    const randomBytes = crypto.randomBytes(4);
    const randomValue = randomBytes.readUInt32BE(0) / 0xffffffff;
    return min + randomValue * range;
  }

  /**
   * Generate weighted random selection
   */
  static weightedRandom<T extends { weight: number }>(items: T[]): T {
    const totalWeight = items.reduce((sum, item) => sum + item.weight, 0);
    let random = this.generateRandomNumber(0, totalWeight);

    for (const item of items) {
      random -= item.weight;
      if (random <= 0) return item;
    }

    return items[items.length - 1];
  }

  /**
   * Shuffle array using Fisher-Yates algorithm
   */
  static shuffle<T>(array: T[]): T[] {
    const shuffled = [...array];
    for (let i = shuffled.length - 1; i > 0; i--) {
      const j = Math.floor(this.generateRandomNumber(0, i + 1));
      [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
    }
    return shuffled;
  }

  /**
   * Generate reel outcome with RTP control
   */
  static generateReelOutcome(
    game: GameConfig,
    betAmount: number,
    targetRtp: number = game.rtp
  ): { reels: Symbol[][]; winAmount: number; multiplier: number } {
    const reels: Symbol[][] = [];
    let winAmount = 0;
    let multiplier = 1;

    // Generate random reel positions
    for (let reelIndex = 0; reelIndex < game.reels; reelIndex++) {
      const reel = game.reelConfig[reelIndex];
      const reelSymbols: Symbol[] = [];

      for (let rowIndex = 0; rowIndex < game.rows; rowIndex++) {
        const randomIndex = Math.floor(this.generateRandomNumber(0, reel.symbols.length));
        reelSymbols.push(reel.symbols[randomIndex]);
      }

      reels.push(reelSymbols);
    }

    // Calculate wins based on paylines or ways-to-win
    if (game.waysToWin?.enabled) {
      const waysWin = this.calculateWaysToWin(reels, game);
      winAmount = waysWin.amount;
      multiplier = waysWin.multiplier;
    } else if (game.paylines && game.paylines.length > 0) {
      const paylineWins = this.calculatePaylineWins(reels, game);
      winAmount = paylineWins.amount;
      multiplier = paylineWins.multiplier;
    }

    // Apply RTP control: if win is too low, boost it
    const expectedWin = betAmount * (targetRtp / 100);
    if (winAmount < expectedWin * 0.5 && this.generateRandomNumber() < 0.15) {
      winAmount = Math.floor(expectedWin * this.generateRandomNumber(0.8, 1.2));
    }

    return { reels, winAmount, multiplier };
  }

  /**
   * Calculate wins using paylines
   */
  static calculatePaylineWins(
    reels: Symbol[][],
    game: GameConfig
  ): { amount: number; multiplier: number } {
    let totalWin = 0;
    let multiplier = 1;

    if (!game.paylines) return { amount: 0, multiplier: 1 };

    for (const payline of game.paylines) {
      let isWin = true;
      let symbolMatch = reels[0][payline.positions[0]];
      let matchCount = 1;

      // Check if symbols match along payline
      for (let i = 1; i < payline.positions.length; i++) {
        const symbol = reels[i][payline.positions[i]];
        if (symbol.id === symbolMatch.id || symbol.type === 'wild' || symbolMatch.type === 'wild') {
          matchCount++;
        } else {
          isWin = false;
          break;
        }
      }

      if (isWin && matchCount >= 3) {
        const winAmount = symbolMatch.value * payline.multiplier * matchCount;
        totalWin += winAmount;
        multiplier = Math.max(multiplier, symbolMatch.multiplier || 1);
      }
    }

    return { amount: totalWin, multiplier };
  }

  /**
   * Calculate wins using ways-to-win system
   */
  static calculateWaysToWin(
    reels: Symbol[][],
    game: GameConfig
  ): { amount: number; multiplier: number } {
    let totalWin = 0;
    let multiplier = 1;

    if (!game.waysToWin?.enabled) return { amount: 0, multiplier: 1 };

    // Check for matching symbols from left to right
    const matchingCombos = this.findMatchingCombos(reels, game);

    for (const combo of matchingCombos) {
      const symbol = combo.symbol;
      const ways = combo.ways;
      const winAmount = symbol.value * ways;
      totalWin += winAmount;
      multiplier = Math.max(multiplier, symbol.multiplier || 1);
    }

    return { amount: totalWin, multiplier };
  }

  /**
   * Find all matching symbol combinations
   */
  static findMatchingCombos(
    reels: Symbol[][],
    game: GameConfig
  ): Array<{ symbol: Symbol; ways: number }> {
    const combos: Array<{ symbol: Symbol; ways: number }> = [];
    const firstReelSymbols = reels[0];

    for (const symbol of firstReelSymbols) {
      let ways = 1;
      let isMatching = true;

      for (let reelIndex = 1; reelIndex < reels.length; reelIndex++) {
        const matchingInReel = reels[reelIndex].filter(
          (s) => s.id === symbol.id || s.type === 'wild' || symbol.type === 'wild'
        ).length;

        if (matchingInReel === 0) {
          isMatching = false;
          break;
        }

        ways *= matchingInReel;
      }

      if (isMatching && ways > 0) {
        combos.push({ symbol, ways });
      }
    }

    return combos;
  }

  /**
   * Calculate cascading reel wins
   */
  static calculateCascadeWins(
    initialReels: Symbol[][],
    game: GameConfig
  ): { totalWin: number; cascadeCount: number; finalReels: Symbol[][] } {
    let currentReels = initialReels;
    let totalWin = 0;
    let cascadeCount = 0;

    while (cascadeCount < 10) {
      // Calculate wins for current reel state
      const { amount: cascadeWin } = this.calculatePaylineWins(currentReels, game);

      if (cascadeWin === 0) break;

      totalWin += cascadeWin;
      cascadeCount++;

      // Remove winning symbols and drop new ones
      currentReels = this.dropNewSymbols(currentReels, game);
    }

    return { totalWin, cascadeCount, finalReels: currentReels };
  }

  /**
   * Drop new symbols after cascade
   */
  static dropNewSymbols(reels: Symbol[][], game: GameConfig): Symbol[][] {
    const newReels = reels.map((reel) => [...reel]);

    for (let reelIndex = 0; reelIndex < newReels.length; reelIndex++) {
      for (let rowIndex = 0; rowIndex < game.rows; rowIndex++) {
        const randomIndex = Math.floor(
          this.generateRandomNumber(0, game.reelConfig[reelIndex].symbols.length)
        );
        newReels[reelIndex][rowIndex] = game.reelConfig[reelIndex].symbols[randomIndex];
      }
    }

    return newReels;
  }

  /**
   * Check for bonus trigger
   */
  static checkBonusTriggered(
    reels: Symbol[][],
    game: GameConfig
  ): BonusTriggered | undefined {
    if (!game.bonusConfig) return undefined;

    // Check free spins trigger
    if (game.bonusConfig.freeSpins) {
      const scatterCount = this.countSymbol(reels, game.bonusConfig.freeSpins.triggerSymbol);
      if (scatterCount >= game.bonusConfig.freeSpins.triggerCount) {
        return {
          type: 'free_spins',
          data: {
            spins: game.bonusConfig.freeSpins.baseSpins,
            multiplier: game.bonusConfig.freeSpins.multiplier || 1,
          },
        };
      }
    }

    // Check collector trigger
    if (game.bonusConfig.collector) {
      const collectorCount = this.countSymbol(reels, game.bonusConfig.collector.triggerSymbol);
      if (collectorCount > 0) {
        return {
          type: 'collector',
          data: { count: collectorCount },
        };
      }
    }

    return undefined;
  }

  /**
   * Count occurrences of a symbol in reels
   */
  static countSymbol(reels: Symbol[][], symbolId: string): number {
    return reels.reduce(
      (count, reel) => count + reel.filter((s) => s.id === symbolId).length,
      0
    );
  }
}
