//
//  GameState.swift
//  Baccarat
//
//  Observable game state managing the flow of a baccarat game.
//

import Foundation
import SwiftUI
import CasinoKit

/// The current phase of a baccarat round.
enum GamePhase: Equatable {
    case betting
    case dealingInitial
    case playerThirdCard
    case bankerThirdCard
    case showingResult
    case roundComplete
}

/// Result of an individual bet after a round.
struct BetResult: Identifiable {
    let id = UUID()
    let type: BetType
    let amount: Int
    let payout: Int  // Net winnings (positive) or loss (negative)
    
    var isWin: Bool { payout > 0 }
    var isLoss: Bool { payout < 0 }
    var isPush: Bool { payout == 0 }
    
    /// Display name for the bet type
    var displayName: String {
        switch type {
        case .player: return "Player"
        case .banker: return "Banker"
        case .tie: return "Tie"
        case .playerPair: return "P Pair"
        case .bankerPair: return "B Pair"
        case .dragonBonusPlayer: return "Dragon P"
        case .dragonBonusBanker: return "Dragon B"
        }
    }
}

/// Main observable game state class managing all game logic and UI state.
@Observable
@MainActor
final class GameState {
    // MARK: - Settings
    let settings: GameSettings
    
    // MARK: - Sound
    private let sound = SoundManager.shared
    
    // MARK: - Persistence
    private var persistence: CloudSyncManager<BaccaratGameData>!
    
    // MARK: - Game Engine
    private(set) var engine: BaccaratEngine
    
    // MARK: - Player State
    var balance: Int = 10_000
    var currentBets: [Bet] = []
    
    // MARK: - Round State
    var currentPhase: GamePhase = .betting
    var lastResult: GameResult?
    var lastWinnings: Int = 0
    
    // MARK: - Bet Results
    var playerHadPair: Bool = false
    var bankerHadPair: Bool = false
    var betResults: [BetResult] = []
    
    // MARK: - Card Display State (for animations)
    var visiblePlayerCards: [Card] = []
    var visibleBankerCards: [Card] = []
    var playerCardsFaceUp: [Bool] = []
    var bankerCardsFaceUp: [Bool] = []
    
    // MARK: - History
    var roundHistory: [RoundResult] = []
    
    // MARK: - Animation Flags
    var isAnimating: Bool = false
    var showResultBanner: Bool = false
    
    // MARK: - Computed Properties
    
    var totalBetAmount: Int {
        currentBets.reduce(0) { $0 + $1.amount }
    }
    
    var canPlaceBet: Bool {
        currentPhase == .betting && !isAnimating
    }
    
    var canDeal: Bool {
        currentPhase == .betting && hasMainBet && mainBetMeetsMinimum && !isAnimating
    }
    
    var playerHandValue: Int {
        engine.playerHand.value
    }
    
    var bankerHandValue: Int {
        engine.bankerHand.value
    }
    
    // Recent results for the road display (last 20)
    var recentResults: [RoundResult] {
        Array(roundHistory.suffix(20))
    }
    
    // MARK: - Animation Timing (based on settings)
    
    private var dealDelay: Duration {
        .milliseconds(Int(400 * settings.dealingSpeed))
    }
    
    private var flipDelay: Duration {
        .milliseconds(Int(300 * settings.dealingSpeed))
    }
    
    private var shortDelay: Duration {
        .milliseconds(Int(200 * settings.dealingSpeed))
    }
    
    private var resultDelay: Duration {
        .milliseconds(Int(500 * settings.dealingSpeed))
    }
    
    // MARK: - Initialization
    
    /// Convenience initializer that constructs default settings on the main actor.
    convenience init() {
        self.init(settings: GameSettings())
    }

    init(settings: GameSettings) {
        self.settings = settings
        self.engine = BaccaratEngine(deckCount: settings.deckCount.rawValue)
        self.balance = settings.startingBalance
        
        // Sync sound settings with SoundManager
        syncSoundSettings()
        
        // Initialize persistence with cloud data callback
        self.persistence = CloudSyncManager<BaccaratGameData>()
        persistence.onCloudDataReceived = { [weak self] cloudData in
            self?.handleCloudDataReceived(cloudData)
        }
        
        // Load saved game data
        loadSavedGame()
    }
    
    /// Handles data received from iCloud (e.g., after fresh install or from another device).
    private func handleCloudDataReceived(_ cloudData: BaccaratGameData) {
        
        // Only update if cloud has more progress than current state
        guard cloudData.roundsPlayed > roundHistory.count else {
            return
        }
        
        // Restore balance
        self.balance = cloudData.balance
        
        // Restore round history
        self.roundHistory = cloudData.roundHistory.compactMap { saved in
            guard let result = GameResult(persistenceKey: saved.result) else { return nil }
            return RoundResult(
                result: result,
                playerValue: saved.playerValue,
                bankerValue: saved.bankerValue,
                playerPair: saved.playerPair,
                bankerPair: saved.bankerPair
            )
        }
    }
    
