import Foundation
import PokerCore
import PokerAI
import PokerCoach
import PokerSession

/// The two store screenshots that need somebody playing: the felt with a decision on it, and the
/// coach's review of a hand.
///
/// The other six screens are reached by name through `POKERTUTOR_SCREEN`. These two cannot be:
/// the app never reopens onto a felt nobody is sitting at, and a review only exists once a hand
/// has been played to the end. So the screenshot script asks for a scene instead, through
/// `POKERTUTOR_DEMO`, and the app plays a fixed-seed game against itself until the scene is on
/// the table. Nothing in here runs unless that variable is set, and a test plays both scenes
/// headlessly, for both products, so the script cannot start producing an empty felt without
/// anybody noticing.
public enum DemoScene: String, CaseIterable, Sendable {
    /// The player to act on the flop with a hand worth looking at.
    case table
    /// The coach's review open over a hand it had something to say about.
    case review

    /// The seed both scenes are played from. Fixed so the shots can be remade identically; the
    /// hand each scene lands on is whatever this seed deals first that qualifies.
    public static let seed: UInt64 = 20_260_912

    static let environmentKey = "POKERTUTOR_DEMO"

    /// The scene asked for in the environment, if any.
    static var requested: DemoScene? {
        guard let named = ProcessInfo.processInfo.environment[environmentKey] else { return nil }
        return DemoScene(rawValue: named)
    }

    /// A player for the hero's chair. The review scene wants somebody who makes the odd mistake,
    /// so the coach has something to open a sheet about. The table scene wants somebody who sees
    /// flops: a sound player folds most hands before one, and a fixed seed then runs out of
    /// tournament before it deals a flop worth photographing.
    var player: Bot {
        switch self {
        case .table: return Bot(id: "demo", personality: .callingStation, skill: 40, seed: Self.seed)
        case .review: return Bot(id: "demo", personality: .solidRegular, skill: 35, seed: Self.seed)
        }
    }

    /// The table the cash product's scenes are played at: the middle stakes, so the chips in the
    /// shot are the ones most people would recognise. The felt is shot at fixed-limit, because
    /// that is the one thing no other table in these apps shows — the tray gone, the bet written
    /// on the button, "Limit: Fixed-limit" in the header — while the review is shot at no-limit,
    /// where the cash lessons about the money behind and a stack going in with one pair live.
    public func cashSetup() -> CashSetup {
        let stakes = BlindLevel(smallBlind: 50, bigBlind: 100)
        switch self {
        case .table: return CashSetup(stakes: stakes, structure: .fixedLimit)
        case .review: return CashSetup(stakes: stakes, structure: .noLimit)
        }
    }
}

extension AppState {

    /// Plays the session until `scene` is on the table, and leaves the app showing it.
    ///
    /// Returns false if the seed never produced the scene within the hands allowed, which the
    /// test turns into a failure — a screenshot script that quietly captured Home instead of the
    /// felt is exactly the kind of stale-by-accident this exists to prevent.
    @discardableResult
    public func arrangeDemo(_ scene: DemoScene, handsAllowed: Int = 400) -> Bool {
        var bot = scene.player
        sheet = nil
        // The cash product's scenes are played at a cash table, so the shot shows the product:
        // the stakes and the limit in the header, and a review about the money behind rather
        // than about a tournament stack.
        let mode: GameMode = session.variant.format == .cash ? .cash : .tournament
        if mode == .cash { session.cashSetup = scene.cashSetup() }
        session.start(mode)
        var handsSeen = 0
        var steps = 0
        while steps < 60_000, handsSeen < handsAllowed {
            steps += 1
            // Busted or won: the next tournament, from the same stream of cards. A cash table
            // never ends on its own — a bust buys back in.
            if !session.isPlaying { session.start(mode) }
            if session.heroToAct, let view = session.engine?.view(for: session.heroSeat) {
                if scene == .table, Self.isWorthAShot(view) {
                    screen = .table
                    return true
                }
                _ = session.act(bot.decide(view))
            } else if session.engine?.isComplete == true {
                handsSeen += 1
                if scene == .review, let review = session.review, !review.playedWell,
                   Self.isWorthShowing(review, patience: handsSeen < handsAllowed / 2,
                                       format: session.variant.format) {
                    // Setting the screen is what raises the sheet: coming back to a finished hand
                    // with something to say puts the review up, the same as it does for a person.
                    screen = .table
                    if openSheet != "review" { showReview() }
                    return true
                }
                session.continueAfterReview()
            } else if session.stepOpponent() == nil {
                return false
            }
        }
        return false
    }

    /// A review worth photographing: one whose headline is a piece of coaching about a decision
    /// — a call or a fold that was wrong, too many hands played — rather than a sizing quibble.
    /// While there are hands to spare the scene waits for one of those; after that any hand the
    /// coach spoke about will do.
    ///
    /// At a cash table the first choice is a cash lesson — the money behind, a stack that went in
    /// with one pair, a re-raise called without the odds, a limit habit — and the shared list is
    /// the second.
    static func isWorthShowing(_ review: HandReview, patience: Bool,
                               format: AppVariant.Format = .tournament) -> Bool {
        guard patience else { return true }
        guard let error = review.worthMentioning.first?.error else { return false }
        return wanted(for: format).contains(error)
    }

    static func wanted(for format: AppVariant.Format) -> Set<ErrorKind> {
        let shared: Set<ErrorKind> = [.calledTooWide, .foldedTooMuch, .playedTooManyHands,
                                      .limpedInstead]
        switch format {
        case .tournament: return shared.union([.shovedTooWide, .failedToShove])
        case .cash: return shared.union([.missedImpliedOdds, .stackedOffOnePair,
                                         .setMinedWithoutOdds, .cappedLight, .missedFreeCard])
        }
    }

    /// A flop worth photographing: the player still has an opponent, there is a pot to look at,
    /// and the hand is either already something — two pair or better — or has a real draw, the
    /// two things a decision on the flop is usually about.
    static func isWorthAShot(_ view: DecisionView) -> Bool {
        guard view.street == .flop, view.opponentCount >= 1,
              view.pot >= 6 * view.bigBlind else { return false }
        let read = HandReader.assess(hole: view.holeCards, board: view.board,
                                     opponents: view.opponentCount)
        return read.category >= .twoPair || read.outs >= 8
    }
}
