import Foundation
import PokerCore
import PokerAI
import PokerCoach

/// One rung of the ladder: a room, described by who is sitting in it.
///
/// The rung is the **cast**, not the dial, because that is what the shootouts measured. A
/// competent regular beats a table of calling stations by well over a
/// hundred big blinds per hundred hands, and beats a better version of its own style by single
/// digits: style is worth roughly ten times what skill is worth. So a promotion here replaces
/// people. Three of the players who called everything are gone and two regulars have taken their
/// chairs, and *that* is what makes the next rung hard. The skill number moves too, second, and it
/// only decides how well each of them plays the style they were given.
///
/// Every rung seats eight opponents, because the tables in this app are nine handed.
public struct LadderTier: Sendable, Equatable, Identifiable {
    /// Stable, and written into every saved hand. Never renamed: a hand recorded under an id that
    /// no longer exists simply stops counting towards the ladder, which is a silent demotion.
    public let id: String
    /// Zero at the bottom.
    public let rung: Int
    public let name: String
    /// The eight people who sit down. This is what the rung actually is.
    public let roster: [Personality]
    /// How well they play it. Secondary, and deliberately so.
    public let skill: Int
    /// What the room is like, in plain English.
    public let blurb: String
    /// The rating that moves you up from here, or nil at the top.
    public let promotionRating: Int?
    /// The rating that drops you back, or nil at the bottom — nobody falls off the ladder.
    public let demotionRating: Int?
    /// Cash rungs only: the blinds this room plays. A cash ladder is a ladder of stakes.
    public let stakes: BlindLevel?

    public init(id: String, rung: Int, name: String, roster: [Personality], skill: Int, blurb: String,
                promotionRating: Int?, demotionRating: Int?, stakes: BlindLevel? = nil) {
        self.id = id
        self.rung = rung
        self.name = name
        self.roster = roster
        self.skill = skill
        self.blurb = blurb
        self.promotionRating = promotionRating
        self.demotionRating = demotionRating
        self.stakes = stakes
    }

    public var isTop: Bool { promotionRating == nil }
    public var isBottom: Bool { demotionRating == nil }

    /// The name as it reads inside a sentence. Only the article changes case: lowercasing the
    /// whole thing gives "the friday night home game", and Friday is still a Friday.
    public var inSentence: String {
        name.hasPrefix("The ") ? "the " + name.dropFirst(4) : name
    }

    /// The rung above and below, where there is one.
    public var next: LadderTier? { LadderTier.all.first { $0.rung == rung + 1 } }
    public var previous: LadderTier? { LadderTier.all.first { $0.rung == rung - 1 } }

    /// The cast, counted up and kept in roster order — the order the rungs are written in, which
    /// runs from the players who give money away towards the ones who take it.
    public var cast: [(style: Personality, count: Int)] {
        var order: [Personality] = []
        var counts: [Personality: Int] = [:]
        for style in roster {
            if counts[style] == nil { order.append(style) }
            counts[style, default: 0] += 1
        }
        return order.map { ($0, counts[$0] ?? 0) }
    }

    /// "Three calling stations, three loose passive players, a maniac and a nit."
    public var castLine: String {
        let parts = cast.map { $0.style.counted($0.count) }
        guard let last = parts.last else { return "" }
        if parts.count == 1 { return last + "." }
        return parts.dropLast().joined(separator: ", ") + " and " + last + "."
    }

    // MARK: - The rungs

    /// Five rooms, and the whole difference between them is who is in them.
    ///
    /// Read the rosters downwards and the story is the one the measurements told: the people who
    /// call too much and raise too little are replaced, a couple at a time, by people who do
    /// neither. The skill numbers climb alongside, but they are the smaller half of it, and each
    /// one is pulled into whatever band the style it is attached to can exist in — a calling
    /// station is a losing style by definition and cannot be an expert one.
    ///
    /// The bars are twenty rating points apart on purpose. The rating that moves you off a rung is
    /// always twenty clear of the one that would send you back down to it, so a player sitting
    /// near a bar settles on one side of it instead of being carried up and down by the ordinary
    /// wobble of a few hundred hands. See `LadderStanding`.
    ///
    /// The bars themselves were re-derived when the rating was re-anchored on tournament play
    /// rather than on the coach harness — the old ones sat against a scale where the flawless
    /// reference player scored zero, so on the new one everybody would have climbed to the top in
    /// an evening. They are placed so that each of the scripted players in `pokerlab calibrate`
    /// settles on a different rung, in the order anybody would put them in:
    ///
    ///     plays far too many hands       rating  9   the kitchen table
    ///     folds to every flop bet               17   the kitchen table
    ///     never raises first in                 30   the Friday night home game
    ///     folds the big blind                   48   the casino low stakes game
    ///     never value bets the river            59   the regular game
    ///     no planted flaw                       62   the tough table
    ///
    /// That is the whole ladder doing its job: play like a bot with no deliberate flaw and you
    /// reach the top table, and every habit you have not fixed keeps you a rung below it.
    /// The rungs of the ladder this product plays: rooms at a tournament, stakes at a cash table.
    public static var all: [LadderTier] { all(for: AppVariant.current.format) }