    // MARK: - Persistence
    
    /// Loads saved game data from iCloud/local storage.
    private func loadSavedGame() {
        let savedData = persistence.data
        
        // Only restore if there's saved progress
        guard savedData.roundsPlayed > 0 else { return }
        
        // Restore balance
        self.balance = savedData.balance
        
        // Restore round history (convert saved to RoundResult)
        self.roundHistory = savedData.roundHistory.compactMap { saved in
            guard let result = GameResult(persistenceKey: saved.result) else { return nil }
            return RoundResult(
                result: result,
                playerValue: saved.playerValue,
                bankerValue: saved.bankerValue,
                playerPair: saved.playerPair,
                bankerPair: saved.bankerPair
            )
        }
    }
    
    /// Saves current game state to iCloud/local storage.
    private func saveGame(netWinnings: Int = 0) {
        var data = persistence.data
        
        // Update balance
        data.balance = balance
        
        // Update statistics
        data.totalWinnings += netWinnings
        if netWinnings > data.biggestWin {
            data.biggestWin = netWinnings
        }
        if netWinnings < 0 && abs(netWinnings) > data.biggestLoss {
            data.biggestLoss = abs(netWinnings)
        }
        
        // Update round history from current session
        data.roundHistory = roundHistory.enumerated().map { index, round in
            // Try to get existing saved result for net winnings
            if index < data.roundHistory.count {
                return data.roundHistory[index]
            }
            // New round - calculate net winnings from betResults if available
            let netForRound = betResults.reduce(0) { $0 + $1.payout }
            return SavedRoundResult(from: round, netWinnings: index == roundHistory.count - 1 ? netWinnings : netForRound)
        }
        
        persistence.save(data)
    }
    
    /// Whether iCloud sync is available.
    var iCloudAvailable: Bool {
        persistence.iCloudAvailable
    }
    
    /// Whether iCloud sync is enabled.
    var iCloudEnabled: Bool {
        get { persistence.iCloudEnabled }
        set { persistence.iCloudEnabled = newValue }
    }
    
    /// Last sync date.
    var lastSyncDate: Date? {
        persistence.lastSyncDate
    }
    
    /// Forces a sync with iCloud.
    func syncWithCloud() {
        persistence.sync()
    }
    
    /// Syncs sound settings from GameSettings to SoundManager.
    private func syncSoundSettings() {
        sound.soundEnabled = settings.soundEnabled
        sound.hapticsEnabled = settings.hapticsEnabled
        sound.volume = settings.soundVolume
    }
    
    // MARK: - Computed Properties for Bets
    
    /// Returns the current main bet (Player or Banker), if any.
    var mainBet: Bet? {
        currentBets.first(where: { $0.type == .player || $0.type == .banker })
    }
    
    /// Returns the current tie bet, if any.
    var tieBet: Bet? {
        currentBets.first(where: { $0.type == .tie })
    }
    
    /// Returns bets for a specific type.
    func bet(for type: BetType) -> Bet? {
        currentBets.first(where: { $0.type == type })
    }
    
    /// Whether the player has placed a main bet (required to deal).
    var hasMainBet: Bool {
        mainBet != nil
    }
    
    /// Whether the player has any side bets.
    var hasSideBets: Bool {
        currentBets.contains(where: { $0.type.isSideBet })
    }
    
    /// Minimum bet for the table.
    var minBet: Int {
        settings.minBet
    }
    
    /// Maximum bet for the table.
    var maxBet: Int {
        settings.maxBet
    }
    
    /// Returns the current bet amount for a specific bet type.
    func betAmount(for type: BetType) -> Int {
        currentBets.first(where: { $0.type == type })?.amount ?? 0
    }
    
    /// Whether the main bet meets the minimum requirement.
    var mainBetMeetsMinimum: Bool {
        guard let bet = mainBet else { return false }
        return bet.amount >= minBet
    }
    
    /// Whether the main bet exists but is below the minimum required.
    var isMainBetBelowMinimum: Bool {
        guard let bet = mainBet else { return false }
        return bet.amount < minBet
    }
    
    /// Amount needed to reach the minimum bet.
    var amountNeededForMinimum: Int {
        guard let bet = mainBet else { return minBet }
        return max(0, minBet - bet.amount)
    }
    
    /// Whether a bet type can accept more chips (hasn't hit max).
    func canAddToBet(type: BetType, amount: Int) -> Bool {
        let currentAmount = betAmount(for: type)
        return currentAmount + amount <= maxBet
    }
    
