import Foundation
import PokerCore

/// Every knob that defines how an opponent plays. Fractions are of all starting hands unless
/// noted; thresholds are on the 0-1 hand-strength scale from `HandReader`.
public struct BotParameters: Sendable {
    public var openRange: Double          // opened from a neutral seat, before position adjusts it
    public var threeBetRange: Double
    public var callVsRaiseRange: Double
    public var blindDefenseRange: Double
    public var foldToThreeBet: Double
    public var cbetFrequency: Double
    public var bluffFrequency: Double
    public var aggression: Double         // how often a strong hand raises rather than calls
    public var callSlack: Double          // equity below the correct price they will still call with
    public var valueThreshold: Double     // strength needed to bet for value
    public var raiseThreshold: Double     // strength needed to raise a bet
    public var betSizing: Double          // preferred bet as a fraction of the pot
    public var sizingNoise: Double        // 0 disciplined, 1 erratic
    public var limpTendency: Double       // chance of limping rather than raising or folding
    public var pushFoldDepth: Double      // big blinds at which they switch to shove-or-fold; 0 never
    public var adaptation: Double         // how much they exploit what they have seen you do
    /// How badly they misjudge the strength of their own hand. Zero is a perfect read; a quarter
    /// means routinely treating middle pair as top pair or a good hand as a marginal one. This is
    /// the one thing on this list that is unambiguously a mistake at every table, in every style,
    /// which is what makes it the backbone of the skill dial.
    public var readError: Double

    /// Blends two parameter sets. `t` of 0 is all `a`, 1 is all `b`.
    static func blend(_ a: BotParameters, _ b: BotParameters, _ t: Double) -> BotParameters {
        func mix(_ x: Double, _ y: Double) -> Double { x + (y - x) * t }
        return BotParameters(
            openRange: mix(a.openRange, b.openRange),
            threeBetRange: mix(a.threeBetRange, b.threeBetRange),
            callVsRaiseRange: mix(a.callVsRaiseRange, b.callVsRaiseRange),
            blindDefenseRange: mix(a.blindDefenseRange, b.blindDefenseRange),
            foldToThreeBet: mix(a.foldToThreeBet, b.foldToThreeBet),
            cbetFrequency: mix(a.cbetFrequency, b.cbetFrequency),
            bluffFrequency: mix(a.bluffFrequency, b.bluffFrequency),
            aggression: mix(a.aggression, b.aggression),
            callSlack: mix(a.callSlack, b.callSlack),
            valueThreshold: mix(a.valueThreshold, b.valueThreshold),
            raiseThreshold: mix(a.raiseThreshold, b.raiseThreshold),
            betSizing: mix(a.betSizing, b.betSizing),
            sizingNoise: mix(a.sizingNoise, b.sizingNoise),
            limpTendency: mix(a.limpTendency, b.limpTendency),
            pushFoldDepth: mix(a.pushFoldDepth, b.pushFoldDepth),
            adaptation: mix(a.adaptation, b.adaptation),
            readError: mix(a.readError, b.readError)
        )
    }
}

/// The kinds of players sitting at the table.
///
/// Personality is *style*; skill is how well the style is executed. The two are separate on
/// purpose, but they are not independent: a calling station is a losing style by definition, so
/// it only exists at low skill. That is why each personality declares the skill band it lives in,
/// and why a table gets harder in two ways at once — the opponents get better at what they do, and
/// the loose recreational players are replaced by competent ones.
public enum Personality: String, CaseIterable, Sendable, Codable {
    case callingStation
    case nit
    case loosePassive
    case maniac
    case tightAggressive
    case looseAggressive
    case solidRegular
    case trickyRegular

    public var name: String {
        switch self {
        case .callingStation: return "Calling station"
        case .nit: return "Nit"
        case .loosePassive: return "Loose passive"
        case .maniac: return "Maniac"
        case .tightAggressive: return "Tight aggressive"
        case .looseAggressive: return "Loose aggressive"
        case .solidRegular: return "Solid regular"
        case .trickyRegular: return "Tricky regular"
        }
    }

