import { EventEmitter } from 'events';

export interface SlotSymbol {
  id: string;
  name: string;
  value: number;
  multiplier: number;
  isWild: boolean;
  isScatter: boolean;
  isBonus: boolean;
}

export interface SlotReel {
  id: string;
  symbols: SlotSymbol[];
  currentPosition: number;
  isSpinning: boolean;
}

export interface SlotGameConfig {
  gameId: string;
  gameName: string;
  reels: number;
  rows: number;
  paylines: number;
  minBet: number;
  maxBet: number;
  rtp: number;
  volatility: 'low' | 'medium' | 'high';
  symbols: SlotSymbol[];
  features: string[];
  bonusConfig?: {
    trigger: 'scatter' | 'bonus' | 'collector';
    spins: number;
    retrigger: boolean;
    multiplier?: number;
  };
  cascadingReels?: boolean;
  clusterPays?: boolean;
  megaways?: boolean;
}

export interface SpinResult {
  reels: SlotSymbol[][];
  winAmount: number;
  newBalance: number;
  winLines: number[][];
  bonusTriggered: boolean;
  freeSpinsAwarded: number;
  cascadeCount: number;
  multiplier: number;
  isWin: boolean;
}

export interface PaylineResult {
  paylineId: number;
  symbols: SlotSymbol[];
  winAmount: number;
  multiplier: number;
}

class SlotGameEngine extends EventEmitter {
  private config: SlotGameConfig;
  private reels: SlotReel[] = [];
  private paylineResults: PaylineResult[] = [];
  private rtpController: Map<number, number> = new Map(); // Tracks RTP per bet level

  constructor(config: SlotGameConfig) {
    super();
    this.config = config;
    this.initializeReels();
    this.initializeRTPController();
  }

  /**
   * Initialize reels with symbols
   */
  private initializeReels(): void {
    for (let i = 0; i < this.config.reels; i++) {
      const reel: SlotReel = {
        id: `reel-${i}`,
        symbols: [...this.config.symbols],
        currentPosition: Math.floor(Math.random() * this.config.symbols.length),
        isSpinning: false,
      };
      this.reels.push(reel);
    }
  }

  /**
   * Initialize RTP controller
   */
  private initializeRTPController(): void {
    for (let i = 0; i < 100; i++) {
      this.rtpController.set(i, Math.random() * 100);
    }
  }

  /**
   * Perform a spin
   */
  spin(betAmount: number): SpinResult {
    // Validate bet
    if (betAmount < this.config.minBet || betAmount > this.config.maxBet) {
      throw new Error('Invalid bet amount');
    }

    // Generate spin result
    const spinResult = this.generateSpinResult(betAmount);

    // Check for cascading reels
    let cascadeCount = 0;
    if (this.config.cascadingReels) {
      cascadeCount = this.performCascade(spinResult.reels);
    }

    // Calculate win
    const winAmount = this.calculateWin(spinResult.reels, betAmount);

    // Check for bonus trigger
    const bonusTriggered = this.checkBonusCondition(spinResult.reels);
    const freeSpinsAwarded = bonusTriggered && this.config.bonusConfig ? this.config.bonusConfig.spins : 0;

    // Apply multiplier
    const multiplier = this.calculateMultiplier(spinResult.reels);
    const finalWinAmount = winAmount * multiplier;

    const result: SpinResult = {
      reels: spinResult.reels,
      winAmount: finalWinAmount,
      newBalance: 0, // Will be set by wallet integration
      winLines: this.paylineResults.map((_, i) => [i]),
      bonusTriggered,
      freeSpinsAwarded,
      cascadeCount,
      multiplier,
      isWin: finalWinAmount > 0,
    };

    this.emit('spin_complete', result);

    return result;
  }

  /**
   * Generate spin result using RNG
   */
  private generateSpinResult(betAmount: number): { reels: SlotSymbol[][] } {
    const reels: SlotSymbol[][] = [];
    const rtpValue = this.rtpController.get(Math.floor(Math.random() * 100)) || 50;
    const shouldWin = rtpValue < this.config.rtp;

    for (let reelIndex = 0; reelIndex < this.config.reels; reelIndex++) {
      const reel: SlotSymbol[] = [];
      const reel_symbols = this.reels[reelIndex].symbols;

      for (let rowIndex = 0; rowIndex < this.config.rows; rowIndex++) {
        let symbolIndex = Math.floor(Math.random() * reel_symbols.length);

        // Bias towards winning symbols if RTP indicates a win
        if (shouldWin && rowIndex === 1) {
          const highValueSymbols = reel_symbols.filter((s) => s.value > 5);
          if (highValueSymbols.length > 0) {
            symbolIndex = reel_symbols.indexOf(highValueSymbols[Math.floor(Math.random() * highValueSymbols.length)]);
          }
        }

        reel.push(reel_symbols[symbolIndex]);
      }

      reels.push(reel);
    }

    return { reels };
  }