    // MARK: - Betting Actions
    
    /// Places a bet of the specified amount on the given bet type.
    /// Player and Banker are mutually exclusive. Side bets can be added independently.
    /// Enforces min/max table limits.
    func placeBet(type: BetType, amount: Int) {
        guard canPlaceBet, balance >= amount else { return }
        
        // Check if adding this bet would exceed max
        let currentAmount = betAmount(for: type)
        guard currentAmount + amount <= maxBet else { return }
        
        // Handle mutually exclusive Player/Banker bets
        if type == .player || type == .banker {
            // Remove any existing opposite main bet and refund it
            if let existingMainBet = mainBet, existingMainBet.type != type {
                balance += existingMainBet.amount
                currentBets.removeAll(where: { $0.type == existingMainBet.type })
            }
        }
        
        // Check if there's already a bet of this type
        if let index = currentBets.firstIndex(where: { $0.type == type }) {
            // Add to existing bet
            let existingBet = currentBets[index]
            currentBets[index] = Bet(type: type, amount: existingBet.amount + amount)
        } else {
            currentBets.append(Bet(type: type, amount: amount))
        }
        
        balance -= amount
        
        // Play chip placement sound and haptic
        sound.playChipPlace()
    }
    
    /// Clears all current bets and returns the amounts to balance.
    func clearBets() {
        guard canPlaceBet, !currentBets.isEmpty else { return }
        balance += totalBetAmount
        currentBets = []
        
        // Play clear bets sound and haptic
        sound.playClearBets()
    }
    
    /// Undoes the last bet placed.
    func undoLastBet() {
        guard canPlaceBet, let lastBet = currentBets.last else { return }
        balance += lastBet.amount
        currentBets.removeLast()
    }
    
    // MARK: - Game Flow
    
    /// Starts a new round by dealing cards with animation.
    func deal() async {
        guard canDeal else { return }
        
        isAnimating = true
        engine.prepareNewRound()
        
        // Clear visible cards and bet results
        visiblePlayerCards = []
        visibleBankerCards = []
        playerCardsFaceUp = []
        bankerCardsFaceUp = []
        lastResult = nil
        showResultBanner = false
        playerHadPair = false
        bankerHadPair = false
        betResults = []
        
        // Deal initial cards
        currentPhase = .dealingInitial
        let initialCards = engine.dealInitialCards()
        
        // Check if animations are enabled
        if settings.showAnimations {
            // Animate dealing: P1, B1, P2, B2
            for (index, card) in initialCards.enumerated() {
                try? await Task.sleep(for: dealDelay)
                
                // Play card deal sound
                sound.playCardDeal()
                
                if index % 2 == 0 {
                    visiblePlayerCards.append(card)
                    playerCardsFaceUp.append(false)
                } else {
                    visibleBankerCards.append(card)
                    bankerCardsFaceUp.append(false)
                }
            }
            
            // Brief pause then flip cards
            try? await Task.sleep(for: flipDelay)
            
            // Play card flip sound
            sound.playCardFlip()
            
            // Flip all cards face up
            for i in 0..<playerCardsFaceUp.count {
                playerCardsFaceUp[i] = true
            }
            for i in 0..<bankerCardsFaceUp.count {
                bankerCardsFaceUp[i] = true
            }
            
            try? await Task.sleep(for: resultDelay)
        } else {
            // No animations - show all cards immediately
            for (index, card) in initialCards.enumerated() {
                if index % 2 == 0 {
                    visiblePlayerCards.append(card)
                    playerCardsFaceUp.append(true)
                } else {
                    visibleBankerCards.append(card)
                    bankerCardsFaceUp.append(true)
                }
            }
        }
        
        // Check for naturals
        if engine.playerHand.isNatural || engine.bankerHand.isNatural {
            await showResult()
            return
        }
        
        // Player third card
        currentPhase = .playerThirdCard
        if let playerThird = engine.drawPlayerThirdCard() {
            if settings.showAnimations {
                try? await Task.sleep(for: dealDelay)
                sound.playCardDeal()
                visiblePlayerCards.append(playerThird)
                playerCardsFaceUp.append(false)
                try? await Task.sleep(for: shortDelay)
                sound.playCardFlip()
                playerCardsFaceUp[2] = true
                try? await Task.sleep(for: flipDelay)
            } else {
                visiblePlayerCards.append(playerThird)
                playerCardsFaceUp.append(true)
            }
        }
        
        // Banker third card
        currentPhase = .bankerThirdCard
        if let bankerThird = engine.drawBankerThirdCard() {
            if settings.showAnimations {
                try? await Task.sleep(for: dealDelay)
                sound.playCardDeal()
                visibleBankerCards.append(bankerThird)
                bankerCardsFaceUp.append(false)
                try? await Task.sleep(for: shortDelay)
                sound.playCardFlip()
                bankerCardsFaceUp[2] = true
                try? await Task.sleep(for: dealDelay)
            } else {
                visibleBankerCards.append(bankerThird)
                bankerCardsFaceUp.append(true)
            }
        }
        
        await showResult()
    }
    