    /// "3 calling stations", "1 maniac" — how a cast is listed when a room is being described.
    ///
    /// Written out rather than pluralised by adding an s, because half of these names are
    /// adjectives: "3 loose passives" is not English, and "3 loose passive players" is.
    public func counted(_ count: Int) -> String {
        let one = count == 1
        let word: String
        switch self {
        case .callingStation: word = one ? "calling station" : "calling stations"
        case .nit: word = one ? "nit" : "nits"
        case .maniac: word = one ? "maniac" : "maniacs"
        case .loosePassive: word = one ? "loose passive player" : "loose passive players"
        case .tightAggressive: word = one ? "tight aggressive player" : "tight aggressive players"
        case .looseAggressive: word = one ? "loose aggressive player" : "loose aggressive players"
        case .solidRegular: word = one ? "solid regular" : "solid regulars"
        case .trickyRegular: word = one ? "tricky regular" : "tricky regulars"
        }
        return "\(count) \(word)"
    }

    /// Plain English, for the table and for the coach's explanations.
    public var summary: String {
        switch self {
        case .callingStation: return "Calls far too much and almost never raises. You cannot bluff them, but you can bet every good hand."
        case .nit: return "Plays only premium hands. When they put money in, believe them — and steal their blinds relentlessly."
        case .loosePassive: return "Plays too many hands and rarely raises. Lets you see cheap cards and pays you off."
        case .maniac: return "Raises constantly with anything. Wait for a hand and let them bet into you."
        case .tightAggressive: return "Patient, then aggressive. Solid but predictable — their range is readable from their line."
        case .looseAggressive: return "Applies constant pressure with a wide range. Hard to play against out of position."
        case .solidRegular: return "Balanced and steady. Few obvious mistakes and few obvious tells."
        case .trickyRegular: return "Balanced, and watching you. Punishes any pattern you fall into."
        }
    }

    /// The skill band this style occupies. Losing styles do not exist at high skill.
    public var skillRange: ClosedRange<Int> {
        switch self {
        case .callingStation: return 0...35
        case .loosePassive: return 5...40
        case .maniac: return 5...45
        case .nit: return 10...50
        case .tightAggressive: return 30...80
        case .looseAggressive: return 40...85
        case .solidRegular: return 45...95
        case .trickyRegular: return 60...100
        }
    }