    public static func all(for format: AppVariant.Format) -> [LadderTier] {
        format == .cash ? cashRungs : tournamentRungs
    }

    /// The cash ladder: four stakes, and the same story down the rosters — the people who call
    /// too much and raise too little are replaced, a couple at a time, by people who do neither.
    /// Moving up is moving up in stakes, which is what a cash player means by it. The bars were
    /// set from `pokerlab calibrate --format cash` (the line is in `SkillRating`) so the scripted
    /// players spread across them: stacks off with one pair 10 and plays far too many hands 30
    /// (25/50), folds to every flop bet 40 (50/100), never raises first in 50, folds the hands the
    /// stacks pay for 49 and folds the big blind 55 (100/200), never bets the river 62 and the
    /// control 66 (500/1000). Twenty points between the bar that moves you up and the one that
    /// sends you back, as on the tournament ladder.
    public static let cashRungs: [LadderTier] = [
        LadderTier(
            id: "cash-25-50", rung: 0, name: "The 25/50 table",
            roster: tournamentRungs[0].roster, skill: tournamentRungs[0].skill,
            blurb: "The smallest game, and the loosest. Almost nobody folds and almost nobody "
                + "raises, so a good hand does not have to be clever here — it has to bet, and it "
                + "gets paid.",
            promotionRating: 34, demotionRating: nil,
            stakes: BlindLevel(smallBlind: 25, bigBlind: 50)),

        LadderTier(
            id: "cash-50-100", rung: 1, name: "The 50/100 table",
            roster: tournamentRungs[1].roster, skill: tournamentRungs[1].skill,
            blurb: "Still loose, but two of them are waiting for good cards and one of them is "
                + "playing properly. Free pots are rarer, and a weak hand now gets raised off "
                + "rather than simply called.",
            promotionRating: 46, demotionRating: 14,
            stakes: BlindLevel(smallBlind: 50, bigBlind: 100)),

        LadderTier(
            id: "cash-100-200", rung: 2, name: "The 100/200 table",
            roster: tournamentRungs[2].roster, skill: tournamentRungs[2].skill,
            blurb: "Half the table knows exactly what it is doing. There is still one person here "
                + "paying for everybody else, and working out which one early is most of the "
                + "evening.",
            promotionRating: 58, demotionRating: 26,
            stakes: BlindLevel(smallBlind: 100, bigBlind: 200)),

        LadderTier(
            id: "cash-500-1000", rung: 3, name: "The 500/1000 table",
            roster: tournamentRungs[3].roster, skill: tournamentRungs[3].skill,
            blurb: "Nobody at this table is giving anything away. Pots are fought over, weak hands "
                + "get pushed out, and the money stops coming from other people's mistakes and "
                + "starts coming from small edges taken over and over.",
            promotionRating: nil, demotionRating: 38,
            stakes: BlindLevel(smallBlind: 500, bigBlind: 1000)),
    ]

    /// The cash rung that plays these blinds, if there is one.
    public static func cash(forStakes stakes: BlindLevel) -> LadderTier? {
        cashRungs.first { $0.stakes?.bigBlind == stakes.bigBlind }
    }

