import Foundation
import PokerCore

/// Runs hands between bots. The app will sit a human in one seat; everything else is the same.
public final class BotTable {
    public private(set) var bots: [String: Bot]
    public let models: OpponentModels
    private var rng: SeededRandom
    /// A seat the bots do not control — the human, when there is one.
    public var humanSeat: Int?

    /// `sharedModels` lets several tables accumulate observations of the same players, which is
    /// how a duplicate match keeps one memory across both passes.
    public init(bots: [Bot], seed: UInt64, humanSeat: Int? = nil, sharedModels: OpponentModels? = nil) {
        self.bots = Dictionary(uniqueKeysWithValues: bots.map { ($0.id, $0) })
        self.rng = SeededRandom(seed: seed)
        self.humanSeat = humanSeat
        self.models = sharedModels ?? OpponentModels()
    }

    public func bot(_ id: String) -> Bot? { bots[id] }

    /// Sits a new player down — a cash table's replacement for somebody who busted twice. The
    /// others' reads of everyone else are kept; there is simply one more person to read.
    public func seat(_ bot: Bot) { bots[bot.id] = bot }

    /// Plays a hand to completion using the bots for every seat. Stops early and returns the
    /// engine if it reaches the human's turn.
    @discardableResult
    public func play(_ engine: HandEngine, observe: Bool = true) -> HandEngine {
        var steps = 0
        while step(engine) != nil {
            steps += 1
            precondition(steps < 500, "hand failed to terminate")
        }
        if observe { observeFinished(engine) }
        return engine
    }

    /// Plays exactly one opponent decision and returns what they did, or nil if it is not an
    /// opponent's turn. The app deals them out one at a time so a person can watch the hand
    /// happen; everything else calls `play` and gets the same sequence in one go.
    @discardableResult
    public func step(_ engine: HandEngine) -> PlayerAction? {
        guard !engine.isComplete, let seat = engine.actor, seat != humanSeat,
              let view = engine.view(for: seat) else { return nil }

        let passive: PlayerAction = view.legal.canCheck ? .check : .fold
        let id = engine.seats[seat].playerID
        guard var bot = bots[id] else {
            try? engine.apply(passive)
            return passive
        }
        let action = bot.decide(view, models: models)
        bots[id] = bot                                      // keep the bot's random state moving
        do {
            try engine.apply(action)
            return action
        } catch {
            // The bot cannot produce an illegal action, but if it ever did, the hand must not
            // wedge. Fall back to the most passive legal option.
            try? engine.apply(passive)
            return passive
        }
    }

    /// Lets the opponents update what they think of everyone. Must be called once per finished
    /// hand — twice would count the same hand twice in their reads.
    public func observeFinished(_ engine: HandEngine) {
        guard engine.isComplete else { return }
        models.observe(engine)
    }

    public func nextSeed() -> UInt64 { rng.next() }
}

/// Head-to-head measurement of whether one opponent is genuinely better than another.
///
/// This is the check that the skill dial means something. If a bot at skill 80 does not reliably
/// beat the same style at skill 20, then "the table gets harder" is a label on a number that
/// changes nothing, and every phase built on top of it would inherit the lie.
public enum Shootout {

    public struct Result: Sendable {
        public let hands: Int
        /// Big blinds won per 100 hands by the first bot. Positive means it is winning.
        public let bigBlindsPer100: Double
        public let netChips: Int
    }

    /// Plays a heads-up match, resetting stacks each hand so the result measures skill rather
    /// than who happened to win the first big pot. The button alternates.
    public static func headsUp(_ a: Bot, _ b: Bot, hands: Int, seed: UInt64, startingStack: Int = 10_000,
                               structure: BettingStructure = .noLimit) -> Result {
        let level = BlindLevel(smallBlind: 50, bigBlind: 100)
        var table = BotTable(bots: [a, b], seed: seed)
        var net = 0
        var rng = SeededRandom(seed: seed &* 31 &+ 7)

        for hand in 0..<hands {
            let seats = [
                SeatState(seat: 0, playerID: a.id, stack: startingStack),
                SeatState(seat: 1, playerID: b.id, stack: startingStack),
            ]
            let config = HandConfig(level: level, buttonSeat: hand % 2, seed: rng.next(), structure: structure)
            let engine = HandEngine(seats: seats, config: config)
            table.play(engine)
            net += engine.result?.net[0] ?? 0
        }

        let per100 = Double(net) / Double(level.bigBlind) / Double(hands) * 100
        return Result(hands: hands, bigBlindsPer100: per100, netChips: net)
    }

