"""Self-play generator: jedna zdielana siet hra vsetkych 4 hracov v Round
epizodach a zbiera trajektorie pre PPO (viz rl/DESIGN.md, sekcie 4-5).

Odmena je sparse a terminalna: kazde rozhodnutie hraca v kole (tip aj vsetky
karty) dostane ako return jeho `points_summary` z konca kola, gamma = 1.

Masky sa ukladaju oddelene pre obe fazy (rozne velkosti akcneho priestoru);
`phase_play` hovori, ktora hlava/maska pre dany krok plati.
"""

from random import Random

import torch

from rl.encoding import N_GUESS_ACTIONS, N_PLAY_ACTIONS
from rl.env import PHASE_PLAY, RoundEnv
from rl.model import BridzikNet, mask_tensor, masked_categorical, obs_tensor
from rl.players import HeuristicPlayer, RandomPlayer

# Returny sa skaluju do [0, 1] (max odmena je 10+8). Bez skalovania ma value
# loss (MSE na 0-18) radovo vacsi gradient nez policy loss a cez zdielany
# trup policy ucenie prevalcuje.
REWARD_SCALE = 18.0


def _assign_seats(rng: Random, mix_random: float, mix_heuristic: float,
                  random_player, heuristic_player) -> dict:
    """Obsadenie sedadiel pre jednu epizodu: None = siet, inak skriptovany
    supper. Aspon jedno sedadlo musi hrat siet (inak niet co zbierat)."""
    seats = {}
    for seat in range(4):
        roll = rng.random()
        if roll < mix_random:
            seats[seat] = random_player
        elif roll < mix_random + mix_heuristic:
            seats[seat] = heuristic_player
        else:
            seats[seat] = None
    if not any(p is None for p in seats.values()):
        seats[rng.randrange(4)] = None
    return seats


def collect_episodes(net: BridzikNet, n_episodes: int, rng: Random,
                     round_numbers: list = None, mix_random: float = 0.0,
                     mix_heuristic: float = 0.0,
                     heuristic_samples: int = 40) -> dict:
    """Odohra `n_episodes` self-play kol a vrati batch tenzorov:

    obs (N, OBS_DIM), phase_play (N,) bool, action (N,), logp (N,), value (N,),
    ret (N,), guess_mask (N, 9), play_mask (N, 32) -- maska nepatriacej fazy je
    pre dany krok cela False a pri update sa nepouzije.
    Navyse 'mean_points': priemerne body na sietove sedadlo a kolo.

    Opponent mixing (robustnost na nie-self-play superov): s pravdepodobnostou
    `mix_random` / `mix_heuristic` hra sedadlo RandomPlayer / HeuristicPlayer
    namiesto siete. Tahy skriptovanych superov sa do batchu NEZAZNAMENAVAJU
    (nie su z trenovanej policy) -- superi len obsadzuju stol.
    """
    env = RoundEnv(rng)
    random_player = RandomPlayer(rng)
    heuristic_player = HeuristicPlayer(rng, n_samples=heuristic_samples)
    mixing = mix_random > 0 or mix_heuristic > 0
    obs_l, phase_l, action_l, logp_l, value_l, ret_l = [], [], [], [], [], []
    gmask_l, pmask_l = [], []
    total_points = 0.0
    net_seat_rounds = 0

    net.eval()
    with torch.no_grad():
        for _ in range(n_episodes):
            round_number = rng.choice(round_numbers) if round_numbers else None
            decision = env.reset(round_number)
            seats = _assign_seats(rng, mix_random, mix_heuristic,
                                  random_player, heuristic_player) if mixing \
                else {seat: None for seat in range(4)}
            net_seat_rounds += sum(1 for p in seats.values() if p is None)
            # indexy krokov sietovych sedadiel -- na priradenie returnu
            player_steps = {p: [] for p in range(4) if seats[p] is None}
            while True:
                opponent = seats[decision.player]
                if opponent is not None:
                    # skriptovany supper: vykonaj tah, nic nezaznamenavaj
                    if decision.phase == PHASE_PLAY:
                        action_i = opponent.play(env.round, decision.player)
                    else:
                        action_i = opponent.guess(env.round, decision.player)
                else:
                    obs = obs_tensor(decision.obs).unsqueeze(0)
                    mask = mask_tensor(decision.mask).unsqueeze(0)
                    guess_logits, play_logits, value = net(obs)
                    is_play = decision.phase == PHASE_PLAY
                    dist = masked_categorical(
                        play_logits if is_play else guess_logits, mask
                    )
                    action = dist.sample()
                    action_i = action.item()

                    player_steps[decision.player].append(len(obs_l))
                    obs_l.append(decision.obs)
                    phase_l.append(is_play)
                    action_l.append(action_i)
                    logp_l.append(dist.log_prob(action).item())
                    value_l.append(value.item())
                    ret_l.append(0.0)  # doplni sa na konci kola
                    if is_play:
                        gmask_l.append([False] * N_GUESS_ACTIONS)
                        pmask_l.append(decision.mask)
                    else:
                        gmask_l.append(decision.mask)
                        pmask_l.append([False] * N_PLAY_ACTIONS)

                decision, rewards, done = env.step(action_i)
                if done:
                    for player, steps in player_steps.items():
                        for i in steps:
                            ret_l[i] = rewards[player] / REWARD_SCALE
                        total_points += rewards[player]
                    break

    return {
        'obs': torch.tensor(obs_l, dtype=torch.float32),
        'phase_play': torch.tensor(phase_l, dtype=torch.bool),
        'action': torch.tensor(action_l, dtype=torch.long),
        'logp': torch.tensor(logp_l, dtype=torch.float32),
        'value': torch.tensor(value_l, dtype=torch.float32),
        'ret': torch.tensor(ret_l, dtype=torch.float32),
        'guess_mask': torch.tensor(gmask_l, dtype=torch.bool),
        'play_mask': torch.tensor(pmask_l, dtype=torch.bool),
        'mean_points': total_points / max(net_seat_rounds, 1),
    }
