import Foundation

/// What one seat can see of another.
public struct SeatView: Sendable, Equatable {
    public let seat: Int
    public let playerID: String
    public let stack: Int
    public let committedThisRound: Int
    public let committedThisHand: Int
    public let antePaid: Int
    public let status: PlayerStatus
    public let position: Position?
    /// Only ever filled in for the seat doing the looking.
    public let hole: [Card]?

    public var isInHand: Bool { status.isInHand }
}

/// Everything a player legitimately knows when it is their turn.
///
/// This type exists to make cheating impossible rather than merely discouraged: bots receive a
/// `DecisionView`, never the engine, so there is no path by which they can read another player's
/// cards. The coach uses the same view when judging what the player could have known.
public struct DecisionView: Sendable {
    public let hero: Int
    public let holeCards: [Card]
    public let board: [Card]
    public let street: Street
    public let pot: Int
    public let legal: LegalActions
    public let seats: [SeatView]
    public let actions: [ActionRecord]
    public let level: BlindLevel
    public let buttonSeat: Int
    public let position: Position
    /// How betting works at this table. Fixed-limit is a different game to play and to coach —
    /// one bet size, a cap on raises — so anyone reading this view can ask.
    public let structure: BettingStructure

    // MARK: Convenience

    public var bigBlind: Int { level.bigBlind }
    public var toCall: Int { legal.callCost }
    public var heroSeat: SeatView { seats.first { $0.seat == hero }! }
    public var heroStack: Int { heroSeat.stack }

    /// Opponents still contesting the pot.
    public var opponents: [SeatView] { seats.filter { $0.seat != hero && $0.isInHand } }
    public var opponentCount: Int { opponents.count }
    public var isHeadsUp: Bool { opponentCount == 1 }
    /// How many players were dealt into this hand. Ranges depend on it heavily.
    public var playersDealtIn: Int { seats.filter { $0.status != .sittingOut }.count }

    /// The most the hero can lose in this hand: their own stack, or the deepest opponent's if
    /// that is smaller, counting what is already committed.
    ///
    /// Deliberately the DEEPEST opponent, not the shortest: in a multiway pot the hero's whole
    /// stack is at risk against the biggest one still in. Every tournament bar was swept on this
    /// number, so it does not move. A question about ONE opponent's money — what a raiser can pay
    /// off, what a bettor is playing for — is `effectiveStack(against:)`.
    public var effectiveStack: Int {
        let heroTotal = heroSeat.stack + heroSeat.committedThisRound
        let deepest = opponents.map { $0.stack + $0.committedThisRound }.max() ?? heroTotal
        return min(heroTotal, deepest)
    }

    /// The most that can pass between the hero and one particular seat: the smaller of the two
    /// stacks, counting what each has already committed.
    public func effectiveStack(against seat: Int) -> Int {
        let heroTotal = heroSeat.stack + heroSeat.committedThisRound
        guard let other = seats.first(where: { $0.seat == seat }) else { return heroTotal }
        return min(heroTotal, other.stack + other.committedThisRound)
    }

    /// Opponents still in the hand who have not yet spoken on this street — the people who could
    /// still raise behind a call.
    public var playersStillToAct: Int {
        let spoken = Set(actionsThisStreet.map(\.seat))
        return opponents.filter { !spoken.contains($0.seat) }.count
    }

    /// Effective stack measured in big blinds — the number that decides whether a tournament
    /// hand is a normal raise-or-fold spot or a push-or-fold one.
    public var effectiveStackInBB: Double { Double(effectiveStack) / Double(max(1, bigBlind)) }

    /// Share of the final pot a call must win to break even.
    public var potOdds: Double {
        guard toCall > 0 else { return 0 }
        return Double(toCall) / Double(pot + toCall)
    }

    /// Stack-to-pot ratio: how much room is left to maneuver relative to what is already in.
    public var stackToPotRatio: Double {
        guard pot > 0 else { return .infinity }
        return Double(effectiveStack) / Double(pot)
    }

    public var actionsThisStreet: [ActionRecord] { actions.filter { $0.street == street && !$0.isForced } }

    /// Voluntary raises made before the flop: 1 means someone opened, 2 means a 3-bet, and so on.
    public var preflopRaiseCount: Int {
        actions.filter { $0.street == .preflop && !$0.isForced }.filter {
            if case .raise = $0.action { return true }
            if case .bet = $0.action { return true }
            return false
        }.count
    }

    /// True when nobody has voluntarily put money in yet before the flop.
    public var isUnopenedPot: Bool {
        street == .preflop && !actions.contains { !$0.isForced && $0.action != .fold }
    }

    /// The seat that made the last aggressive action on this street, if any.
    public var lastAggressor: Int? {
        actionsThisStreet.last {
            if case .raise = $0.action { return true }
            if case .bet = $0.action { return true }
            return false
        }?.seat
    }

    public var heroIsAggressor: Bool { lastAggressor == hero }
}

public extension HandEngine {
    /// The view of the table available to a given seat. Returns nil unless it is that seat's turn.
    func view(for seat: Int) -> DecisionView? {
        guard let legal = legalActions(), legal.seat == seat else { return nil }
        let positions = positions()
        let views = seats.map { s in
            SeatView(
                seat: s.seat,
                playerID: s.playerID,
                stack: s.stack,
                committedThisRound: s.committedThisRound,
                committedThisHand: s.committedThisHand,
                antePaid: s.antePaid,
                status: s.status,
                position: positions[s.seat],
                hole: s.seat == seat ? s.hole : nil        // the whole point of this type
            )
        }
        return DecisionView(
            hero: seat,
            holeCards: seats[seat].hole,
            board: board,
            street: street,
            pot: pot,
            legal: legal,
            seats: views,
            actions: actions,
            level: config.level,
            buttonSeat: config.buttonSeat,
            position: positions[seat] ?? .button,
            structure: config.structure
        )
    }

    /// The view for whoever is to act.
    var currentView: DecisionView? {
        guard let seat = actor else { return nil }
        return view(for: seat)
    }
}