  /**
   * Perform cascading reels
   */
  private performCascade(reels: SlotSymbol[][]): number {
    let cascadeCount = 0;
    let hasMatches = true;

    while (hasMatches) {
      hasMatches = false;
      cascadeCount++;

      // Check for matches
      for (let col = 0; col < reels.length; col++) {
        for (let row = 0; row < reels[col].length - 1; row++) {
          if (reels[col][row].id === reels[col][row + 1].id) {
            // Remove matched symbols
            reels[col].splice(row, 1);
            // Add new symbol at top
            reels[col].unshift(this.config.symbols[Math.floor(Math.random() * this.config.symbols.length)]);
            hasMatches = true;
          }
        }
      }
    }

    return cascadeCount;
  }

  /**
   * Calculate win amount
   */
  private calculateWin(reels: SlotSymbol[][], betAmount: number): number {
    let totalWin = 0;
    this.paylineResults = [];

    // Check each payline
    for (let paylineId = 0; paylineId < this.config.paylines; paylineId++) {
      const paylineSymbols = this.getPaylineSymbols(reels, paylineId);

      // Check for matches
      if (paylineSymbols.length >= 3) {
        const firstSymbol = paylineSymbols[0];
        const isMatch = paylineSymbols.every((s) => s.id === firstSymbol.id || s.isWild);

        if (isMatch) {
          const matchCount = paylineSymbols.filter((s) => s.id === firstSymbol.id || s.isWild).length;
          const symbolValue = firstSymbol.value;
          const paylineWin = symbolValue * matchCount * betAmount;

          this.paylineResults.push({
            paylineId,
            symbols: paylineSymbols,
            winAmount: paylineWin,
            multiplier: matchCount,
          });

          totalWin += paylineWin;
        }
      }
    }

    // Check for cluster pays
    if (this.config.clusterPays) {
      const clusterWin = this.calculateClusterPays(reels, betAmount);
      totalWin += clusterWin;
    }

    return totalWin;
  }

  /**
   * Get symbols on a payline
   */
  private getPaylineSymbols(reels: SlotSymbol[][], paylineId: number): SlotSymbol[] {
    const symbols: SlotSymbol[] = [];
    const paylineRows = this.getPaylineRows(paylineId);

    for (let reelIndex = 0; reelIndex < reels.length; reelIndex++) {
      const rowIndex = paylineRows[reelIndex] || 0;
      if (reels[reelIndex][rowIndex]) {
        symbols.push(reels[reelIndex][rowIndex]);
      }
    }

    return symbols;
  }

  /**
   * Get payline row positions
   */
  private getPaylineRows(paylineId: number): number[] {
    // Standard paylines
    const paylines: Record<number, number[]> = {
      0: [0, 0, 0, 0, 0], // Top line
      1: [1, 1, 1, 1, 1], // Middle line
      2: [2, 2, 2, 2, 2], // Bottom line
      3: [0, 1, 2, 1, 0], // V-shape
      4: [2, 1, 0, 1, 2], // Inverted V
      5: [0, 0, 1, 2, 2], // Diagonal
      6: [2, 2, 1, 0, 0], // Inverted diagonal
      7: [0, 1, 1, 1, 2], // Wave
      8: [2, 1, 1, 1, 0], // Inverted wave
    };

    return paylines[paylineId] || [1, 1, 1, 1, 1];
  }

  /**
   * Calculate cluster pays
   */
  private calculateClusterPays(reels: SlotSymbol[][], betAmount: number): number {
    let totalWin = 0;

    // Group adjacent symbols
    const visited = new Set<string>();

    for (let reelIndex = 0; reelIndex < reels.length; reelIndex++) {
      for (let rowIndex = 0; rowIndex < reels[reelIndex].length; rowIndex++) {
        const key = `${reelIndex}-${rowIndex}`;
        if (!visited.has(key)) {
          const cluster = this.getCluster(reels, reelIndex, rowIndex, visited);
          if (cluster.length >= 5) {
            const symbol = reels[reelIndex][rowIndex];
            const clusterWin = symbol.value * cluster.length * betAmount;
            totalWin += clusterWin;
          }
        }
      }
    }

    return totalWin;
  }

