import Foundation
import PokerCore

/// What a hand is, relative to the board — the distinction a player actually reasons with.
/// A pair of eights is a monster on 8-3-2 and nearly worthless on A-K-Q.
public enum MadeCategory: Int, Comparable, Sendable, CaseIterable {
    case nothing = 0
    case aceHigh
    case underPair          // pocket pair below the board's top card
    case bottomPair
    case middlePair
    case topPairWeakKicker
    case topPair
    case overPair           // pocket pair above every board card
    case twoPair
    case trips              // paired board plus one in hand
    case set                // pocket pair matching a board card
    case straight
    case flush
    case fullHouse
    case monster            // quads or a straight flush

    public static func < (a: MadeCategory, b: MadeCategory) -> Bool { a.rawValue < b.rawValue }

    public var name: String {
        switch self {
        case .nothing: return "no pair"
        case .aceHigh: return "ace high"
        case .underPair: return "an underpair"
        case .bottomPair: return "bottom pair"
        case .middlePair: return "middle pair"
        case .topPairWeakKicker: return "top pair, weak kicker"
        case .topPair: return "top pair"
        case .overPair: return "an overpair"
        case .twoPair: return "two pair"
        case .trips: return "trips"
        case .set: return "a set"
        case .straight: return "a straight"
        case .flush: return "a flush"
        case .fullHouse: return "a full house"
        case .monster: return "the near nuts"
        }
    }
}

public enum DrawKind: Sendable, Equatable {
    case none
    case backdoor
    case gutshot
    case overcards
    case openEnded
    case flushDraw
    case combo          // a flush draw plus a straight draw

    public var name: String {
        switch self {
        case .none: return "no draw"
        case .backdoor: return "a backdoor draw"
        case .gutshot: return "a gutshot"
        case .overcards: return "two overcards"
        case .openEnded: return "an open-ended straight draw"
        case .flushDraw: return "a flush draw"
        case .combo: return "a combination draw"
        }
    }
}

/// How dangerous the board is, which decides how much a made hand is really worth.
public struct BoardTexture: Sendable {
    public let isPaired: Bool
    public let flushPossible: Bool      // three or more of a suit out there
    public let flushDrawPossible: Bool  // exactly two of a suit
    public let straightPossible: Bool
    public let highCard: Rank?
    public let connectedness: Double    // 0 for rainbow rags, 1 for a coordinated mess

    public init(board: [Card]) {
        guard !board.isEmpty else {
            isPaired = false; flushPossible = false; flushDrawPossible = false
            straightPossible = false; highCard = nil; connectedness = 0
            return
        }
        let ranks = board.map(\.rank)
        var rankCounts: [Rank: Int] = [:]
        for r in ranks { rankCounts[r, default: 0] += 1 }
        isPaired = rankCounts.values.contains { $0 >= 2 }

        var suitCounts: [Suit: Int] = [:]
        for c in board { suitCounts[c.suit, default: 0] += 1 }
        let topSuit = suitCounts.values.max() ?? 0
        flushPossible = topSuit >= 3
        flushDrawPossible = topSuit == 2 && board.count <= 4

        var mask: UInt16 = 0
        for r in ranks { mask |= r.bit }
        // Any two cards that would complete a straight on this board.
        var straight = false
        for a in Rank.allCases {
            for b in Rank.allCases where b >= a {
                if HandEvaluator.straightHigh(mask | a.bit | b.bit) != nil { straight = true; break }
            }
            if straight { break }
        }
        straightPossible = straight
        highCard = ranks.max()

        let spread = Int((ranks.max()?.rawValue ?? 0)) - Int((ranks.min()?.rawValue ?? 0))
        let tight = 1.0 - min(1.0, Double(spread) / 12.0)
        connectedness = ((flushPossible ? 0.4 : (flushDrawPossible ? 0.2 : 0)) + tight * 0.4 + (isPaired ? 0.2 : 0)).clamped(to: 0...1)
    }
}

/// A complete read of a hand on a board: what it is, what it is drawing to, and a single number
/// for how often it is likely to be best right now.
public struct HandAssessment: Sendable {
    public let rank: HandRank
    public let category: MadeCategory
    public let draw: DrawKind
    public let outs: Int
    public let texture: BoardTexture
    /// Rough chance this hand is currently the best one out there, against one opponent.
    public let showdownValue: Double
    /// Extra equity from cards still to come.
    public let drawEquity: Double

