"""Baseline hraci pre evaluaciu a neskorsi warm-start siete (viz rl/DESIGN.md).

Spolocne rozhranie: `guess(rnd, seat) -> int` (tip 0..8) a
`play(rnd, seat) -> int` (index karty 0..31). Hrac vidi len to, co by videl
pri stole -- vlastnu ruku, tipy, dokoncene kopky a rozohranu kopku; do cudzich
ruk nesiaha.
"""

from collections import Counter
from random import Random

from bridzik import cards, Card_colors, Stash
from rl.encoding import (
    N_GUESS_ACTIONS, N_PLAY_ACTIONS,
    card_index, deduce_voids, guess_mask, legal_cards, play_mask,
)


class RandomPlayer:
    """Uniformne nahodny legalny tah -- najslabsi mozny baseline."""

    def __init__(self, rng: Random = None):
        self.rng = rng if rng is not None else Random()

    def guess(self, rnd, seat: int) -> int:
        mask = guess_mask(rnd)
        return self.rng.choice([g for g in range(N_GUESS_ACTIONS) if mask[g]])

    def play(self, rnd, seat: int) -> int:
        mask = play_mask(rnd, seat)
        return self.rng.choice([i for i in range(N_PLAY_ACTIONS) if mask[i]])


def _strength(card) -> tuple:
    """Absolutna sila karty: kazda cervena (tromf) bije kazdu necervenu."""
    return (card.color == Card_colors['HEARTS'], card.value.value)


def _current_best(stash):
    """Zatial vitazna karta rozohranej kopky (None ak sa este nevynieslo)."""
    first = stash.get_first_card() if stash is not None else None
    if first is None:
        return None
    best = first
    for card in stash.get_cards().values():
        if _beats(card, best):
            best = card
    return best


def _beats(card, best) -> bool:
    """Ci `card` prebije `best` (karta drziaca kopku; jej farba je smerodajna)."""
    if card.color == best.color:
        return card.value > best.value
    return card.color == Card_colors['HEARTS']


def simulate_tricks(hands: dict, leader: int, rng: Random) -> list:
    """Dohra kopky s nahodnou legalnou strategiou; vrati pocty vyhier hracov.

    `hands` je dict seat -> zoznam kariet (rovnako velke ruky); zoznamy sa
    spotrebuju. Vitaza kopky urcuje enginovy Stash.get_winner() -- pravidla
    sa tu neduplikuju.
    """
    tricks = [0] * 4
    for _ in range(len(hands[leader])):
        stash = Stash(leader)
        for i in range(4):
            seat = (leader + i) % 4
            card = rng.choice(legal_cards(hands[seat], stash.get_first_card()))
            hands[seat].remove(card)
            stash.add_card(seat, card)
        leader = stash.get_winner()
        tricks[leader] += 1
    return tricks


def deal_consistent(unknown: list, hand_sizes: dict, voids: dict,
                    rng: Random, max_tries: int = 20) -> dict:
    """Nahodne rozdanie neznamych kariet superom respektujuce voidy.

    Greedy priradenie po zamiesani (najviac obmedzeni hraci prvi); ak sa
    konzistentne rozdanie nepodari za `max_tries`, padne na rozdanie bez
    voidov (zriedkave, radsej mierne skreslena vzorka nez ziadna).
    Zvysok kariet ostava v odlozenej kope mimo hry.
    """
    seats = sorted(hand_sizes, key=lambda s: len(voids.get(s, ())), reverse=True)
    pool = list(unknown)
    for _ in range(max_tries):
        rng.shuffle(pool)
        remaining = list(pool)
        hands = {}
        for seat in seats:
            hand, rest, banned = [], [], voids.get(seat, set())
            for card in remaining:
                if len(hand) < hand_sizes[seat] and card.color not in banned:
                    hand.append(card)
                else:
                    rest.append(card)
            if len(hand) < hand_sizes[seat]:
                break
            hands[seat] = hand
            remaining = rest
        else:
            return hands
    rng.shuffle(pool)
    idx = 0
    hands = {}
    for seat in seats:
        hands[seat] = pool[idx:idx + hand_sizes[seat]]
        idx += hand_sizes[seat]
    return hands


def mc_guess_distribution(rnd, seat: int, n_samples: int, rng: Random) -> Counter:
    """Monte Carlo odhad rozdelenia poctu vlastnych kopiek v kole.

    Nezname karty sa v kazdej vzorke nahodne rozdelia ostatnym trom hracom
    -- kazdemu len (8 - round_number) kariet, zvysok ostava v odlozenej kope
    mimo hry (pozri DESIGN.md, pasca "discard pile"). Leader prvej kopky je
    v case tipovania neznamy (najvyssi tip), sampluje sa uniformne.
    """
    hand = rnd.player_cards[seat]
    hand_size = 8 - rnd.round_number
    unknown = [c for c in cards if c not in hand]
    counts = Counter()
    for _ in range(n_samples):
        rng.shuffle(unknown)
        sim_hands = {seat: list(hand)}
        others = [s for s in range(4) if s != seat]
        for i, other in enumerate(others):
            sim_hands[other] = unknown[i * hand_size:(i + 1) * hand_size]
        tricks = simulate_tricks(sim_hands, rng.randrange(4), rng)
        counts[tricks[seat]] += 1
    return counts