    /// The tournament ladder: what the app shipped with.
    public static let tournamentRungs: [LadderTier] = [
        LadderTier(
            id: "kitchen", rung: 0, name: "The kitchen table",
            roster: [.callingStation, .callingStation, .callingStation,
                     .loosePassive, .loosePassive, .loosePassive, .maniac, .nit],
            skill: 15,
            blurb: "Almost nobody folds and almost nobody raises. Hands get played to the end that "
                + "should never have been played at all, so a good hand here does not have to be "
                + "clever — it just has to bet and get paid.",
            promotionRating: 22, demotionRating: nil),

        LadderTier(
            id: "home-game", rung: 1, name: "The Friday night home game",
            roster: [.callingStation, .callingStation, .loosePassive, .loosePassive,
                     .maniac, .nit, .nit, .tightAggressive],
            skill: 30,
            blurb: "Still loose, but two of them are sitting on their hands waiting for good cards "
                + "and one of them is playing properly. Free pots are rarer, and a weak hand now "
                + "gets raised off rather than simply called.",
            promotionRating: 35, demotionRating: 2),

        LadderTier(
            id: "casino-low", rung: 2, name: "The casino low stakes game",
            roster: [.callingStation, .loosePassive, .nit, .nit,
                     .tightAggressive, .tightAggressive, .looseAggressive, .solidRegular],
            skill: 45,
            blurb: "Half the table knows exactly what it is doing. There is still one person here "
                + "paying for everybody else, and working out which one early is most of the "
                + "evening.",
            promotionRating: 48, demotionRating: 15),

        LadderTier(
            id: "regulars", rung: 3, name: "The regular game",
            roster: [.nit, .tightAggressive, .tightAggressive, .tightAggressive,
                     .solidRegular, .solidRegular, .solidRegular, .looseAggressive],
            skill: 62,
            blurb: "Nobody at this table is giving anything away. Pots are fought over, weak hands "
                + "get pushed out, and the money stops coming from other people's mistakes and "
                + "starts coming from small edges taken over and over.",
            promotionRating: 60, demotionRating: 28),

        LadderTier(
            id: "tough", rung: 4, name: "The tough table",
            roster: [.solidRegular, .solidRegular, .solidRegular,
                     .trickyRegular, .trickyRegular, .trickyRegular,
                     .looseAggressive, .looseAggressive],
            skill: 80,
            blurb: "Everyone here is watching you and changing what they do because of it. Any "
                + "habit you fall into gets found and charged for inside an hour.",
            promotionRating: nil, demotionRating: 40),
    ]

    public static var bottom: LadderTier { all[0] }

    /// The rung a saved hand was played at, or nil if it names one this build no longer has.
    /// Both ladders are searched: a hand is what it says it is whichever product reads it.
    public static func named(_ id: String?) -> LadderTier? {
        guard let id else { return nil }
        return all.first { $0.id == id } ?? tournamentRungs.first { $0.id == id }
            ?? cashRungs.first { $0.id == id }
    }

    /// Whether this rung is on the ladder the product plays. A cash hand read by the tournament
    /// app names a rung that is not on its ladder, and the ladder leaves it out.
    public var isOnThisLadder: Bool { LadderTier.all.contains { $0.id == id } }

    /// Where a rating sits: the highest rung whose entry has actually been earned, which is the
    /// same test the climb itself uses rather than a second set of numbers that could disagree.
    public static func entry(forRating rating: Int) -> LadderTier {
        var placed = bottom
        for tier in all.dropFirst() {
            guard let bar = tier.previous?.promotionRating, rating >= bar else { break }
            placed = tier
        }
        return placed
    }
}

/// Which way the last full sample pointed.
public enum LadderMove: String, Sendable {
    case promoted
    case demoted
    case held
}

/// Where the player is on the ladder, and what their recent hands say about it.
///
/// Worked out from the saved hands, every time, rather than kept in a file of its own. There is no
/// second copy of the standing to fall out of step with the history, for the same reason there is
/// no second copy of an assessment's progress: the hands *are* the record. Every hand written down
/// while sitting on the ladder carries the rung it was played at, so the standing survives a
/// restart because the evidence for it does.
///
/// Two rules decide whether anybody moves, and both exist to stop the ladder reacting to noise.
///
/// - **Nothing moves inside a tournament, or two, or five.** A full sample is two hundred hands at
///   the current table — the same bar every other number in this app is held to — because a good
///   evening and a good player are indistinguishable over forty hands. Someone can win a whole
///   tournament and still be exactly where they were.
/// - **The bars do not touch.** The rating that promotes you off a rung is twenty points clear of
///   the one that would send you back down to it. Without that gap a player sitting on a bar is
///   carried up on one sample and back down on the next for as long as they keep playing, and a
///   ladder that moves someone every fortnight in alternating directions is telling them nothing.
public struct LadderStanding: Sendable {

    /// Hands at the current table before anything can move. Two hundred is roughly five
    /// tournaments, and it is the number the rest of the app already treats as the point where a
    /// rate stops being an anecdote.
    public static let sampleSize = 200

    /// How far back the sample reaches. Beyond this the older hands are let go, so a bad month
    /// last year cannot hold somebody down after they have fixed it.
    public static let window = 400