    /// How this style plays at the bottom of its skill band.
    var novice: BotParameters {
        switch self {
        case .callingStation:
            return BotParameters(openRange: 0.48, threeBetRange: 0.010, callVsRaiseRange: 0.55, blindDefenseRange: 0.70,
                                 foldToThreeBet: 0.25, cbetFrequency: 0.20, bluffFrequency: 0.02, aggression: 0.10,
                                 callSlack: 0.30, valueThreshold: 0.72, raiseThreshold: 0.88, betSizing: 0.35,
                                 sizingNoise: 0.8, limpTendency: 0.75, pushFoldDepth: 0, adaptation: 0, readError: 0.26)
        case .loosePassive:
            return BotParameters(openRange: 0.50, threeBetRange: 0.015, callVsRaiseRange: 0.42, blindDefenseRange: 0.60,
                                 foldToThreeBet: 0.45, cbetFrequency: 0.28, bluffFrequency: 0.05, aggression: 0.18,
                                 callSlack: 0.18, valueThreshold: 0.68, raiseThreshold: 0.84, betSizing: 0.38,
                                 sizingNoise: 0.7, limpTendency: 0.70, pushFoldDepth: 0, adaptation: 0, readError: 0.24)
        case .maniac:
            return BotParameters(openRange: 0.62, threeBetRange: 0.22, callVsRaiseRange: 0.35, blindDefenseRange: 0.65,
                                 foldToThreeBet: 0.30, cbetFrequency: 0.85, bluffFrequency: 0.55, aggression: 0.90,
                                 callSlack: 0.12, valueThreshold: 0.35, raiseThreshold: 0.50, betSizing: 0.90,
                                 sizingNoise: 0.9, limpTendency: 0.05, pushFoldDepth: 0, adaptation: 0, readError: 0.25)
        case .nit:
            return BotParameters(openRange: 0.075, threeBetRange: 0.020, callVsRaiseRange: 0.045, blindDefenseRange: 0.09,
                                 foldToThreeBet: 0.80, cbetFrequency: 0.45, bluffFrequency: 0.04, aggression: 0.45,
                                 callSlack: -0.10, valueThreshold: 0.66, raiseThreshold: 0.80, betSizing: 0.55,
                                 sizingNoise: 0.4, limpTendency: 0.20, pushFoldDepth: 0, adaptation: 0, readError: 0.19)
        case .tightAggressive:
            return BotParameters(openRange: 0.15, threeBetRange: 0.040, callVsRaiseRange: 0.075, blindDefenseRange: 0.20,
                                 foldToThreeBet: 0.65, cbetFrequency: 0.70, bluffFrequency: 0.12, aggression: 0.60,
                                 callSlack: -0.04, valueThreshold: 0.58, raiseThreshold: 0.75, betSizing: 0.60,
                                 sizingNoise: 0.35, limpTendency: 0.05, pushFoldDepth: 8, adaptation: 0.05, readError: 0.16)
        case .looseAggressive:
            return BotParameters(openRange: 0.30, threeBetRange: 0.090, callVsRaiseRange: 0.13, blindDefenseRange: 0.34,
                                 foldToThreeBet: 0.50, cbetFrequency: 0.78, bluffFrequency: 0.28, aggression: 0.75,
                                 callSlack: 0.02, valueThreshold: 0.52, raiseThreshold: 0.70, betSizing: 0.68,
                                 sizingNoise: 0.30, limpTendency: 0.02, pushFoldDepth: 9, adaptation: 0.10, readError: 0.15)
        case .solidRegular:
            return BotParameters(openRange: 0.20, threeBetRange: 0.055, callVsRaiseRange: 0.10, blindDefenseRange: 0.28,
                                 foldToThreeBet: 0.58, cbetFrequency: 0.65, bluffFrequency: 0.18, aggression: 0.62,
                                 callSlack: -0.02, valueThreshold: 0.55, raiseThreshold: 0.72, betSizing: 0.62,
                                 sizingNoise: 0.25, limpTendency: 0.02, pushFoldDepth: 10, adaptation: 0.15, readError: 0.14)
        case .trickyRegular:
            return BotParameters(openRange: 0.23, threeBetRange: 0.070, callVsRaiseRange: 0.11, blindDefenseRange: 0.32,
                                 foldToThreeBet: 0.52, cbetFrequency: 0.62, bluffFrequency: 0.22, aggression: 0.65,
                                 callSlack: 0.00, valueThreshold: 0.53, raiseThreshold: 0.71, betSizing: 0.60,
                                 sizingNoise: 0.20, limpTendency: 0.01, pushFoldDepth: 11, adaptation: 0.35, readError: 0.12)
        }
    }

    /// How this style plays at the top of its skill band. The leaks shrink; the style remains.
    ///
    /// Skill only ever removes *mistakes* — erratic bet sizing, calling at the wrong price,
    /// limping instead of raising, not knowing when a stack is short enough to move in, failing
    /// to notice what an opponent is doing. It deliberately does not widen ranges or add bluffs.
    /// Those look like sophistication and measure as losses: a wider range is only profitable if
    /// you can play the extra hands well after the flop, so handing them to a bot whose postflop
    /// game is unchanged just donates money. Tuning it the other way produced a skill dial that
    /// ran backwards — the "expert" version of every competent style lost to its own novice.
    var expert: BotParameters {
        var p = novice
        switch self {
        case .callingStation:
            p.callSlack = 0.16; p.valueThreshold = 0.62; p.aggression = 0.25; p.limpTendency = 0.55
            p.sizingNoise = 0.55; p.bluffFrequency = 0.06; p.openRange = 0.42; p.readError = 0.15
        case .loosePassive:
            p.callSlack = 0.08; p.valueThreshold = 0.60; p.aggression = 0.35; p.limpTendency = 0.45
            p.sizingNoise = 0.45; p.bluffFrequency = 0.10; p.openRange = 0.44; p.threeBetRange = 0.030; p.readError = 0.13
        case .maniac:
            p.bluffFrequency = 0.38; p.valueThreshold = 0.46; p.raiseThreshold = 0.62; p.betSizing = 0.75
            p.sizingNoise = 0.55; p.openRange = 0.48; p.foldToThreeBet = 0.42; p.callSlack = 0.05; p.readError = 0.12
        case .nit:
            p.openRange = 0.125; p.blindDefenseRange = 0.18; p.threeBetRange = 0.035; p.callSlack = -0.04
            p.cbetFrequency = 0.58; p.bluffFrequency = 0.10; p.sizingNoise = 0.20; p.pushFoldDepth = 8
            p.foldToThreeBet = 0.68; p.readError = 0.09
        case .tightAggressive:
            p.sizingNoise = 0.12; p.limpTendency = 0.01; p.callSlack = -0.015
            p.pushFoldDepth = 11; p.adaptation = 0.25; p.readError = 0.05
        case .looseAggressive:
            p.sizingNoise = 0.10; p.limpTendency = 0.0; p.callSlack = -0.005
            p.pushFoldDepth = 12; p.adaptation = 0.40; p.readError = 0.045
        case .solidRegular:
            p.sizingNoise = 0.06; p.limpTendency = 0.0; p.callSlack = -0.005
            p.pushFoldDepth = 12; p.adaptation = 0.55; p.readError = 0.03
        case .trickyRegular:
            p.sizingNoise = 0.05; p.limpTendency = 0.0; p.callSlack = 0.0
            p.pushFoldDepth = 13; p.adaptation = 0.85; p.readError = 0.02
        }
        return p
    }

