import Foundation
import PokerCore
import PokerAI

/// How busy the table is made in front of the player for one hand.
///
/// The eight opponents are the same eight all the way through a sitting; what changes is where
/// they sit. Putting the people who raise constantly in the seats that speak before you is how a
/// hand gets steered towards "somebody has raised, now what" without touching a single card.
public enum TableLean: String, Sendable, Equatable, Codable, CaseIterable {
    /// The quietest players speak before the hero, so the pot is usually still unopened.
    case quiet
    case mixed
    /// The busiest players speak before the hero, so there is usually a raise to answer.
    case busy

    /// How it reads in a sentence about the hand about to be dealt.
    public var phrase: String {
        switch self {
        case .quiet: return "with the quietest players speaking before you"
        case .mixed: return "with a mix of players either side of you"
        case .busy: return "with the busiest players speaking before you"
        }
    }
}

/// One hand of an assessment, set up on purpose.
///
/// Three things are chosen and nothing else is: which seat the player sits in, how many big blinds
/// everybody has in front of them, and which opponent sits where. The cards come off a fresh
/// shuffle every hand, exactly as they do at a normal table, and that is the whole reason the
/// resulting numbers are worth anything — see `AssessmentReport.fairnessNote`.
public struct AssessmentSetup: Sendable, Equatable, Identifiable {
    public let id: String
    /// Where the player sits. Chosen, not dealt.
    public let seat: Position
    /// What everyone at the table has in front of them, in big blinds.
    public let depthInBB: Int
    public let lean: TableLean
    /// What this hand is for, in the player's own words.
    public let watching: String
    /// Roughly how often a hand set up this way lands the player in each kind of situation.
    ///
    /// A prior, not a promise. The cards still decide, which is exactly why these are chances
    /// rather than certainties, and why the planner treats the whole thing as "what would I expect
    /// to learn" rather than "what will happen".
    public let expected: [SpotKind: Double]

    public init(id: String, seat: Position, depthInBB: Int, lean: TableLean, watching: String,
                expected: [SpotKind: Double]) {
        self.id = id
        self.seat = seat
        self.depthInBB = depthInBB
        self.lean = lean
        self.watching = watching
        self.expected = expected
    }

    /// "You are in seat 2 of 9 (the big blind) with 100 big blinds in front of everybody, with the
    /// busiest players speaking before you." The seat leads with its number, as it does everywhere
    /// else, and the stack is said in words a beginner already has — "deep" is a word you have to
    /// be taught.
    public func introduction(atTableOf count: Int) -> String {
        "You are in \(seat.described(inTableOf: count)) with \(depthInBB) big blinds in front of everybody, \(lean.phrase)."
    }

    /// The short form, for a list of what the sitting steered.
    public func summary(atTableOf count: Int) -> String {
        "\(seat.seatPhrase(inTableOf: count)), \(depthInBB) big blinds in front of everybody, \(lean.phrase)"
    }
}

/// How much of the player's game has actually been seen, situation by situation.
///
/// The measure is the standard error of a proportion, not a count, because the question is not
/// "how many hands" but "how far off could this number still be". A player who has folded five
/// out of five is already better understood than one who has folded three out of six, and the
/// planner should stop spending hands on the first and keep spending them on the second.
public struct AssessmentCoverage: Sendable, Equatable {
    public private(set) var chances: [SpotKind: Int] = [:]
    public private(set) var mistakes: [SpotKind: Int] = [:]
    public private(set) var decisions = 0

    public init() {}

    public mutating func saw(_ spot: SpotKind, mistake: Bool) {
        decisions += 1
        chances[spot, default: 0] += 1
        if mistake { mistakes[spot, default: 0] += 1 }
    }

    public func chances(in spot: SpotKind) -> Int { chances[spot] ?? 0 }
    public func mistakes(in spot: SpotKind) -> Int { mistakes[spot] ?? 0 }

    /// Two imaginary chances, one that went each way, added to every count.
    ///
    /// Without them a spot seen twice and got wrong twice reads as a hundred per cent with no
    /// uncertainty at all, which is how a coach ends up telling somebody they *always* do
    /// something on the strength of two hands.
    public func rate(in spot: SpotKind) -> Double {
        Double(mistakes(in: spot) + 1) / Double(chances(in: spot) + 2)
    }

    /// How far the measured rate could still be from the real one.
    public func standardError(in spot: SpotKind) -> Double {
        let p = rate(in: spot)
        return (p * (1 - p) / Double(chances(in: spot) + 2)).squareRoot()
    }

    /// The standard error of a spot nobody has been watched in at all — the worst it ever gets,
    /// and therefore what everything else is measured against.
    public static let blindStandardError = (0.25 / 2.0).squareRoot()