    /// Showdown value plus what the draw is worth — how the hand should be valued overall.
    public var total: Double { (showdownValue + drawEquity * (1 - showdownValue)).clamped(to: 0...1) }

    /// One pair of any kind — from a pair under the board to an overpair — and nothing better.
    public var isOnePair: Bool { category >= .underPair && category <= .overPair }

    public var summary: String {
        draw == .none ? category.name : "\(category.name) with \(draw.name)"
    }
}

public enum HandReader {

    /// Reads a hand against a board. Preflop (an empty board) returns the hand's raw equity.
    public static func assess(hole: [Card], board: [Card], opponents: Int = 1) -> HandAssessment {
        let texture = BoardTexture(board: board)

        guard board.count >= 3 else {
            let equity = PreflopTable.equity(hole, opponents: opponents)
            return HandAssessment(
                rank: HandRank(value: 0), category: .nothing, draw: .none, outs: 0,
                texture: texture, showdownValue: equity, drawEquity: 0
            )
        }

        let rank = HandEvaluator.evaluate(hole + board)
        let category = categorise(hole: hole, board: board, rank: rank)
        let (draw, outs) = readDraw(hole: hole, board: board, rank: rank)

        let streetsToCome = max(0, 5 - board.count)
        // The rule of two and four: each out is worth about 2% per card still to come.
        var drawEquity = streetsToCome == 0 ? 0 : Double(outs) * 0.02 * Double(streetsToCome)
        if category >= .twoPair { drawEquity *= 0.3 }        // already strong; improving matters less
        drawEquity = drawEquity.clamped(to: 0...0.55)

        let showdown = showdownValue(category: category, texture: texture, opponents: opponents)

        return HandAssessment(
            rank: rank, category: category, draw: draw, outs: outs, texture: texture,
            showdownValue: showdown, drawEquity: drawEquity
        )
    }

    // MARK: - What the hand is

    static func categorise(hole: [Card], board: [Card], rank: HandRank) -> MadeCategory {
        let boardRanks = board.map(\.rank).sorted(by: >)
        let holeRanks = hole.map(\.rank).sorted(by: >)
        let topBoard = boardRanks.first ?? .two

        switch rank.category {
        case .straightFlush, .fourOfAKind:
            return .monster
        case .fullHouse:
            return .fullHouse
        case .flush:
            return .flush
        case .straight:
            return .straight
        case .threeOfAKind:
            // A pocket pair matching the board is a set; a paired board is trips.
            return holeRanks[0] == holeRanks[1] ? .set : .trips
        case .twoPair:
            return .twoPair
        case .pair:
            return categorisePair(holeRanks: holeRanks, boardRanks: boardRanks, topBoard: topBoard)
        case .highCard:
            return holeRanks.contains(.ace) ? .aceHigh : .nothing
        }
    }

    private static func categorisePair(holeRanks: [Rank], boardRanks: [Rank], topBoard: Rank) -> MadeCategory {
        // A pocket pair: is it above the board, or under it?
        if holeRanks[0] == holeRanks[1] {
            return holeRanks[0] > topBoard ? .overPair : .underPair
        }
        // Otherwise one hole card has paired the board. Which one?
        guard let paired = holeRanks.first(where: { boardRanks.contains($0) }) else {
            return holeRanks.contains(.ace) ? .aceHigh : .nothing
        }
        let kicker = holeRanks.first { $0 != paired } ?? .two
        if paired == topBoard {
            // Top pair. A weak kicker is the difference between a hand that can call a raise
            // and one that cannot.
            return kicker >= .jack ? .topPair : .topPairWeakKicker
        }
        if paired == boardRanks.min() { return .bottomPair }
        return .middlePair
    }

    // MARK: - What the hand is drawing to