  /**
   * Get cluster of adjacent symbols
   */
  private getCluster(
    reels: SlotSymbol[][],
    reelIndex: number,
    rowIndex: number,
    visited: Set<string>
  ): Array<{ reel: number; row: number }> {
    const cluster: Array<{ reel: number; row: number }> = [];
    const symbol = reels[reelIndex][rowIndex];
    const queue = [[reelIndex, rowIndex]];

    while (queue.length > 0) {
      const [r, c] = queue.shift()!;
      const key = `${r}-${c}`;

      if (visited.has(key) || !reels[r] || !reels[r][c]) {
        continue;
      }

      if (reels[r][c].id === symbol.id) {
        visited.add(key);
        cluster.push({ reel: r, row: c });

        // Check adjacent cells
        queue.push([r - 1, c], [r + 1, c], [r, c - 1], [r, c + 1]);
      }
    }

    return cluster;
  }

  /**
   * Check for bonus condition
   */
  private checkBonusCondition(reels: SlotSymbol[][]): boolean {
    if (!this.config.bonusConfig) {
      return false;
    }

    const bonusSymbols = reels.flat().filter((s) => {
      if (this.config.bonusConfig?.trigger === 'scatter') {
        return s.isScatter;
      } else if (this.config.bonusConfig?.trigger === 'bonus') {
        return s.isBonus;
      } else if (this.config.bonusConfig?.trigger === 'collector') {
        return s.name.includes('coin') || s.name.includes('collector');
      }
      return false;
    });

    return bonusSymbols.length >= 3;
  }

  /**
   * Calculate multiplier
   */
  private calculateMultiplier(reels: SlotSymbol[][]): number {
    let multiplier = 1;

    // Check for expanding symbols
    const expandingSymbols = reels.flat().filter((s) => s.multiplier > 1);
    if (expandingSymbols.length > 0) {
      multiplier = Math.max(...expandingSymbols.map((s) => s.multiplier));
    }

    // Check for bonus multiplier
    if (this.config.bonusConfig?.multiplier) {
      multiplier *= this.config.bonusConfig.multiplier;
    }

    return multiplier;
  }

  /**
   * Perform free spin
   */
  freeSpinRound(freeSpins: number, betAmount: number): SpinResult[] {
    const results: SpinResult[] = [];

    for (let i = 0; i < freeSpins; i++) {
      const result = this.spin(betAmount);
      results.push(result);

      // Check for retrigger
      if (this.config.bonusConfig?.retrigger && result.bonusTriggered) {
        freeSpins += this.config.bonusConfig.spins;
      }
    }

    return results;
  }

  /**
   * Get game configuration
   */
  getConfig(): SlotGameConfig {
    return this.config;
  }

  /**
   * Get RTP
   */
  getRTP(): number {
    return this.config.rtp;
  }

  /**
   * Get volatility
   */
  getVolatility(): string {
    return this.config.volatility;
  }

  /**
   * Get payline results
   */
  getPaylineResults(): PaylineResult[] {
    return this.paylineResults;
  }

  /**
   * Update RTP (admin only)
   */
  updateRTP(newRTP: number): void {
    if (newRTP < 85 || newRTP > 99) {
      throw new Error('RTP must be between 85 and 99');
    }
    this.config.rtp = newRTP;
    this.emit('rtp_updated', newRTP);
  }

  /**
   * Get hit frequency
   */
  calculateHitFrequency(spins: number = 1000): number {
    let wins = 0;

    for (let i = 0; i < spins; i++) {
      const result = this.spin(this.config.minBet);
      if (result.isWin) {
        wins++;
      }
    }

    return (wins / spins) * 100;
  }

  /**
   * Get average win
   */
  calculateAverageWin(spins: number = 1000, betAmount: number = this.config.minBet): number {
    let totalWin = 0;

    for (let i = 0; i < spins; i++) {
      const result = this.spin(betAmount);
      totalWin += result.winAmount;
    }

    return totalWin / spins;
  }
}

export default SlotGameEngine;