    /// Nought where nothing has been seen, approaching one as the measurement firms up.
    public func certainty(in spot: SpotKind) -> Double {
        (1 - standardError(in: spot) / Self.blindStandardError).clamped(to: 0...1)
    }

    /// The gentlest error rate a steady player is allowed in this situation. Going past any one of
    /// the habits that live there is worth knowing about, so the lowest of them is the line.
    public static func acceptableRate(in spot: SpotKind) -> Double {
        LeakCatalogue.all.filter { $0.spot == spot }.map(\.acceptableRate).min() ?? 0.2
    }

    /// How much this situation looks like it might be hiding something, nought to one and a half.
    ///
    /// Certainty on its own spreads the hands evenly across the eleven situations, and evenly is
    /// the wrong answer: forty hands cannot measure eleven things properly, so the parts of the
    /// game that come back clean early should be left alone and the hands spent where the answer
    /// is still in doubt. The reason is arithmetic rather than curiosity: another ten hands in a
    /// spot already sitting above what a steady player manages can change what the report is
    /// allowed to say, and another ten in a spot that is plainly fine cannot.
    public func doubt(in spot: SpotKind) -> Double {
        let seen = chances(in: spot)
        guard seen > 0 else { return 0 }
        let acceptable = Self.acceptableRate(in: spot)
        let excess = (rate(in: spot) - acceptable) / max(0.05, acceptable)
        // Damped while the count is tiny, so one bad hand does not send the whole sitting chasing
        // a spot on the strength of it.
        return excess.clamped(to: 0...1.5) * Double(seen) / Double(seen + 3)
    }

    /// Below this many chances a report is not allowed to say anything about a situation at all,
    /// however lopsided the few it has look. Matches the floor the report itself applies.
    public static let readableChances = 6

    /// What another hand here would be worth: how little is known, weighted up where what little
    /// is known looks wrong.
    ///
    /// The floor is the important part. A situation seen four times has a respectable-looking
    /// standard error and is still worth nothing, because nothing can be said off four chances —
    /// so until there are enough of them, another hand there is worth exactly as much as a hand in
    /// a situation nobody has ever been in. Without that floor the planner walked away from
    /// half-measured spots to go and half-measure another one, and finished forty hands with
    /// eleven situations it could not speak about.
    public func need(in spot: SpotKind) -> Double {
        let known = chances(in: spot) >= Self.readableChances ? certainty(in: spot) : 0
        return (1 - known) * (1 + doubt(in: spot))
    }

    /// What is still least understood, worst first. Ties are broken by the catalogue's own order
    /// so the same coverage always produces the same answer.
    public var leastMeasured: [SpotKind] {
        SpotKind.measured.enumerated()
            .sorted { a, b in
                let (x, y) = (certainty(in: a.element), certainty(in: b.element))
                return x != y ? x < y : a.offset < b.offset
            }
            .map(\.element)
    }
}

/// What each way of setting the table up has actually turned out to produce.
///
/// `AssessmentSetup.expected` is a belief, and beliefs about poker are wrong all the time. A setup
/// can promise river decisions and then never get anybody as far as the river, because the player
/// keeps folding before the flop — and a planner that took the promise at face value would deal
/// that same hand forty times waiting for a river that was never coming. That is not a made-up
/// worry: it is exactly what the first version of this did, forty hands of one setup, and it is
/// why what actually happened outvotes what was expected as soon as there is anything to go on.
public struct SetupYield: Sendable, Equatable {
    private var handsDealt: [String: Int] = [:]
    private var produced: [String: [SpotKind: Int]] = [:]

    /// How many hands of a setup it takes before what happened counts for more than what was
    /// expected. Three: enough that one strange hand does not rewrite the belief, few enough that
    /// a belief which is simply wrong is abandoned inside the first few hands.
    public static let priorWeight = 3.0

    public init() {}

    /// One finished hand: which setup it used, and which situations it actually produced. Repeats
    /// within a hand are ignored — the question is how often a hand like this gets you there at
    /// all, not how many decisions it hands you once you are.
    public mutating func saw(_ setup: String, spots: [SpotKind]) {
        handsDealt[setup, default: 0] += 1
        for spot in Set(spots) { produced[setup, default: [:]][spot, default: 0] += 1 }
    }

    public func hands(of setup: String) -> Int { handsDealt[setup] ?? 0 }

    /// How often a hand set up this way lands the player in this situation, as best as can be
    /// told: what was expected, then what has been seen, with the second taking over.
    public func expected(_ spot: SpotKind, of setup: AssessmentSetup) -> Double {
        let dealt = Double(hands(of: setup.id))
        let seen = Double(produced[setup.id]?[spot] ?? 0)
        let prior = setup.expected[spot] ?? 0
        return (prior * Self.priorWeight + seen) / (Self.priorWeight + dealt)
    }
}

