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

/// Measures a habit's bar against the two populations it has to separate, across many seeds.
///
/// This exists because the project has been burned three times by moving a threshold off a single
/// run. A bar is only honest if the player who has the habit and the player who does not measure
/// on opposite sides of it, at a sample where the two bands have stopped overlapping — and neither
/// of those things can be seen in one run of one seed. So the numbers written into
/// `LeakCatalogue` come from here, and the band each one came from is copied into a comment
/// beside it.
///
///     pokerlab sweep --hands 4000 --seeds 12                  every habit, control only
///     pokerlab sweep --player river --against control --hands 4000 --seeds 20
///
/// The `named` column is the one that matters for a control run: any habit a flawless player is
/// named for is a bar sitting inside the band of the player it exists to clear.
///     pokerlab sweep --format cash --rung 0 --player stackoff --against control
///
/// At a cash table the run goes through the app's own table (`runPlantedCash`) rather than the
/// six-handed rig, at the rung asked for — the bars there have to hold in the rooms people play.
func runSweep(playerName: String, againstName: String?, hands: Int, seedCount: Int, firstSeed: UInt64,
              format: AppVariant.Format = .tournament, setup: CashSetup = .standard, rung: Int = 0) {
    let seeds = (0..<seedCount).map { firstSeed &+ UInt64($0) &* 977 }
    if format == .cash { AppVariant.use(.pokerTutorCash) }

    var worst: [String: [Int: Double]] = [:]

    func measure(_ player: PlantedPlayer) -> [String: [(rate: Double, chances: Int, named: Bool)]] {
        var out: [String: [(rate: Double, chances: Int, named: Bool)]] = [:]
        for seed in seeds {
            let coach = format == .cash
                ? runPlantedCash(player, hands: hands, seed: seed, setup: setup, rung: rung, keepEveryReview: true)
                : runPlanted(player, hands: hands, seed: seed, keepEveryReview: true)
            // Named at any point in the run, not only at the end of it. A habit named at hand three
            // hundred and gone by hand four thousand was still named, and the player read it.
            let sweep = NamingSweep(coach)
            let named = Set(sweep.named.map(\.leak.id))
            for (id, marks) in sweep.worstFrom {
                for (mark, rate) in marks { worst[id, default: [:]][mark] = max(worst[id]?[mark] ?? 0, rate) }
            }
            for leak in LeakCatalogue.all {
                guard let record = coach.profile.records[leak.id], record.opportunities > 0 else { continue }
                out[leak.id, default: []].append((record.rate, record.opportunities, named.contains(leak.id)))
            }
        }
        return out
    }

    func band(_ runs: [(rate: Double, chances: Int, named: Bool)]) -> String {
        let rates = runs.map(\.rate).sorted()
        let mean = rates.reduce(0, +) / Double(rates.count)
        let chances = runs.map(\.chances).sorted()
        return String(format: "%3.0f-%3.0f%% (mean %4.1f)  %4d-%4d chances  named %d/%d",
                      rates.first! * 100, rates.last! * 100, mean * 100,
                      chances.first!, chances.last!, runs.filter(\.named).count, runs.count)
    }

    let subject = plantedPlayer(named: playerName)
    print("\(seedCount) seeds x \(hands) hands, first seed \(firstSeed)."
          + (format == .cash ? "  Cash table, \(setup.stakesLabel) \(setup.structure.rawValue), rung \(rung)." : ""))
    print("")
    let mine = measure(subject)
    let theirs = againstName.map { measure(plantedPlayer(named: $0)) }

    for leak in LeakCatalogue.all {
        guard let runs = mine[leak.id] else { continue }
        print(String(format: "%-40@ bar %2.0f%% over %3d", leak.name as NSString,
                     leak.acceptableRate * 100, leak.minimumSample))
        print("   \(subject.name.padding(toLength: 22, withPad: " ", startingAt: 0)) \(band(runs))")
        // The worst the running rate ever reached once it had this many chances behind it, across
        // every seed. This is the column a floor is chosen from: a habit is named the moment the
        // running rate crosses the bar with the floor behind it.
        if againstName == nil, let marks = worst[leak.id], !marks.isEmpty {
            let cells = NamingSweep.marks.compactMap { mark -> String? in
                guard let rate = marks[mark] else { return nil }
                return String(format: "%d:%2.0f%%", mark, rate * 100)
            }
            print("     worst running rate from   \(cells.joined(separator: "  "))")
        }
        if let theirs, let other = theirs[leak.id] {
            let name = plantedPlayer(named: againstName!).name
            print("   \(name.padding(toLength: 22, withPad: " ", startingAt: 0)) \(band(other))")
        }
    }
}