    /// Plays every hand twice, with the two bots swapping seats and the deck unchanged.
    ///
    /// Poker results are mostly noise. Over four thousand ordinary hands the swing from cards
    /// alone is larger than any real skill edge below about ten big blinds per hundred, so a
    /// straight match cannot tell "slightly better" from "got the aces". Dealing each hand to
    /// both players cancels the cards out: whatever luck one seat got, the other got it too, and
    /// what is left is the difference in how they played it. This is the same trick duplicate
    /// bridge uses, and it makes a small edge measurable in a fraction of the hands.
    public static func duplicate(_ a: Bot, _ b: Bot, hands: Int, seed: UInt64, startingStack: Int = 10_000,
                                 settlement: Settlement = .allInEquity(trials: 400),
                                 structure: BettingStructure = .noLimit) -> Result {
        let level = BlindLevel(smallBlind: 50, bigBlind: 100)
        var net = 0
        var rng = SeededRandom(seed: seed &* 31 &+ 7)
        let models = OpponentModels()

        for hand in 0..<hands {
            let deckSeed = rng.next()
            let seatSeeds = [rng.next(), rng.next()]
            let button = hand % 2

            // The random seed follows the *seat*, not the player, and is reset for every hand.
            // That makes the second pass an exact mirror of the first: whoever sits in seat one
            // holds the same cards and draws the same coin flips as the player who sat there
            // before. Two identical bots then measure at precisely zero, which is the only way
            // to know the harness is reporting skill rather than luck.
            func seat(_ index: Int, _ bot: Bot) -> Bot {
                Bot(copying: bot, seed: seatSeeds[index])
            }

            // Pass one: A in seat 0.
            let first = HandEngine(
                seats: [SeatState(seat: 0, playerID: a.id, stack: startingStack),
                        SeatState(seat: 1, playerID: b.id, stack: startingStack)],
                config: HandConfig(level: level, buttonSeat: button, seed: deckSeed, settlement: settlement, structure: structure)
            )
            BotTable(bots: [seat(0, a), seat(1, b)], seed: deckSeed, sharedModels: models).play(first)
            net += first.result?.net[0] ?? 0

            // Pass two: same cards, same seat randomness, seats swapped.
            let second = HandEngine(
                seats: [SeatState(seat: 0, playerID: b.id, stack: startingStack),
                        SeatState(seat: 1, playerID: a.id, stack: startingStack)],
                config: HandConfig(level: level, buttonSeat: button, seed: deckSeed, settlement: settlement, structure: structure)
            )
            BotTable(bots: [seat(0, b), seat(1, a)], seed: deckSeed, sharedModels: models).play(second)
            net += second.result?.net[1] ?? 0
        }

        let played = hands * 2
        let per100 = Double(net) / Double(level.bigBlind) / Double(played) * 100
        return Result(hands: played, bigBlindsPer100: per100, netChips: net)
    }

    /// Seats one bot against a table of another, rotating it through every seat on the same deal.
    ///
    /// The same idea as the heads-up duplicate, extended to a full table: each hand is played once
    /// per seat, so the measured player holds every set of cards exactly once. Position and card
    /// luck both wash out and what is left is how the hands were played.
    public static func ring(_ hero: Bot, versus villain: Personality, villainSkill: Int, seats: Int,
                            hands: Int, seed: UInt64, startingStack: Int = 10_000,
                            settlement: Settlement = .allInEquity(trials: 400),
                            structure: BettingStructure = .noLimit) -> Result {
        let level = BlindLevel(smallBlind: 50, bigBlind: 100)
        let models = OpponentModels()
        var net = 0
        var rng = SeededRandom(seed: seed &* 17 &+ 3)

        for hand in 0..<hands {
            let deckSeed = rng.next()
            let seatSeeds = (0..<seats).map { _ in rng.next() }
            let button = hand % seats

            for heroSeat in 0..<seats {
                var lineup: [Bot] = []
                var ids: [String] = []
                var villainIndex = 0
                for index in 0..<seats {
                    if index == heroSeat {
                        lineup.append(Bot(copying: hero, seed: seatSeeds[index]))
                        ids.append(hero.id)
                    } else {
                        let id = "V\(villainIndex)"
                        villainIndex += 1
                        lineup.append(Bot(id: id, personality: villain, skill: villainSkill, seed: seatSeeds[index]))
                        ids.append(id)
                    }
                }
                let states = ids.enumerated().map { SeatState(seat: $0.offset, playerID: $0.element, stack: startingStack) }
                let engine = HandEngine(
                    seats: states,
                    config: HandConfig(level: level, buttonSeat: button, seed: deckSeed, settlement: settlement, structure: structure)
                )
                BotTable(bots: lineup, seed: deckSeed, sharedModels: models).play(engine)
                net += engine.result?.net[heroSeat] ?? 0
            }
        }

        let played = hands * seats
        let per100 = Double(net) / Double(level.bigBlind) / Double(played) * 100
        return Result(hands: played, bigBlindsPer100: per100, netChips: net)
    }
}