/// Chooses how to set the table up for the next hand.
///
/// It is greedy and it is honest about being greedy: for each way of setting the table up, add up
/// how likely that setup is to produce each kind of decision, weighted by how little is known
/// about that kind of decision, and take the best total. There is no cleverer search here because
/// there is nothing to search — forty hands, seven setups, and every hand changes the answer.
public enum AssessmentPlanner {

    /// The ways a hand can be set up, in the edition the format selects. Between them they reach
    /// every situation the coach measures in that game.
    public static var setups: [AssessmentSetup] { setups(for: LeakCatalogue.format) }

    public static func setups(for format: CoachFormat) -> [AssessmentSetup] {
        switch format {
        case .tournament: return tournamentSetups
        case .cash: return cashSetups
        case .limit: return limitSetups
        }
    }

    /// The fixed-limit table's arrangements: the cash ones minus the two-hundred-blind seat — a
    /// limit pot never asks anybody for a whole stack, so depth is not a thing to arrange — with
    /// priors measured at the limit table (`pokerlab assess --format cash --structure fixedLimit
    /// --setup <id> --hands 200 --player control`, 2026-09-12). A limit table reaches the flop
    /// less often from the early seats than a no-limit one, because a raise is called rather
    /// than folded to and the raiser is then answering bets; the turn comes up more.
    public static let limitSetups: [AssessmentSetup] = cashSetups.filter { $0.id != "deep" }.map { setup in
        guard let measured = limitYields[setup.id] else { return setup }
        return AssessmentSetup(id: setup.id, seat: setup.seat, depthInBB: setup.depthInBB, lean: setup.lean,
                               watching: setup.watching, expected: measured)
    }

    static let limitYields: [String: [SpotKind: Double]] = [
        "early-seat": [.openingChance: 1.00, .facingReraise: 0.06, .continuationBet: 0.09,
                       .facingContinuationBet: 0.04, .flopDecision: 0.04, .turnDecision: 0.18,
                       .riverDecision: 0.12, .facingRiverBet: 0.03],
        "last-seats": [.openingChance: 0.20, .facingRaise: 0.75, .facingReraise: 0.12,
                       .continuationBet: 0.02, .facingContinuationBet: 0.07, .flopDecision: 0.07,
                       .turnDecision: 0.13, .riverDecision: 0.07, .facingRiverBet: 0.07],
        "big-blind": [.openingChance: 0.09, .facingReraise: 0.37, .bigBlindDefence: 0.56,
                      .continuationBet: 0.04, .facingContinuationBet: 0.23, .flopDecision: 0.27,
                      .turnDecision: 0.49, .riverDecision: 0.22, .facingRiverBet: 0.17,
                      .freeCardChance: 0.01],
        "facing-heat": [.openingChance: 0.50, .facingRaise: 0.46, .facingReraise: 0.09,
                        .continuationBet: 0.07, .facingContinuationBet: 0.07, .flopDecision: 0.03,
                        .turnDecision: 0.14, .riverDecision: 0.09, .facingRiverBet: 0.07],
        "deep-and-sticky": [.openingChance: 0.34, .facingRaise: 0.65, .facingReraise: 0.09,
                            .continuationBet: 0.04, .facingContinuationBet: 0.08, .flopDecision: 0.04,
                            .turnDecision: 0.16, .riverDecision: 0.08, .facingRiverBet: 0.09,
                            .freeCardChance: 0.01],
        "bet-at-you": [.openingChance: 0.06, .facingRaise: 0.64, .facingReraise: 0.35,
                       .continuationBet: 0.04, .facingContinuationBet: 0.04, .flopDecision: 0.09,
                       .turnDecision: 0.17, .riverDecision: 0.10, .facingRiverBet: 0.04],
    ]

    /// The cash table's arrangements: the tournament ones minus the short stack — nobody at a cash
    /// table is eleven big blinds deep unless they chose to be — and a deep one in its place,
    /// because two hundred big blinds is where the hundred-blind mistakes get expensive.
    public static let cashSetups: [AssessmentSetup] =
        tournamentSetups.filter { $0.id != "short-stack" } + [
            AssessmentSetup(
                id: "deep", seat: .cutoff, depthInBB: 200, lean: .quiet,
                watching: "What you do with two hundred big blinds in front of everybody: whether "
                    + "one pair tempts you into a big pot, and whether you call for the hands that "
                    + "the money behind pays for.",
                // Measured: `pokerlab assess --format cash --setup deep --hands 200 --player
                // control`, 2026-09-11. Two hundred big blinds in the cutoff at a quiet table is
                // mostly a preflop seat — a raise to answer in half the hands — and reaches the
                // flop in about one hand in six. The stack-off spot is rare with a sound player in
                // the chair (none in 200 hands; one in 200 from the last seat), and the prior is
                // kept small rather than zero so the planner knows the arrangement can reach it.
                expected: [.openingChance: 0.43, .facingRaise: 0.54, .facingReraise: 0.10,
                           .continuationBet: 0.04, .facingContinuationBet: 0.06, .flopDecision: 0.01,
                           .turnDecision: 0.06, .riverDecision: 0.03, .facingRiverBet: 0.01,
                           .deepStackOff: 0.02]),
        ]

