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

/// Runs a player past the coach and prints what it concluded, so the coaching can be read and
/// judged rather than taken on trust.
func runCoach(hands: Int, seed: UInt64, playerName: String, samples: Int, showProfile: Bool,
              format: AppVariant.Format = .tournament, setup: CashSetup = .standard, rung: Int = 0,
              keepEveryReview: Bool = false, breakdown: Bool = false) {
    let player = plantedPlayer(named: playerName)
    // A cash run goes through the app's own cash table, at the structure asked for; the
    // tournament run is the six-handed rig every tournament bar was set on.
    let coach = format == .cash
        ? runPlantedCash(player, hands: hands, seed: seed, setup: setup, rung: rung, keepEveryReview: keepEveryReview)
        : runPlanted(player, hands: hands, seed: seed)

    print(format == .cash
          ? "Player: \(player.name).  \(hands) hands at the app's cash table, \(setup.stakesLabel) \(setup.structure.rawValue), rung \(rung)."
          : "Player: \(player.name).  \(hands) hands, six-handed, 100 big blinds deep.")
    print("")

    let spoken = coach.reviews.flatMap { review in review.worthMentioning.map { (review, $0) } }
    let opened = coach.reviews.filter { !$0.playedWell }.count
    print(String(format: "Reviews opened on %.1f%% of the last %d hands (%d), %d decisions spoken.",
                 Double(opened) / Double(max(1, coach.reviews.count)) * 100, coach.reviews.count, opened, spoken.count))
    print("")
    if samples > 0 {
        print("What the coach said, worst first")
        print("")
        for (review, verdict) in spoken.sorted(by: { $0.1.costInBB > $1.1.costInBB }).prefix(samples) {
            let cards = verdict.holeCards.map(\.pretty).joined(separator: " ")
            let board = verdict.board.isEmpty ? "—" : verdict.board.map(\.pretty).joined(separator: " ")
            print("  Hand \(review.handNumber), \(verdict.street.name), \(verdict.spot.name)")
            print("    you had \(cards)   board \(board)")
            print("    you \(verdict.taken);  better: \(verdict.best)")
            print("    \(verdict.headline)")
            print("    \(wrap(verdict.detail, indent: 4))")
            print(String(format: "    cost about %.1f big blinds   (confidence %.0f%%)", verdict.costInBB, verdict.confidence * 100))
            print("")
        }
    }

    // `--breakdown`: what the coach charged, grouped by situation, mistake and what was done
    // instead of what — the view that says whether a sound player is being stopped for one
    // habit or for a hundred small disagreements.
    if breakdown {
        var groups: [String: (count: Int, cost: Double)] = [:]
        for (_, verdict) in spoken {
            func shape(_ action: PlayerAction) -> String {
                switch action {
                case .bet: return "bet"
                case .raise: return "raise"
                default: return action.description
                }
            }
            let key = String(format: "%-28@ %-24@ %-6@ -> %@", verdict.spot.name as NSString,
                             (verdict.error?.name ?? "close call") as NSString,
                             shape(verdict.taken) as NSString, shape(verdict.best) as NSString)
            groups[key, default: (0, 0)].count += 1
            groups[key, default: (0, 0)].cost += verdict.costInBB
        }
        print("Spoken decisions by kind (count, big blinds)")
        for (key, value) in groups.sorted(by: { $0.value.count > $1.value.count }) {
            print(String(format: "  %3d  %7.1f  %@", value.count, value.cost, key))
        }
        print("")
    }

    let report = coach.sessionReport()
    print("Session report")
    print("  \(report.headline)")
    print("  Rating \(report.rating.value) — \(report.rating.label)")
    print("")

    if report.confirmed.isEmpty {
        print("  No leaks confirmed.")
    } else {
        print("  Leaks found:")
        for (leak, record) in report.confirmed {
            print(String(format: "    %-38@ %3d of %3d chances (%.0f%%), costing %.0f big blinds",
                         leak.name as NSString, record.mistakes, record.opportunities,
                         record.rate * 100, record.costInBB))
        }
    }
    if !report.watching.isEmpty {
        print("")
        print("  Watching (not enough hands yet):")
        for (leak, record) in report.watching {
            print("    \(leak.name) — \(record.mistakes) of \(record.opportunities)")
        }
    }

    if showProfile {
        print("")
        print("  Every spot the coach measured:")
        for leak in LeakCatalogue.all {
            guard let record = coach.profile.records[leak.id], record.opportunities > 0 else { continue }
            let flag = record.opportunities >= leak.minimumSample ? " " : "*"
            print(String(format: "   %@ %-38@ %3d/%3d = %4.0f%%  (allowed %.0f%%, needs %d)",
                         flag, leak.name as NSString, record.mistakes, record.opportunities,
                         record.rate * 100, leak.acceptableRate * 100, leak.minimumSample))
        }
        print("    * not yet enough chances to judge")
    }
}

/// A bot of a chosen strength, sat in the hero's seat.
///
/// The rating scale has to be anchored on something, and the only players this repo can measure are
/// its own. `control` is one point on that curve — a solid regular at skill 88 — and one point is
/// not a calibration.
func botPlayer(skill: Int, personality: Personality = .solidRegular) -> PlantedPlayer {
    PlantedPlayer(name: "\(personality.rawValue) at skill \(skill)", leakID: "") { view, rng in
        var bot = Bot(id: "calibration", personality: personality, skill: skill, seed: rng.next())
        return bot.decide(view)
    }
}

func plantedPlayer(named name: String) -> PlantedPlayer {
    if name.lowercased().hasPrefix("bot") {
        let skill = Int(name.dropFirst(3)) ?? 88
        return botPlayer(skill: skill)
    }
    switch name.lowercased() {
    case "foldsbb", "bb": return .foldsBigBlind
    case "limps": return .limpsEverything
    case "noriverbet", "river": return .neverValueBetsRiver
    case "noturnbet", "turn": return .neverValueBetsTurn
    case "foldsflop", "flop": return .foldsToEveryFlopBet
    case "callsriver", "hero": return .callsEveryRiverBet
    case "minbet": return .alwaysMinBetsFlop
    case "overbet", "shove": return .alwaysOverbetsFlop
    case "loose", "toomany": return .playsEverything
    case "stackoff": return .stacksOffOnePair
    case "setminer": return .setMinesEverything
    case "foldsimplied": return .foldsSpeculativeHands
    case "limitfolder": return .foldsDrawsForOneBet
    case "noraise": return .neverRaisesPostflop
    case "coldcaller": return .coldCallsRaises
    case "riverfolder": return .foldsRiverToOneBet
    case "calldown": return .callsRiverWithNothing
    case "nofreecard": return .neverRaisesDraws
    case "capper": return .capsEverything
    default:
        // "badseat4" — a sound player who opens far too many hands from seat 4 of 9 only.
        if name.lowercased().hasPrefix("badseat"),
           let number = Int(name.dropFirst(7)),
           let seat = Position.names(forPlayerCount: 9).indices.contains(number - 1)
               ? Position.names(forPlayerCount: 9)[number - 1] : nil {
            return .badInSeat(seat)
        }
        return .control
    }
}

private func wrap(_ text: String, indent: Int, width: Int = 84) -> String {
    var lines: [String] = []
    var current = ""
    for word in text.split(separator: " ") {
        if current.count + word.count + 1 > width {
            lines.append(current)
            current = String(word)
        } else {
            current += current.isEmpty ? String(word) : " \(word)"
        }
    }
    if !current.isEmpty { lines.append(current) }
    return lines.joined(separator: "\n" + String(repeating: " ", count: indent))
}