    /// Shows the result and processes payouts.
    private func showResult() async {
        currentPhase = .showingResult
        
        let result = engine.determineResult()
        lastResult = result
        
        // Record pair results for display
        playerHadPair = engine.playerHasPair
        bankerHadPair = engine.bankerHasPair
        
        // Calculate and apply payouts, track individual results
        var totalWinnings = 0
        var results: [BetResult] = []
        
        for bet in currentBets {
            let payout = engine.calculatePayout(bet: bet, result: result)
            totalWinnings += payout
            
            // Track individual bet result
            results.append(BetResult(type: bet.type, amount: bet.amount, payout: payout))
            
            // Return original bet if not a loss
            if payout >= 0 {
                balance += bet.amount
            }
            
            // Add winnings
            if payout > 0 {
                balance += payout
            }
        }
        
        betResults = results
        lastWinnings = totalWinnings
        
        // Play result sound based on MAIN BET outcome (not total winnings)
        // This way winning the main hand plays win sound even if side bets lost
        let mainBetResult = results.first(where: { $0.type == .player || $0.type == .banker })
        
        if let mainResult = mainBetResult {
            if mainResult.isWin {
                // Main bet won - play win sound
                let maxBetAmount = currentBets.map(\.amount).max() ?? 0
                let isBigWin = totalWinnings >= maxBetAmount * 5 || totalWinnings >= 500
                sound.playWin(isBigWin: isBigWin && totalWinnings > 0)
            } else if mainResult.isPush {
                // Main bet pushed (tie)
                sound.playPush()
            } else {
                // Main bet lost
                sound.playLose()
            }
        } else {
            // No main bet (only side bets) - use total winnings
            if totalWinnings > 0 {
                sound.playWin(isBigWin: false)
            } else if totalWinnings < 0 {
                sound.playLose()
            } else {
                sound.playPush()
            }
        }
        
        // Record result in history
        roundHistory.append(RoundResult(
            result: result,
            playerValue: playerHandValue,
            bankerValue: bankerHandValue,
            playerPair: playerHadPair,
            bankerPair: bankerHadPair
        ))
        
        // Save game state to iCloud/local
        saveGame(netWinnings: totalWinnings)
        
        // Show result banner - stays until user taps New Round
        showResultBanner = true
        currentPhase = .roundComplete
        isAnimating = false
    }
    
    /// Prepares for a new round.
    func newRound() {
        guard currentPhase == .roundComplete else { return }
        
        // Play new round sound
        sound.playNewRound()
        
        // Dismiss result banner
        showResultBanner = false
        
        currentBets = []
        visiblePlayerCards = []
        visibleBankerCards = []
        playerCardsFaceUp = []
        bankerCardsFaceUp = []
        lastResult = nil
        lastWinnings = 0
        playerHadPair = false
        bankerHadPair = false
        betResults = []
        currentPhase = .betting
    }
    
    /// Rebet the same amounts as last round.
    func rebet() {
        guard currentPhase == .roundComplete || (currentPhase == .betting && currentBets.isEmpty) else { return }
        
        if currentPhase == .roundComplete {
            newRound()
        }
    }
    
    /// Resets the game to initial state with current settings.
    func resetGame() {
        engine = BaccaratEngine(deckCount: settings.deckCount.rawValue)
        balance = settings.startingBalance
        currentBets = []
        currentPhase = .betting
        lastResult = nil
        lastWinnings = 0
        visiblePlayerCards = []
        visibleBankerCards = []
        playerCardsFaceUp = []
        bankerCardsFaceUp = []
        roundHistory = []
        isAnimating = false
        showResultBanner = false
        playerHadPair = false
        bankerHadPair = false
        betResults = []
        
        // Save the reset state (keeps lifetime stats, resets balance and session history)
        saveGame()
        
        // Play new game sound
        sound.playNewRound()
    }
    
    /// Completely clears all saved data and starts fresh (including lifetime stats).
    func clearAllData() {
        persistence.reset()
        resetGame()
    }
    
    /// Returns lifetime statistics from saved data.
    var lifetimeStats: BaccaratGameData {
        persistence.data
    }
    
    /// Applies new settings (call after settings change).
    func applySettings() {
        resetGame()
    }
}