    /// The tournament's arrangements: what the app shipped with.
    public static let tournamentSetups: [AssessmentSetup] = [
        AssessmentSetup(
            id: "early-seat", seat: .underTheGun, depthInBB: 100, lean: .mixed,
            watching: "Which hands you are willing to play from the first seat to speak, where "
                + "everybody else still gets a say after you.",
            expected: [.openingChance: 0.95, .facingReraise: 0.16, .continuationBet: 0.24,
                       .flopDecision: 0.20, .turnDecision: 0.12, .riverDecision: 0.08]),

        AssessmentSetup(
            id: "last-seats", seat: .button, depthInBB: 100, lean: .quiet,
            watching: "Whether you take the pot when it is handed to you: the last seat, with only "
                + "the two blinds left to get past.",
            expected: [.openingChance: 0.90, .facingRaise: 0.18, .continuationBet: 0.42,
                       .flopDecision: 0.32, .turnDecision: 0.20, .riverDecision: 0.14]),

        AssessmentSetup(
            id: "big-blind", seat: .bigBlind, depthInBB: 100, lean: .busy,
            watching: "What you do with the money you have already put in when somebody raises "
                + "at it — the single most expensive habit there is.",
            expected: [.bigBlindDefence: 0.72, .facingContinuationBet: 0.30, .flopDecision: 0.28,
                       .turnDecision: 0.17, .riverDecision: 0.11, .facingRiverBet: 0.10]),

        AssessmentSetup(
            id: "facing-heat", seat: .middle, depthInBB: 100, lean: .busy,
            watching: "How you answer somebody who has already raised, and what happens when you "
                + "raise and they come back at you.",
            expected: [.facingRaise: 0.58, .facingReraise: 0.26, .openingChance: 0.30,
                       .facingContinuationBet: 0.24, .flopDecision: 0.24, .turnDecision: 0.12]),

        AssessmentSetup(
            id: "short-stack", seat: .hijack, depthInBB: 11, lean: .mixed,
            watching: "A stack too short to raise and then change your mind, where the only two "
                + "answers are all in or fold.",
            expected: [.shortStackShove: 0.94, .flopDecision: 0.06]),

        AssessmentSetup(
            id: "deep-and-sticky", seat: .cutoff, depthInBB: 180, lean: .quiet,
            watching: "Whether you get paid: plenty of chips in front of everybody, against people "
                + "who call far too much, where the money is made on the last two cards.",
            expected: [.openingChance: 0.45, .continuationBet: 0.48, .flopDecision: 0.52,
                       .turnDecision: 0.40, .riverDecision: 0.32, .facingContinuationBet: 0.14]),

        AssessmentSetup(
            id: "bet-at-you", seat: .smallBlind, depthInBB: 100, lean: .busy,
            watching: "What you do when somebody keeps betting at you and you have to answer "
                + "first, all the way to the last card.",
            expected: [.facingRaise: 0.30, .facingContinuationBet: 0.44, .facingRiverBet: 0.24,
                       .flopDecision: 0.34, .turnDecision: 0.24, .riverDecision: 0.14]),
    ]

    public static func setup(id: String) -> AssessmentSetup? {
        setups.first { $0.id == id }
    }

    /// What a hand set up this way would be expected to teach, given what is already known — and
    /// given what this setup has actually been producing, which is not always what it promised.
    public static func gain(of setup: AssessmentSetup, given coverage: AssessmentCoverage,
                            yield: SetupYield = SetupYield()) -> Double {
        SpotKind.measured.reduce(0.0) { total, spot in
            total + yield.expected(spot, of: setup) * coverage.need(in: spot)
        }
    }

    /// The setup for the next hand. Ties go to the earlier setup in the list, so the same picture
    /// always yields the same hand — a run that replayed differently would be untestable.
    public static func next(for coverage: AssessmentCoverage,
                            yield: SetupYield = SetupYield()) -> AssessmentSetup {
        var best = setups[0]
        var bestGain = gain(of: best, given: coverage, yield: yield)
        for setup in setups.dropFirst() {
            let value = gain(of: setup, given: coverage, yield: yield)
            if value > bestGain + 1e-9 {
                best = setup
                bestGain = value
            }
        }
        return best
    }
}