    /// Parameters for this style at a given skill level.
    public func parameters(skill: Int) -> BotParameters {
        let band = skillRange
        let clamped = Double(skill.clamped(to: band.lowerBound...band.upperBound))
        let span = Double(band.upperBound - band.lowerBound)
        let t = span <= 0 ? 1.0 : (clamped - Double(band.lowerBound)) / span
        return BotParameters.blend(novice, expert, t)
    }

    /// How likely this style is to put money in before you get to decide.
    ///
    /// Not a rating of how good they are — a maniac scores highest and is the worst player on the
    /// list. It answers one narrow question: if this person is sitting where they have to speak
    /// before you do, are you going to be facing a raise or a folded pot? That is what decides
    /// which kind of decision you get handed, which is why the assessment sits people by it.
    public func pressure(skill: Int) -> Double {
        let p = parameters(skill: skill)
        return p.openRange * (1 - p.limpTendency) + p.threeBetRange * 2 + p.aggression * 0.4
    }

    /// Personalities that can plausibly appear at this difficulty.
    public static func available(atDifficulty difficulty: Int) -> [Personality] {
        allCases.filter { $0.skillRange.contains(difficulty.clamped(to: 0...100)) }
    }
}

/// How much wider or tighter a seat plays than its baseline range. Late position is worth far
/// more than most amateurs act like it is.
public enum PositionAdjustment {
    public static func openMultiplier(_ position: Position) -> Double {
        switch position {
        case .underTheGun: return 0.55
        case .underTheGunPlus1: return 0.65
        case .middle: return 0.78
        case .lojack: return 0.88
        case .hijack: return 1.00
        case .cutoff: return 1.30
        case .button: return 1.85
        case .smallBlind: return 1.25
        case .bigBlind: return 1.00
        }
    }

    /// How much wider every range gets as the table shortens.
    ///
    /// Ranges are written for a full table, but the fewer players there are, the fewer hands can
    /// beat you and the more often you are in a blind. Heads-up, the big blind is getting better
    /// than three to one to continue and folding it away is the single most expensive thing a
    /// player can do. A bot that plays its nine-handed range three-handed bleeds blinds until it
    /// dies, which is exactly what tournaments do to it as the table breaks up.
    public static func tableSizeMultiplier(playerCount: Int) -> Double {
        switch max(2, playerCount) {
        case 2: return 2.4
        case 3: return 1.8
        case 4: return 1.5
        case 5: return 1.3
        case 6: return 1.15
        case 7, 8: return 1.05
        default: return 1.0
        }
    }

    /// Being out of position for the rest of the hand costs money; ranges tighten to pay for it.
    public static func isInPosition(_ position: Position, versus other: Position) -> Bool {
        order(position) > order(other)
    }

    /// Whether this seat acts after every one of the others once the cards are out — the seat
    /// that gets to see what everybody else does first.
    public static func actsLast(_ position: Position, among others: [Position]) -> Bool {
        others.allSatisfy { order(position) > order($0) }
    }

    static func order(_ position: Position) -> Int {
        switch position {
        case .smallBlind: return 0
        case .bigBlind: return 1
        case .underTheGun: return 2
        case .underTheGunPlus1: return 3
        case .middle: return 4
        case .lojack: return 5
        case .hijack: return 6
        case .cutoff: return 7
        case .button: return 8
        }
    }
}