    /// The rung the hands in question were actually played at.
    public let settled: LadderTier
    /// The rung the next table will be — `settled` moved by whatever the sample decided.
    public let tier: LadderTier
    public let move: LadderMove
    /// Hands played at `settled` since the last time anybody moved.
    public let handsAtTier: Int
    /// How many of those the rating below was worked out from.
    public let sample: Int
    /// What those hands say the player is worth. Nil before there are any.
    public let rating: SkillRating?
    /// The habit costing the most at this table, where the hands support naming one at all.
    public let costliest: LeakScorecard.Entry?
    /// Big blinds won or lost over the counted hands — the number a cash player calls a result.
    public let netBB: Double

    /// Big blinds per hundred hands over the counted sample, the way cash players count it.
    public var winRatePer100: Double { sample > 0 ? netBB / Double(sample) * 100 : 0 }
    /// Whether this is a ladder of stakes.
    public var isCash: Bool { settled.stakes != nil }
    /// "50/100" for a cash rung.
    public static func stakesText(_ tier: LadderTier) -> String {
        guard let stakes = tier.stakes else { return tier.inSentence }
        return "\(stakes.smallBlind)/\(stakes.bigBlind)"
    }

    public init(of records: [HandRecord]) {
        // Hands written down under a rung this build still has. A rung that was removed takes its
        // hands out of the ladder with it rather than being guessed at.
        let onLadder = records.filter { LadderTier.named($0.tier)?.isOnThisLadder == true }

        guard let latest = onLadder.last, let settled = LadderTier.named(latest.tier) else {
            // Nobody has sat on the ladder yet. Everyone starts at the bottom unless there is
            // already a real history saying otherwise — and "real" is the same two hundred hands,
            // because a rating off forty hands is held near the middle on purpose and placing
            // somebody off it would be reading a number the hands do not support.
            //
            // Arranged hands are not results. An assessment chose the seat and the stacks and a
            // drill chose the situation, so neither can move anybody — and counting them towards
            // the two hundred while the rating below refuses to count them meant an evening of
            // drilling opened the door on a number built from almost nothing.
            // And a hand written at a rung of the other ladder is no evidence about this one.
            let real = records.filter { !$0.isArranged && (LadderTier.named($0.tier)?.isOnThisLadder ?? true) }
            let placed = real.count >= Self.sampleSize
                ? LadderTier.entry(forRating: LeakScorecard(of: real).rating.value)
                : LadderTier.bottom
            self.settled = placed
            self.tier = placed
            self.move = .held
            self.handsAtTier = 0
            self.sample = 0
            self.rating = nil
            self.costliest = nil
            self.netBB = 0
            return
        }

        // The run at this rung. On the cash ladder a rung is a stake AND a kind of betting: two
        // hundred hands of fixed-limit and two hundred of no-limit at 50/100 are two records on
        // two scales, not one run — and the hands are read on the scale of the game they were
        // played at, whatever the player is looking at now. Hands at another stake in between do
        // not break the run; each stake keeps its own.
        let structure = settled.stakes != nil ? latest.structure : nil
        let run = onLadder.filter { $0.tier == settled.id && (structure == nil || $0.structure == structure) }

        let counted = Array(run.suffix(Self.window))
        let card = LeakScorecard(of: counted)
        let format: CoachFormat = settled.stakes == nil ? .tournament
            : (structure == BettingStructure.fixedLimit.rawValue ? .limit : .cash)
        let rating = card.rating(for: format)

        self.settled = settled
        self.handsAtTier = run.count
        self.sample = counted.count
        self.rating = rating
        self.costliest = card.costing.first
        self.netBB = counted.reduce(0.0) { $0 + $1.netBB }

        if run.count < Self.sampleSize {
            self.move = .held
            self.tier = settled
        } else if let bar = settled.promotionRating, rating.value >= bar, let up = settled.next {
            self.move = .promoted
            self.tier = up
        } else if let bar = settled.demotionRating, rating.value <= bar, let down = settled.previous {
            self.move = .demoted
            self.tier = down
        } else {
            self.move = .held
            self.tier = settled
        }
    }

    /// True once the sample is long enough for the bars to apply at all.
    public var enoughHands: Bool { handsAtTier >= Self.sampleSize }
    /// Hands still to play before anything can move.
    public var handsToGo: Int { max(0, Self.sampleSize - handsAtTier) }
    /// How far through the sample they are, for a bar on the screen.
    public var fraction: Double {
        min(1, Double(handsAtTier) / Double(Self.sampleSize))
    }

    // MARK: - Saying it out loud

    /// Where they are sitting, or where they are about to sit.
    public var placeLine: String {
        if isCash {
            switch move {
            case .promoted:
                return "You have earned \(Self.stakesText(tier)). That is the game to sit down at next."
            case .demoted:
                return "You are going back to \(Self.stakesText(tier)) for a while."
            case .held:
                return "You play \(Self.stakesText(settled))."
            }
        }
        switch move {
        case .promoted:
            return "You have earned \(tier.name). That is where you sit down next."
        case .demoted:
            return "You are going back to \(tier.name) for a while."
        case .held:
            return "You are at \(settled.name)."
        }
    }