    /// Counts the cards that actually improve *this* hand.
    ///
    /// The tempting shortcut — "any card that raises my hand category" — is wrong, because a card
    /// that pairs the board raises everyone's hand equally and helps nobody. An out has to use a
    /// hole card. Getting this right matters beyond the bots: the coach tells the player how many
    /// outs they had, and a coach that says fifteen when the answer is nine is worse than silent.
    static func readDraw(hole: [Card], board: [Card], rank: HandRank) -> (DrawKind, Int) {
        guard board.count >= 3, board.count < 5 else { return (.none, 0) }

        let seen = Set(hole + board)
        let unseen = Card.all.filter { !seen.contains($0) }
        var outCards = Set<Card>()

        // A flush draw needs four of a suit with at least one of them in hand.
        var suitCounts: [Suit: Int] = [:]
        for card in hole + board { suitCounts[card.suit, default: 0] += 1 }
        var hasFlushDraw = false
        if rank.category < .flush,
           let suit = suitCounts.first(where: { $0.value == 4 })?.key,
           hole.contains(where: { $0.suit == suit }) {
            hasFlushDraw = true
            outCards.formUnion(unseen.filter { $0.suit == suit })
        }

        // A straight draw: which ranks would complete one, and does hero need a hole card for it?
        var straightRanks = Set<Rank>()
        if rank.category < .straight {
            let boardMask = board.reduce(UInt16(0)) { $0 | $1.rank.bit }
            let fullMask = (hole + board).reduce(UInt16(0)) { $0 | $1.rank.bit }
            for candidate in Rank.allCases {
                let withCard = fullMask | candidate.bit
                guard HandEvaluator.straightHigh(withCard) != nil else { continue }
                // If the board alone plus that card already makes the straight, everyone has it.
                guard HandEvaluator.straightHigh(boardMask | candidate.bit) == nil else { continue }
                straightRanks.insert(candidate)
            }
            for rankNeeded in straightRanks {
                outCards.formUnion(unseen.filter { $0.rank == rankNeeded })
            }
        }

        // Two overcards to the board: pairing either usually wins, but only when there is
        // genuinely nothing else going on.
        let holeRanks = hole.map(\.rank)
        let boardHigh = board.map(\.rank).max() ?? .two
        let overcards = rank.category == .highCard && holeRanks.allSatisfy { $0 > boardHigh }
        if overcards && outCards.isEmpty {
            outCards.formUnion(unseen.filter { holeRanks.contains($0.rank) })
        }

        let kind: DrawKind
        if hasFlushDraw && !straightRanks.isEmpty {
            kind = .combo
        } else if hasFlushDraw {
            kind = .flushDraw
        } else if straightRanks.count >= 2 {
            kind = .openEnded
        } else if straightRanks.count == 1 {
            kind = .gutshot
        } else if overcards {
            kind = .overcards
        } else if board.count == 3 && backdoorPossible(hole: hole, board: board) {
            kind = .backdoor
        } else {
            kind = .none
        }

        return (kind, outCards.count)
    }

    private static func backdoorPossible(hole: [Card], board: [Card]) -> Bool {
        var suits: [Suit: Int] = [:]
        for c in hole + board { suits[c.suit, default: 0] += 1 }
        return suits.values.contains { $0 == 3 }
    }

    // MARK: - What the hand is worth

    /// How often a made hand is best against one opponent, before considering draws.
    /// Deliberately conservative on dangerous boards: a bare top pair on a coordinated board is a
    /// bluff-catcher, and treating it as a value hand is one of the most expensive amateur habits.
    static func showdownValue(category: MadeCategory, texture: BoardTexture, opponents: Int) -> Double {
        var base: Double
        switch category {
        case .monster: base = 0.98
        case .fullHouse: base = 0.95
        case .flush: base = 0.88
        case .straight: base = 0.84
        case .set: base = 0.86
        case .trips: base = 0.76
        case .twoPair: base = 0.74
        case .overPair: base = 0.66
        case .topPair: base = 0.61
        case .topPairWeakKicker: base = 0.52
        case .middlePair: base = 0.36
        case .bottomPair: base = 0.27
        case .underPair: base = 0.30
        case .aceHigh: base = 0.17
        case .nothing: base = 0.09
        }

        // Danger the hand does not itself hold.
        if category < .flush && texture.flushPossible { base *= 0.78 }
        if category < .straight && texture.straightPossible { base *= 0.88 }
        if category < .trips && texture.isPaired { base *= 0.90 }

        // Every extra opponent is another chance somebody has something.
        let extra = max(0, opponents - 1)
        if extra > 0 { base = pow(base, 1.0 + Double(extra) * 0.35) }

        return base.clamped(to: 0.02...0.99)
    }
}
