import Foundation
import PokerCore
import PokerAI
import PokerCoach
import PokerSession
import PokerSession
import PokerTests

/// What mistakes actually cost in the game the app ships, across the whole range of strengths.
///
/// The rating scale was anchored on the fixed-depth coach harness — a hundred big blinds deep from
/// the first hand to the last. Tournaments are not that. Blinds climb, stacks get short, and the
/// same player gives up far more per hundred hands than the harness ever showed, so the flawless
/// reference player came out at zero: the bottom of a scale built for a different game.
///
/// This plays real tournaments with a bot of a chosen strength in the hero's seat and reports what
/// the scorecard charges it. Several seeds, because one poker measurement is not a measurement —
/// this repo has drawn the wrong conclusion from a single seed four times now.
///
///     pokerlab calibrate --hands 1200 --seeds 6
func runCalibration(hands: Int, seedCount: Int, firstSeed: UInt64,
                    format: AppVariant.Format = .tournament, setup: CashSetup = .standard) {
    let mode: GameMode = format == .cash ? .cash : .tournament
    // The scripted players, not the skill dial. The dial is a weak lever — this repo already knows
    // style beats skill by roughly ten to one — and measured over tournaments it produced identical
    // numbers for skills 5, 20 and 40 and ranked skill 60 below skill 5. The planted players differ
    // in ways the coach can actually see, which is what a rating scale has to be anchored on.
    var players: [(String, String)] = [
        ("control", "no planted flaw"),
        ("bb", "folds the big blind"),
        ("river", "never value bets the river"),
        ("flop", "folds to every flop bet"),
        ("limps", "never raises first in"),
        ("loose", "plays far too many hands"),
    ]
    if format == .cash, setup.structure == .fixedLimit {
        // The limit coach's own habits, on the limit scale.
        players += [("limitfolder", "folds a draw to one bet"),
                    ("noraise", "calls with the best hand"),
                    ("coldcaller", "calls two bets cold"),
                    ("riverfolder", "folds the river to one bet"),
                    ("calldown", "calls the last bet with nothing"),
                    ("nofreecard", "never raises for the free card"),
                    ("capper", "caps with anything")]
        AppVariant.use(.pokerTutorCash)
    } else if format == .cash {
        // The cash coach's own habits belong on its scale too.
        players += [("stackoff", "stacks off with one pair"),
                    ("setminer", "calls every raise with a small pair"),
                    ("foldsimplied", "folds the hands the stacks pay for")]
        AppVariant.use(.pokerTutorCash)
    }
    let coachFormat: CoachFormat = format == .cash ? (setup.structure == .fixedLimit ? .limit : .cash) : .tournament
    print((format == .cash ? "Cash play (\(setup.stakesLabel) \(setup.structure.rawValue))" : "Tournament play")
          + ", \(hands) hands per run, \(seedCount) seeds per strength.")
    print("")
    print("player                        cost per 100 hands (bb)               mean   penalty  rating now")
    print("---------------------------   ------------------------------------  -----  -------  ----------")

    // (label, low cost, mean cost, high cost, mean penalty the confirmed habits actually incurred)
    var table: [(String, Double, Double, Double, Double)] = []
    for (key, label) in players {
        var costs: [Double] = []
        var penalties: [Double] = []
        for i in 0..<seedCount {
            let seed = firstSeed &+ UInt64(i &* 97)
            let history = HistoryStore.inMemory()
            let session = TableSession(seed: seed, history: history,
                                       variant: format == .cash ? .pokerTutorCash : .pokerTutor)
            session.cashSetup = setup
            var rng = SeededRandom(seed: seed &* 13 &+ 7)
            let player = plantedPlayer(named: key)
            var steps = 0
            session.start(mode)
            while history.hands.count < hands {
                steps += 1
                guard steps < 900_000 else { break }
                if !session.isPlaying { session.start(mode); continue }
                if session.heroToAct, let view = session.engine?.view(for: session.heroSeat) {
                    session.act(player.decide(view, &rng))
                } else if session.engine?.isComplete == true {
                    session.continueAfterReview()
                } else {
                    session.stepOpponent()
                }
            }
            let card = LeakScorecard(of: history.hands)
            costs.append(card.rating.costPer100)
            // The penalty the player's own confirmed habits add. A real player always carries
            // some, so a scale anchored on cost alone had its bottom end below zero.
            penalties.append(card.profile.leakPenalty)
        }
        let mean = costs.reduce(0, +) / Double(costs.count)
        let low = costs.min() ?? 0, high = costs.max() ?? 0
        let penalty = penalties.reduce(0, +) / Double(penalties.count)
        let rating = SkillRating(costPer100: mean, hands: hands, leakPenalty: penalty,
                                 format: coachFormat).value
        table.append((label, low, mean, high, penalty))
        let spread = costs.map { String(format: "%.1f", $0) }.joined(separator: " ")
        print("  \(label.padding(toLength: 28, withPad: " ", startingAt: 0))"
              + spread.padding(toLength: 38, withPad: " ", startingAt: 0)
              + String(format: "%5.1f  %7.1f  %4d", mean, penalty, rating))
    }

    print("")
    print("What that says about the scale")
    guard let best = table.first, let worst = table.max(by: { $0.2 < $1.2 }) else { return }
    print("  \(best.0) costs " + String(format: "%.1f", best.2) + " bb/100 — the top of the scale")
    print("  \(worst.0) costs " + String(format: "%.1f", worst.2) + " bb/100 — the bottom")
    print("")
    // Where the scale would have to sit for the two ends to land somewhere meaningful. The product
    // decision is what a strong player should see; the arithmetic to get there is not a decision.
    // With each end's own penalty in: rating = a - cost * b - penalty, so the two ends give
    //   a - best.cost * b - best.penalty = strong,  a - worst.cost * b - worst.penalty = weak.
    let target = (weak: 10.0, strong: 66.0)
    let slope = (target.strong - target.weak - (worst.4 - best.4)) / (worst.2 - best.2)
    let intercept = target.strong + slope * best.2 + best.4
    print(String(format: "  to put the weakest at %.0f and the strongest at %.0f, penalties included:",
                 target.weak, target.strong))
    print(String(format: "      rating = %.1f - cost * %.3f - penalty", intercept, slope))
    print("  every player, on that line:")
    for row in table {
        let value = Int((intercept - row.2 * slope - row.4).clamped(to: 0...100).rounded())
        print("      \(row.0.padding(toLength: 28, withPad: " ", startingAt: 0)) \(value)")
    }
}