    /// A cash player's own number for the table, said beside the rating that moves them.
    public var winRateLine: String? {
        guard isCash, sample > 0 else { return nil }
        let rate = String(format: "%+.0f", winRatePer100)
        return "Win rate there: \(rate) big blinds for every 100 hands, over \(sample) hands."
    }

    /// What the hands actually say — including, most of the time, that they do not say enough yet.
    public var resultsLine: String {
        guard let rating, handsAtTier > 0 else {
            return "You have not played a hand at this table yet. Nothing moves in either "
                + "direction until you have played \(Self.sampleSize) of them."
        }
        guard enoughHands else {
            let spell = isCash ? "an evening or two" : "a few tournaments"
            return "\(handsAtTier) hands here so far, and they rate you \(rating.value). Nothing "
                + "moves until \(Self.sampleSize) — \(handsToGo) to go — because over "
                + "\(spell) a good player and a lucky one look exactly the same."
        }
        switch move {
        case .promoted:
            return "Over your last \(sample) hands at this table you rate \(rating.value), which "
                + "is past the \(settled.promotionRating ?? 0) it asks for. That is a big enough "
                + "run of hands to mean it."
        case .demoted:
            return "Over your last \(sample) hands at this table you rate \(rating.value), which "
                + "is under the \(settled.demotionRating ?? 0) it holds you to. A table you cannot "
                + "beat teaches you nothing, so you go back a rung and build the sample up again."
        case .held:
            let bar = settled.promotionRating
            let needed = bar.map { " You need \($0) to move up." } ?? " There is nothing above this one."
            return "Over your last \(sample) hands at this table you rate \(rating.value).\(needed)"
        }
    }

    /// What it takes to move up from where they are.
    public var requirementLine: String {
        guard let bar = settled.promotionRating, let up = settled.next else {
            return "This is the top of the ladder. There is no tougher table to be moved to, so "
                + "from here the rating is the only thing that goes up."
        }
        if isCash {
            return "Rate \(bar) or better across at least \(Self.sampleSize) hands at "
                + "\(Self.stakesText(settled)) — the last \(Self.window) count — and the ladder moves "
                + "you up to \(Self.stakesText(up))."
        }
        return "Rate \(bar) or better across at least \(Self.sampleSize) hands at \(settled.inSentence) "
            + "— the last \(Self.window) count — and you move to \(up.inSentence)."
    }

    /// And what it takes to fall back, said out loud rather than left as a surprise.
    public var demotionLine: String? {
        guard let bar = settled.demotionRating, let down = settled.previous else {
            return "Nobody falls off the bottom rung. This is where everyone starts."
        }
        return "If those same \(Self.sampleSize) hands rate you \(bar) or worse you go back to "
            + "\(down.inSentence). That is \((down.promotionRating ?? 0) - bar) points below what it "
            + "takes to come back up, and the gap is deliberate: a bad fortnight should not undo a "
            + "promotion, and a good one should not hand you a table you are not ready for."
    }

    /// The honest caveat that belongs under all of it.
    public var meaningNote: String {
        "This rating is only about what your own mistakes cost. It is not a win rate and it does "
            + "not care whether the cards ran for you, which is exactly why it is the thing the "
            + "ladder is hung on."
    }

    /// Which hands this rating is off, said out loud beside the other one.
    ///
    /// Two numbers, both called "rating", visible at the same time on a wide screen: the one beside
    /// this is counted over every hand ever played, and this one over at most the last 400 at the
    /// table the player is on now. They disagree, often by a lot — a forty-hand assessment drags
    /// the all-time number about and cannot touch this one at all — and "you rate 47, you need 52
    /// to move up" beside a permanent "RATING 62" reads as an app that has forgotten to promote
    /// somebody. It lives here rather than in the view because a sentence a test cannot read is a
    /// sentence nobody is checking.
    public var scopeNote: String {
        "This is counted over your hands at this table only, so it will not match the all-time "
            + "rating in the corner — that one is every hand you have ever played, and moving up "
            + "turns on this one."
    }

    /// Where the money is going at this table, when there is enough to say. Nil is the common
    /// answer and the honest one.
    public var costliestLine: String? {
        guard let costliest else { return nil }
        guard let seat = costliest.seatLine else { return costliest.frequencyLine }
        return costliest.frequencyLine + " " + seat
    }
}