def finish_round(hands: dict, current_cards: dict, first_player: int,
                 me: int, my_card, tricks: list, rng: Random) -> list:
    """Dohra kolo od mojho tahu: dokonci rozohranu kopku (ja hram `my_card`,
    dalsi nahodne legalne) a zvysne kopky dohra nahodnou legalnou strategiou.
    `tricks` su uz vyhrane kopky (mutuje sa kopia volajuceho); vrati final."""
    stash = Stash(first_player)
    for seat, card in current_cards.items():
        stash.add_card(seat, card)
    stash.add_card(me, my_card)
    while not stash.is_completed():
        seat = stash.get_active_player()
        card = rng.choice(legal_cards(hands[seat], stash.get_first_card()))
        hands[seat].remove(card)
        stash.add_card(seat, card)
    leader = stash.get_winner()
    tricks[leader] += 1
    while hands[leader]:
        stash = Stash(leader)
        for i in range(4):
            seat = (leader + i) % 4
            card = rng.choice(legal_cards(hands[seat], stash.get_first_card()))
            hands[seat].remove(card)
            stash.add_card(seat, card)
        leader = stash.get_winner()
        tricks[leader] += 1
    return tricks


class HeuristicPlayer:
    """MC tipper + jednoducha hracia heuristika riadena vlastnym tipom."""

    def __init__(self, rng: Random = None, n_samples: int = 100):
        self.rng = rng if rng is not None else Random()
        self.n_samples = n_samples

    def guess(self, rnd, seat: int) -> int:
        counts = mc_guess_distribution(rnd, seat, self.n_samples, self.rng)
        mask = guess_mask(rnd)
        # najcastejsi LEGALNY pocet kopiek (mod rozdelenia, nie priemer --
        # boduje sa len presna zhoda); pri nule vzoriek pre legalny tip
        # rozhodne blizkost k celkovemu modu
        mode = counts.most_common(1)[0][0]
        legal = [g for g in range(N_GUESS_ACTIONS) if mask[g]]
        return max(legal, key=lambda g: (counts[g], -abs(g - mode)))

    def play(self, rnd, seat: int) -> int:
        hand = rnd.player_cards[seat]
        stash = rnd.get_last_stash()
        allowed = legal_cards(hand, stash.get_first_card() if stash else None)
        need = rnd.guesses[seat] - rnd.get_stashes_winner_summary()[seat]
        best = _current_best(stash)

        if best is None:
            # vynasam: chcem kopku -> najsilnejsia karta; nechcem -> najslabsia
            chosen = max(allowed, key=_strength) if need > 0 else min(allowed, key=_strength)
        else:
            winning = [c for c in allowed if _beats(c, best)]
            if need > 0 and winning:
                # ber kopku co najlacnejsie
                chosen = min(winning, key=_strength)
            elif need <= 0 and len(winning) < len(allowed):
                # kopku nechcem: zbav sa najsilnejsej neberucej karty
                chosen = max((c for c in allowed if not _beats(c, best)), key=_strength)
            elif need <= 0:
                # vsetko berie -> ber co najlacnejsie (setri silne karty netreba,
                # ale nizka karta drzi sancu, ze ma este niekto prebije)
                chosen = min(allowed, key=_strength)
            else:
                # kopku chcem, ale nic neberie -> odhod najslabsiu
                chosen = min(allowed, key=_strength)
        return card_index(chosen)


class McPlayer(HeuristicPlayer):
    """Heuristika s MC hracou fazou: kazdy kandidatsky tah sa ohodnoti
    simulaciami zvysku kola nad rozdaniami neznamych kariet konzistentnymi
    s dedukovanymi voidmi (`use_voids=False` = ablacia bez dedukcie).
    Tipovanie ostava MC tipper z HeuristicPlayer (pred prvou kartou niet
    z coho voidy dedukovat)."""

    def __init__(self, rng: Random = None, n_samples: int = 100,
                 play_samples: int = 24, use_voids: bool = True):
        super().__init__(rng, n_samples)
        self.play_samples = play_samples
        self.use_voids = use_voids

    def play(self, rnd, seat: int) -> int:
        hand = rnd.player_cards[seat]
        stash = rnd.get_last_stash()
        first_card = stash.get_first_card() if stash else None
        candidates = legal_cards(hand, first_card)
        if len(candidates) == 1:
            return card_index(candidates[0])

        target = rnd.guesses[seat]
        base_tricks = rnd.get_stashes_winner_summary()
        voids = deduce_voids(rnd) if self.use_voids else {}
        seen = set()
        played_count = Counter()
        for st in rnd.stashes:
            for other, card in st.get_cards().items():
                seen.add(card)
                played_count[other] += 1
        unknown = [c for c in cards if c not in seen and c not in hand]
        hand_size0 = 8 - rnd.round_number
        hand_sizes = {s: hand_size0 - played_count[s] for s in range(4) if s != seat}
        current_cards = stash.get_cards()

        # spolocne rozdanie pre vsetkych kandidatov (common random numbers --
        # porovnavame tahy na tych istych svetoch, mensia variancia)
        scores = {card_index(c): 0 for c in candidates}
        for _ in range(self.play_samples):
            world = deal_consistent(unknown, hand_sizes, voids, self.rng)
            for candidate in candidates:
                sim_hands = {s: list(h) for s, h in world.items()}
                sim_hands[seat] = [c for c in hand if c != candidate]
                tricks = finish_round(sim_hands, dict(current_cards),
                                      stash.first_player, seat, candidate,
                                      list(base_tricks), self.rng)
                if tricks[seat] == target:
                    scores[card_index(candidate)] += 1

        best_index = max(scores, key=scores.get)
        if scores[best_index] == 0:
            # tip uz je (takmer) nedosiahnutelny -> aspon rozumny pravidlovy tah
            return super().play(rnd, seat)
        return best_index
