//! # Play a TricTrac Game
use crate::board::{Board, CheckerMove};
use crate::dice::Dice;
use crate::game_rules_moves::MoveRules;
use crate::game_rules_points::{PointsRules, PossibleJans, PossibleJansMethods};
use crate::player::{Color, Player, PlayerId};
// use anyhow::{Context, Result};
use log::{debug, error};

// use itertools::Itertools;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::{fmt, str};

use base64::{engine::general_purpose, Engine as _};

/// The different stages a game can be in. (not to be confused with the entire "GameState")
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Stage {
    PreGame,
    InGame,
    Ended,
}

/// The different stages a game turn can be in.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TurnStage {
    RollDice,
    RollWaiting,
    MarkPoints,
    HoldOrGoChoice,
    Move,
    MarkAdvPoints,
}

impl From<u8> for TurnStage {
    fn from(item: u8) -> Self {
        match item {
            0 => TurnStage::RollWaiting,
            1 => TurnStage::RollDice,
            2 => TurnStage::MarkPoints,
            3 => TurnStage::HoldOrGoChoice,
            4 => TurnStage::Move,
            5 => TurnStage::MarkAdvPoints,
            _ => TurnStage::RollWaiting,
        }
    }
}

impl From<TurnStage> for u8 {
    fn from(stage: TurnStage) -> u8 {
        match stage {
            TurnStage::RollWaiting => 0,
            TurnStage::RollDice => 1,
            TurnStage::MarkPoints => 2,
            TurnStage::HoldOrGoChoice => 3,
            TurnStage::Move => 4,
            TurnStage::MarkAdvPoints => 5,
        }
    }
}

/// Represents a TricTrac game
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GameState {
    pub stage: Stage,
    pub turn_stage: TurnStage,
    pub board: Board,
    pub active_player_id: PlayerId,
    pub players: HashMap<PlayerId, Player>,
    pub history: Vec<GameEvent>,
    /// last dice pair rolled
    pub dice: Dice,
    /// players points computed for the last dice pair rolled
    pub dice_points: (u8, u8),
    pub dice_moves: (CheckerMove, CheckerMove),
    pub dice_jans: PossibleJans,
    /// true if player needs to roll first
    roll_first: bool,
    // NOTE: add to a Setting struct if other fields needed
    pub schools_enabled: bool,
    pub debug_message: String,
}

// implement Display trait
impl fmt::Display for GameState {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let mut s = String::new();
        s.push_str(&format!(
            "Stage: {:?} / {:?}\n",
            self.stage, self.turn_stage
        ));
        s.push_str(&format!("Dice: {:?}\n", self.dice));
        let empty_string = String::from("");
        s.push_str(&format!(
            "Who plays: {}\n",
            self.who_plays()
                .map(|player| &player.name)
                .unwrap_or_else(|| &empty_string)
        ));
        s.push_str(&format!("Board: {:?}\n", self.board));
        // s.push_str(&format!("History: {:?}\n", self.history));
        write!(f, "{s}")
    }
}

impl Default for GameState {
    fn default() -> Self {
        Self {
            stage: Stage::PreGame,
            turn_stage: TurnStage::RollDice,
            board: Board::default(),
            active_player_id: 0,
            players: HashMap::new(),
            history: Vec::new(),
            dice: Dice::default(),
            dice_points: (0, 0),
            dice_moves: (CheckerMove::default(), CheckerMove::default()),
            dice_jans: PossibleJans::default(),
            roll_first: true,
            schools_enabled: false,
            debug_message: "".into(),
        }
    }
}
impl Hash for GameState {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.to_string_id().hash(state);
    }
}

impl GameState {
    /// Create a new default game
    pub fn new(schools_enabled: bool) -> Self {
        let mut gs = GameState::default();
        gs.set_schools_enabled(schools_enabled);
        gs
    }

    pub fn new_with_players(p1_name: &str, p2_name: &str) -> Self {
        let mut game = Self::default();
        if let Some(p1) = game.init_player(p1_name) {
            game.init_player(p2_name);
            let _ = game
                .consume(&GameEvent::BeginGame { goes_first: p1 })
                .inspect_err(|e| error!("{}", e));
        }
        game
    }

    pub fn get_debug_message(&self) -> String {
        // format!("{:?}", self.history.last())
        format!("{:?}", self.debug_message)
    }

    pub fn mirror(&self) -> GameState {
        let mirrored_active_player = if self.active_player_id == 1 { 2 } else { 1 };
        let mut mirrored_players = HashMap::new();
        if let Some(p2) = self.players.get(&2) {
            mirrored_players.insert(1, p2.mirror());
        }
        if let Some(p1) = self.players.get(&1) {
            mirrored_players.insert(2, p1.mirror());
        }
        let (move1, move2) = self.dice_moves;
        GameState {
            stage: self.stage,
            turn_stage: self.turn_stage,
            board: self.board.mirror(),
            active_player_id: mirrored_active_player,
            // active_player_id: self.active_player_id,
            players: mirrored_players,
            history: Vec::new(),
            dice: self.dice,
            dice_points: self.dice_points,
            dice_moves: (move1.mirror(), move2.mirror()),
            dice_jans: self.dice_jans.mirror(),
            roll_first: self.roll_first,
            schools_enabled: self.schools_enabled,
            debug_message: self.debug_message.clone(),
        }
    }

    fn set_schools_enabled(&mut self, schools_enabled: bool) {
        self.schools_enabled = schools_enabled;
    }

    fn get_active_player(&self) -> Option<&Player> {
        self.players.get(&self.active_player_id)
    }

    fn get_opponent_id(&self) -> Option<PlayerId> {
        self.players
            .keys()
            .copied()
            .filter(|k| k != &self.active_player_id)
            .collect::<Vec<PlayerId>>()
            .first()
            .copied()
    }

    // -------------------------------------------------------------------------
    //                        accessors
    // -------------------------------------------------------------------------

    pub fn to_vec_float(&self) -> Vec<f32> {
        self.to_vec().iter().map(|&x| x as f32).collect()
    }

    /// Get state as a tensor for neural network training (Option B, TD-Gammon style).
    /// Returns 217 f32 values, all normalized to [0, 1].
    ///
    /// Must be called from the active player's perspective: callers should mirror
    /// the GameState for Black before calling so that "own" always means White.
    ///
    /// Layout:
    ///   [0..95]    own (White) checkers: 4 values per field × 24 fields
    ///   [96..191]  opp (Black) checkers: 4 values per field × 24 fields
    ///   [192..193] dice values / 6
    ///   [194]      active player color (0=White, 1=Black)
    ///   [195]      turn_stage / 5
    ///   [196..199] White player: points/12, holes/12, can_bredouille, can_big_bredouille
    ///   [200..203] Black player: same
    ///   [204..207] own quarter filled (quarters 1-4)
    ///   [208..211] opp quarter filled (quarters 1-4)
    ///   [212]      own checkers all in exit zone (fields 19-24)
    ///   [213]      opp checkers all in exit zone (fields 1-6)
    ///   [214]      own coin de repos taken (field 12 has ≥2 own checkers)
    ///   [215]      opp coin de repos taken (field 13 has ≥2 opp checkers)
    ///   [216]      own dice_roll_count / 3, clamped to 1
    pub fn to_tensor(&self) -> Vec<f32> {
        let mut t = Vec::with_capacity(217);
        let pos: Vec<i8> = self.board.to_vec(); // 24 elements, positive=White, negative=Black

        // [0..95] own (White) checkers, TD-Gammon encoding.
        // Each field contributes 4 values:
        //   (count==1), (count==2), (count==3), (count-3)/12  ← all in [0,1]
        // The overflow term is divided by 12 because the maximum excess is
        // 15 (all checkers) − 3 = 12.
        for &c in &pos {
            let own = c.max(0) as u8;
            t.push((own == 1) as u8 as f32);
            t.push((own == 2) as u8 as f32);
            t.push((own == 3) as u8 as f32);
            t.push(own.saturating_sub(3) as f32 / 12.0);
        }

        // [96..191] opp (Black) checkers, same encoding.
        for &c in &pos {
            let opp = (-c).max(0) as u8;
            t.push((opp == 1) as u8 as f32);
            t.push((opp == 2) as u8 as f32);
            t.push((opp == 3) as u8 as f32);
            t.push(opp.saturating_sub(3) as f32 / 12.0);
        }

        // [192..193] dice
        t.push(self.dice.values.0 as f32 / 6.0);
        t.push(self.dice.values.1 as f32 / 6.0);

        // [194] active player color
        t.push(
            self.who_plays()
                .map(|p| if p.color == Color::Black { 1.0f32 } else { 0.0 })
                .unwrap_or(0.0),
        );

        // [195] turn stage
        t.push(u8::from(self.turn_stage) as f32 / 5.0);

        // [196..199] White player stats
        let wp = self.get_white_player();
        t.push(wp.map_or(0.0, |p| p.points as f32 / 12.0));
        t.push(wp.map_or(0.0, |p| p.holes as f32 / 12.0));
        t.push(wp.map_or(0.0, |p| p.can_bredouille as u8 as f32));
        t.push(wp.map_or(0.0, |p| p.can_big_bredouille as u8 as f32));

        // [200..203] Black player stats
        let bp = self.get_black_player();
        t.push(bp.map_or(0.0, |p| p.points as f32 / 12.0));
        t.push(bp.map_or(0.0, |p| p.holes as f32 / 12.0));
        t.push(bp.map_or(0.0, |p| p.can_bredouille as u8 as f32));
        t.push(bp.map_or(0.0, |p| p.can_big_bredouille as u8 as f32));

        // [204..207] own (White) quarter fill status
        for &start in &[1usize, 7, 13, 19] {
            t.push(self.board.is_quarter_filled(Color::White, start) as u8 as f32);
        }

        // [208..211] opp (Black) quarter fill status
        for &start in &[1usize, 7, 13, 19] {
            t.push(self.board.is_quarter_filled(Color::Black, start) as u8 as f32);
        }

        // [212] can_exit_own: no own checker in fields 1-18
        t.push(pos[0..18].iter().all(|&c| c <= 0) as u8 as f32);

        // [213] can_exit_opp: no opp checker in fields 7-24
        t.push(pos[6..24].iter().all(|&c| c >= 0) as u8 as f32);

        // [214] own coin de repos taken (field 12 = index 11, ≥2 own checkers)
        t.push((pos[11] >= 2) as u8 as f32);

        // [215] opp coin de repos taken (field 13 = index 12, ≥2 opp checkers)
        t.push((pos[12] <= -2) as u8 as f32);

        // [216] own dice_roll_count / 3, clamped to 1
        t.push((wp.map_or(0, |p| p.dice_roll_count) as f32 / 3.0).min(1.0));

        debug_assert_eq!(t.len(), 217, "to_tensor length mismatch");
        t
    }

    /// Get state as a vector (to be used for bot training input) :
    /// length = 36
    /// i8 for board positions with negative values for blacks
    pub fn to_vec(&self) -> Vec<i8> {
        let state_len = 36;
        let mut state = Vec::with_capacity(state_len);

        // length = 24
        state.extend(self.board.to_vec());

        // active player -> length = 1
        // white : 0 (false)
        // black : 1 (true)
        state.push(
            self.who_plays()
                .map(|player| if player.color == Color::Black { 1 } else { 0 })
                .unwrap_or(0), // White by default
        );

        // step  -> length = 1
        let turn_stage: u8 = self.turn_stage.into();
        state.push(turn_stage as i8);

        // dice roll -> length = 2
        state.push(self.dice.values.0 as i8);
        state.push(self.dice.values.1 as i8);

        // points, trous, bredouille, grande bredouille length=4 x2 joueurs = 8
        let white_player: Vec<i8> = self
            .get_white_player()
            .map(|p| p.to_vec().iter().map(|&x| x as i8).collect())
            .unwrap_or(vec![0; 10]);
        state.extend(white_player);
        let black_player: Vec<i8> = self
            .get_black_player()
            .map(|p| p.to_vec().iter().map(|&x| x as i8).collect())
            .unwrap_or(vec![0; 10]);
        state.extend(black_player);

        // ensure state has length state_len
        state.truncate(state_len);
        while state.len() < state_len {
            state.push(0);
        }
        state
    }

    /// Calculate game state id :
    pub fn to_string_id_slow(&self) -> String {
        // Pieces placement -> 77 bits (24 + 23 + 30 max)
        let mut pos_bits = self.board.to_gnupg_pos_id();

        // active player -> 1 bit
        // white : 0 (false)
        // black : 1 (true)
        pos_bits.push(
            self.who_plays()
                .map(|player| {
                    if player.color == Color::Black {
                        '1'
                    } else {
                        '0'
                    }
                })
                .unwrap_or('0'), // White by default
        );

        // step  -> 3 bits
        let step_bits = match self.turn_stage {
            TurnStage::RollWaiting => "000",
            TurnStage::RollDice => "001",
            TurnStage::MarkPoints => "010",
            TurnStage::HoldOrGoChoice => "011",
            TurnStage::Move => "100",
            TurnStage::MarkAdvPoints => "101",
        };
        pos_bits.push_str(step_bits);

        // dice roll -> 6 bits
        let dice_bits = self.dice.to_bits_string();
        pos_bits.push_str(&dice_bits);

        // points 10bits x2 joueurs = 20bits
        let white_bits = self
            .get_white_player()
            .map(|p| p.to_bits_string())
            .unwrap_or("0000000000".into());
        let black_bits = self
            .get_black_player()
            .map(|p| p.to_bits_string())
            .unwrap_or("0000000000".into());
        pos_bits.push_str(&white_bits);
        pos_bits.push_str(&black_bits);

        pos_bits = format!("{pos_bits:0<108}");
        // println!("{}", pos_bits);
        // let pos_u8 = pos_bits
        //     .as_bytes()
        //     .chunks(6)
        //     .map(|chunk| str::from_utf8(chunk).unwrap())
        //     .map(|chunk| u8::from_str_radix(chunk, 2).unwrap())
        //     .collect::<Vec<u8>>();

        let pos_u8 = pos_bits
            .as_bytes()
            .chunks(6)
            .map(|chunk| chunk.iter().fold(0u8, |acc, &b| (acc << 1) | (b - b'0')))
            .collect::<Vec<u8>>();

        general_purpose::STANDARD.encode(pos_u8)
    }

    pub fn to_string_id(&self) -> String {
        const TOTAL_BITS: usize = 108;
        const TOTAL_BYTES: usize = TOTAL_BITS / 6; // 18 bytes

        let mut output = Vec::with_capacity(TOTAL_BYTES);

        let mut current: u8 = 0;
        let mut bit_count: u8 = 0;

        // helper to push a single bit
        let push_bit = |bit: u8, output: &mut Vec<u8>, current: &mut u8, bit_count: &mut u8| {
            *current = (*current << 1) | (bit & 1);
            *bit_count += 1;

            if *bit_count == 6 {
                output.push(*current);
                *current = 0;
                *bit_count = 0;
            }
        };

        // helper to push a string of '0'/'1'
        let push_bits_str =
            |bits: &str, output: &mut Vec<u8>, current: &mut u8, bit_count: &mut u8| {
                for b in bits.bytes() {
                    push_bit(b - b'0', output, current, bit_count);
                }
            };

        // --------------------------------------------------
        // 1️⃣ Board position bits
        // --------------------------------------------------
        push_bits_str(
            &self.board.to_gnupg_pos_id(),
            &mut output,
            &mut current,
            &mut bit_count,
        );

        // --------------------------------------------------
        // 2️⃣ Active player (1 bit)
        // --------------------------------------------------
        let active_bit = self
            .who_plays()
            .map(|player| (player.color == Color::Black) as u8)
            .unwrap_or(0);

        push_bit(active_bit, &mut output, &mut current, &mut bit_count);

        // --------------------------------------------------
        // 3️⃣ Turn stage (3 bits)
        // --------------------------------------------------
        let stage_bits: u8 = match self.turn_stage {
            TurnStage::RollWaiting => 0b000,
            TurnStage::RollDice => 0b001,
            TurnStage::MarkPoints => 0b010,
            TurnStage::HoldOrGoChoice => 0b011,
            TurnStage::Move => 0b100,
            TurnStage::MarkAdvPoints => 0b101,
        };

        for i in (0..3).rev() {
            push_bit(
                (stage_bits >> i) & 1,
                &mut output,
                &mut current,
                &mut bit_count,
            );
        }

        // --------------------------------------------------
        // 4️⃣ Dice (6 bits)
        // --------------------------------------------------
        push_bits_str(
            &self.dice.to_bits_string(),
            &mut output,
            &mut current,
            &mut bit_count,
        );

        // --------------------------------------------------
        // 5️⃣ Players points (10 bits each)
        // --------------------------------------------------
        let white_bits = self
            .get_white_player()
            .map(|p| p.to_bits_string())
            .unwrap_or_else(|| "0000000000".to_string());

        let black_bits = self
            .get_black_player()
            .map(|p| p.to_bits_string())
            .unwrap_or_else(|| "0000000000".to_string());

        push_bits_str(&white_bits, &mut output, &mut current, &mut bit_count);
        push_bits_str(&black_bits, &mut output, &mut current, &mut bit_count);

        // --------------------------------------------------
        // 6️⃣ Pad remaining bits (if needed)
        // --------------------------------------------------
        while output.len() < TOTAL_BYTES {
            push_bit(0, &mut output, &mut current, &mut bit_count);
        }

        base64::engine::general_purpose::STANDARD.encode(output)
    }

    pub fn from_string_id(id: &str) -> Result<Self, String> {
        let bytes = general_purpose::STANDARD
            .decode(id)
            .map_err(|e| e.to_string())?;

        let bits_str: String = bytes.iter().map(|byte| format!("{:06b}", byte)).collect();

        // The original string was padded to 108 bits.
        let bits = if bits_str.len() >= 108 {
            &bits_str[..108]
        } else {
            return Err("Invalid decoded string length".to_string());
        };

        let board_bits = &bits[0..77];
        let board = Board::from_gnupg_pos_id(board_bits)?;

        let Some(active_player_bit) = bits.chars().nth(77) else {
            return Err("No bit at 77th position".to_string());
        };
        let active_player_color = if active_player_bit == '1' {
            Color::Black
        } else {
            Color::White
        };

        let turn_stage_bits = &bits[78..81];
        let turn_stage = match turn_stage_bits {
            "000" => TurnStage::RollWaiting,
            "001" => TurnStage::RollDice,
            "010" => TurnStage::MarkPoints,
            "011" => TurnStage::HoldOrGoChoice,
            "100" => TurnStage::Move,
            "101" => TurnStage::MarkAdvPoints,
            _ => return Err(format!("Invalid bits for turn stage : {turn_stage_bits}")),
        };

        let dice_bits = &bits[81..87];
        let dice = Dice::from_bits_string(dice_bits).map_err(|e| e.to_string())?;

        let white_player_bits = &bits[87..97];
        let black_player_bits = &bits[97..107];

        let white_player =
            Player::from_bits_string(white_player_bits, "Player 1".to_string(), Color::White)
                .map_err(|e| e.to_string())?;
        let black_player =
            Player::from_bits_string(black_player_bits, "Player 2".to_string(), Color::Black)
                .map_err(|e| e.to_string())?;

        let mut players = HashMap::new();
        players.insert(1, white_player);
        players.insert(2, black_player);

        let active_player_id = if active_player_color == Color::White {
            1
        } else {
            2
        };

        // Some fields are not in the ID, so we use defaults.
        Ok(GameState {
            stage: Stage::InGame, // Assume InGame from ID
            turn_stage,
            board,
            active_player_id,
            players,
            history: Vec::new(),
            dice,
            dice_points: (0, 0),
            dice_moves: (CheckerMove::default(), CheckerMove::default()),
            dice_jans: PossibleJans::default(),
            roll_first: false,        // Assume not first roll
            schools_enabled: false,   // Assume disabled
            debug_message: "".into(), // Assume disabled
        })
    }

    pub fn who_plays(&self) -> Option<&Player> {
        self.get_active_player()
    }

    pub fn get_white_player(&self) -> Option<&Player> {
        self.players
            .iter()
            .filter(|(_id, player)| player.color == Color::White)
            .map(|(_id, player)| player)
            .next()
    }

    pub fn get_black_player(&self) -> Option<&Player> {
        self.players
            .iter()
            .filter(|(_id, player)| player.color == Color::Black)
            .map(|(_id, player)| player)
            .next()
    }

    pub fn player_id_by_color(&self, color: Color) -> Option<&PlayerId> {
        self.players
            .iter()
            .filter(|(_id, player)| player.color == color)
            .map(|(id, _player)| id)
            .next()
    }

    pub fn player_id(&self, player: &Player) -> Option<&PlayerId> {
        self.players
            .iter()
            .filter(|(_id, candidate)| player.color == candidate.color)
            .map(|(id, _candidate)| id)
            .next()
    }

    pub fn player_color_by_id(&self, player_id: &PlayerId) -> Option<Color> {
        self.players
            .iter()
            .filter(|(id, _)| *id == player_id)
            .map(|(_, player)| player.color)
            .next()
    }

    // ----------------------------------------------------------------------------------
    //                          Rules checks
    // ----------------------------------------------------------------------------------

    /// Determines whether an event is valid considering the current GameState
    pub fn validate(&self, event: &GameEvent) -> bool {
        use GameEvent::*;
        match event {
            BeginGame { goes_first } => {
                // Check that the player supposed to go first exists
                if !self.players.contains_key(goes_first) {
                    return false;
                }

                // Check that the game hasn't started yet. (we don't want to double start a game)
                if self.stage != Stage::PreGame {
                    return false;
                }
            }
            EndGame { reason } => {
                if let EndGameReason::PlayerWon { winner: _ } = reason {
                    // Check that the game has started before someone wins it
                    if self.stage != Stage::InGame {
                        return false;
                    }
                }
            }
            PlayerJoined { player_id, name: _ } => {
                // Check that there isn't another player with the same id
                if self.players.contains_key(player_id) {
                    return false;
                }
            }
            PlayerDisconnected { player_id } => {
                // Check player exists
                if !self.players.contains_key(player_id) {
                    return false;
                }
            }
            Roll { player_id } => {
                // Check player exists
                if !self.players.contains_key(player_id) {
                    error!("unknown player_id");
                    return false;
                }
                // Check player is currently the one making their move
                if self.active_player_id != *player_id {
                    error!("not active player_id");
                    return false;
                }
                // Check the turn stage
                if self.turn_stage != TurnStage::RollDice {
                    error!("bad stage {:?}", self.turn_stage);
                    return false;
                }
            }
            RollResult { player_id, dice: _ } => {
                // Check player exists
                if !self.players.contains_key(player_id) {
                    return false;
                }
                // Check player is currently the one making their move
                if self.active_player_id != *player_id {
                    return false;
                }
                // Check the turn stage
                if self.turn_stage != TurnStage::RollWaiting {
                    error!("bad stage {:?}", self.turn_stage);
                    return false;
                }
            }
            Mark {
                player_id,
                points: _,
            } => {
                // Check player exists
                if !self.players.contains_key(player_id) {
                    return false;
                }
                // Check player is currently the one making their move
                if self.active_player_id != *player_id {
                    return false;
                }

                // Check points are correct
                // let (board, moves) = if *color == Color::Black {
                //     (board.mirror(), (moves.0.mirror(), moves.1.mirror()))
                // } else {
                //     (board.clone(), *moves)
                // };
                // let rules_points: u8 = self.get_points().iter().map(|r| r.0).sum();
                // if rules_points != *points {
                //     return false;
                // }
            }
            Go { player_id } => {
                if !self.players.contains_key(player_id) {
                    error!("Player {player_id} unknown");
                    return false;
                }
                // Check player is currently the one making their move
                if self.active_player_id != *player_id {
                    error!("Player not active : {}", self.active_player_id);
                    return false;
                }
                // Check the player can leave (ie the game is in the KeepOrLeaveChoice stage)
                if self.turn_stage != TurnStage::HoldOrGoChoice {
                    error!("bad stage {:?}", self.turn_stage);
                    error!(
                        "black player points : {:?}",
                        self.get_black_player()
                            .map(|player| (player.points, player.holes))
                    );
                    // error!("history {:?}", self.history);
                    return false;
                }
            }
            Move { player_id, moves } => {
                // Check player exists
                if !self.players.contains_key(player_id) {
                    error!("Player {player_id} unknown");
                    return false;
                }
                // Check player is currently the one making their move
                if self.active_player_id != *player_id {
                    error!("Player not active : {}", self.active_player_id);
                    return false;
                }
                // Check the turn stage
                if self.turn_stage != TurnStage::Move
                    && self.turn_stage != TurnStage::HoldOrGoChoice
                {
                    error!("bad stage {:?}", self.turn_stage);
                    return false;
                }
                let color = &self.players[player_id].color;

                let rules = MoveRules::new(color, &self.board, self.dice);
                let moves = if *color == Color::Black {
                    (moves.0.mirror(), moves.1.mirror())
                } else {
                    *moves
                };
                if !rules.moves_follow_rules(&moves) {
                    // println!(">>> rules not followed ");
                    error!("rules not followed ");
                    return false;
                }
            }
            PlayError => {
                return true;
            }
        }

        // We couldn't find anything wrong with the event so it must be good
        true
    }

    // ----------------------------------------------------------------------------------
    //                   State updates
    // ----------------------------------------------------------------------------------

    pub fn init_player(&mut self, player_name: &str) -> Option<PlayerId> {
        if self.players.len() > 2 {
            // println!("more than two players");
            return None;
        }

        let player_id = self.players.len() + 1;
        let color = if player_id == 1 {
            Color::White
        } else {
            Color::Black
        };
        let player = Player::new(player_name.into(), color);
        self.players.insert(player_id as PlayerId, player);
        Some(player_id as PlayerId)
    }

    #[cfg(test)]
    fn add_player(&mut self, player_id: PlayerId, player: Player) {
        self.players.insert(player_id, player);
    }

    pub fn switch_active_player(&mut self) {
        let other_player_id = self
            .players
            .iter()
            .filter(|(id, _player)| **id != self.active_player_id)
            .map(|(id, _player)| *id)
            .next();
        self.active_player_id = other_player_id.unwrap_or(0);
    }

    /// Consumes an event, modifying the GameState and adding the event to its history
    /// NOTE: consume assumes the event to have already been validated and will accept *any* event passed to it
    pub fn consume(&mut self, valid_event: &GameEvent) -> Result<(), String> {
        use GameEvent::*;
        match valid_event {
            BeginGame { goes_first } => {
                self.active_player_id = *goes_first;
                self.stage = Stage::InGame;
                self.turn_stage = TurnStage::RollDice;
            }
            EndGame { reason: _ } => {
                self.stage = Stage::Ended;
            }
            PlayerJoined { player_id, name } => {
                let color = if !self.players.is_empty() {
                    Color::White
                } else {
                    Color::Black
                };
                self.players.insert(
                    *player_id,
                    Player {
                        name: name.to_string(),
                        color,
                        holes: 0,
                        points: 0,
                        can_bredouille: true,
                        can_big_bredouille: true,
                        dice_roll_count: 0,
                    },
                );
            }
            PlayerDisconnected { player_id } => {
                self.players.remove(player_id);
            }
            Roll { player_id: _ } => {
                self.turn_stage = TurnStage::RollWaiting;
            }
            RollResult { player_id: _, dice } => {
                self.dice = *dice;
                self.inc_roll_count(self.active_player_id);
                self.turn_stage = TurnStage::MarkPoints;
                (self.dice_jans, self.dice_points) = self.get_rollresult_jans(dice)?;
                debug!("points from result : {:?}", self.dice_points);
                if !self.schools_enabled {
                    // Schools are not enabled. We mark points automatically
                    // the points earned by the opponent will be marked on its turn
                    let new_hole = self.mark_points(self.active_player_id, self.dice_points.0);
                    if new_hole {
                        let Some(holes_count) = self.get_active_player().map(|p| p.holes) else {
                            return Err("No active player".into());
                        };
                        debug!("new hole  -> {holes_count:?}");
                        if holes_count >= 12 {
                            self.stage = Stage::Ended;
                        } else {
                            self.turn_stage = TurnStage::HoldOrGoChoice;
                        }
                    } else {
                        self.turn_stage = TurnStage::Move;
                    }
                }
            }
            Mark { player_id, points } => {
                if self.schools_enabled {
                    let new_hole = self.mark_points(*player_id, *points);
                    if new_hole {
                        let Some(holes) = self.get_active_player().map(|p| p.holes) else {
                            return Err("No active player".into());
                        };
                        if holes >= 12 {
                            self.stage = Stage::Ended;
                        } else {
                            self.turn_stage = if self.turn_stage == TurnStage::MarkAdvPoints {
                                TurnStage::RollDice
                            } else {
                                TurnStage::HoldOrGoChoice
                            };
                        }
                    } else {
                        self.turn_stage = if self.turn_stage == TurnStage::MarkAdvPoints {
                            TurnStage::RollDice
                        } else {
                            TurnStage::Move
                        };
                    }
                }
            }
            Go { player_id: _ } => self.new_pick_up(),
            Move { player_id, moves } => {
                let Some(player) = self.players.get(player_id) else {
                    return Err("unknown player {player_id}".into());
                };
                self.board
                    .move_checker(&player.color, moves.0)
                    .map_err(|e| e.to_string())?;
                self.board
                    .move_checker(&player.color, moves.1)
                    .map_err(|e| e.to_string())?;
                self.dice_moves = *moves;
                let Some(active_player_id) = self.players.keys().find(|id| *id != player_id) else {
                    return Err("Can't find player id {id}".into());
                };
                self.active_player_id = *active_player_id;
                self.turn_stage = if self.schools_enabled {
                    TurnStage::MarkAdvPoints
                } else {
                    // The player has moved, we can mark its opponent's points (which is now the current player)
                    let new_hole = self.mark_points(self.active_player_id, self.dice_points.1);
                    if new_hole && self.get_active_player().map(|p| p.holes).unwrap_or(0) >= 12 {
                        self.stage = Stage::Ended;
                    }
                    TurnStage::RollDice
                };
            }
            PlayError => {}
        }
        self.history.push(valid_event.clone());
        Ok(())
    }

    /// Set a new pick up ('relevé') after a player won a hole and choose to 'go',
    /// or after a player has bore off (took of his men off the board)
    fn new_pick_up(&mut self) {
        self.players.iter_mut().for_each(|(_id, p)| {
            // reset points
            p.points = 0;
            // reset dice_roll_count
            p.dice_roll_count = 0;
            // reset bredouille
            p.can_bredouille = true;
            // XXX : switch colors
            // désactivé pour le moment car la vérification des mouvements échoue, cf. https://code.rhumbs.fr/henri/trictrac/issues/31
            // p.color = p.color.opponent_color();
        });
        // joueur actif = joueur ayant sorti ses dames ou est parti (donc deux jeux successifs)
        self.turn_stage = TurnStage::RollDice;
        // reset board
        self.board = Board::new();
    }

    fn get_rollresult_jans(&self, dice: &Dice) -> Result<(PossibleJans, (u8, u8)), String> {
        let Some(player) = &self.players.get(&self.active_player_id) else {
            return Err("No active player".into());
        };
        debug!(
            "get rollresult for {:?} {:?} {:?} (roll count {:?})",
            player.color, self.board, dice, player.dice_roll_count
        );
        let points_rules = PointsRules::new(&player.color, &self.board, *dice);
        let (jans, points) = points_rules.get_result_jans(player.dice_roll_count);
        Ok(if player.color == Color::White {
            (jans, points)
        } else {
            (jans.mirror(), points)
        })
    }

    /// Determines if someone has won the game
    pub fn determine_winner(&self) -> Option<PlayerId> {
        // A player has won if he has got 12 holes
        self.players
            .iter()
            .filter(|(_, p)| p.holes > 11)
            .map(|(id, _)| *id)
            .next()
    }

    fn inc_roll_count(&mut self, player_id: PlayerId) {
        self.players.get_mut(&player_id).map(|p| {
            p.dice_roll_count = p.dice_roll_count.saturating_add(1);
            p
        });
    }

    pub fn mark_points_for_bot_training(&mut self, player_id: PlayerId, points: u8) -> bool {
        self.mark_points(player_id, points)
    }

    /// Total accumulated score for a player: `holes × 12 + points`.
    ///
    /// Returns `0` if `player_id` is not found (e.g. before `init_player`).
    pub fn total_score(&self, player_id: PlayerId) -> i32 {
        self.players
            .get(&player_id)
            .map(|p| p.holes as i32 * 12 + p.points as i32)
            .unwrap_or(0)
    }

    fn mark_points(&mut self, player_id: PlayerId, points: u8) -> bool {
        // Update player points and holes
        let mut new_hole = false;
        self.players.get_mut(&player_id).map(|p| {
            let sum_points = p.points + points;
            let jeux = sum_points / 12;
            let holes = match (jeux, p.can_bredouille) {
                (0, _) => 0,
                (_, false) => 2 * jeux - 1,
                (_, true) => 2 * jeux,
            };

            new_hole = holes > 0;
            if new_hole {
                p.can_bredouille = true;
            }
            p.points = sum_points % 12;
            p.holes += holes;

            // if points > 0 && p.holes > 15 {
            if points > 0 {
                debug!(
                    "player {player_id:?}  holes : {:?} (+{holes:?}) points : {:?} (+{points:?} - {jeux:?})",
                    p.holes, p.points
                )
            }
            p
        });

        // Opponent updates
        let maybe_op = if player_id == self.active_player_id {
            self.get_opponent_id()
        } else {
            Some(player_id)
        };
        if let Some(opp_id) = maybe_op {
            if points > 0 {
                self.players.get_mut(&opp_id).map(|opponent| {
                    // Cancel opponent bredouille
                    opponent.can_bredouille = false;
                    // Reset opponent points if the player finished a hole
                    if new_hole {
                        opponent.points = 0;
                        opponent.can_bredouille = true;
                    }
                    opponent
                });
            }
        }

        new_hole
    }
}

/// The reasons why a game could end
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Deserialize)]
pub enum EndGameReason {
    PlayerLeft { player_id: PlayerId },
    PlayerWon { winner: PlayerId },
}

/// An event that progresses the GameState forward
#[derive(Debug, Clone, Serialize, PartialEq, Eq, Deserialize)]
pub enum GameEvent {
    BeginGame {
        goes_first: PlayerId,
    },
    EndGame {
        reason: EndGameReason,
    },
    PlayerJoined {
        player_id: PlayerId,
        name: String,
    },
    PlayerDisconnected {
        player_id: PlayerId,
    },
    Roll {
        player_id: PlayerId,
    },
    RollResult {
        player_id: PlayerId,
        dice: Dice,
    },
    Mark {
        player_id: PlayerId,
        points: u8,
    },
    Go {
        player_id: PlayerId,
    },
    Move {
        player_id: PlayerId,
        moves: (CheckerMove, CheckerMove),
    },
    PlayError,
}

impl GameEvent {
    pub fn player_id(&self) -> Option<PlayerId> {
        match self {
            Self::PlayerJoined { player_id, name: _ } => Some(*player_id),
            Self::PlayerDisconnected { player_id } => Some(*player_id),
            Self::Roll { player_id } => Some(*player_id),
            Self::RollResult { player_id, dice: _ } => Some(*player_id),
            Self::Mark {
                player_id,
                points: _,
            } => Some(*player_id),
            Self::Go { player_id } => Some(*player_id),
            Self::Move {
                player_id,
                moves: _,
            } => Some(*player_id),
            _ => None,
        }
    }

    pub fn get_mirror(&self, preserve_player: bool) -> Self {
        // let mut mirror = self.clone();
        let mirror_player_id = if let Some(player_id) = self.player_id() {
            if preserve_player {
                player_id
            } else if player_id == 1 {
                2
            } else {
                1
            }
        } else {
            0
        };

        match self {
            Self::PlayerJoined { player_id: _, name } => Self::PlayerJoined {
                player_id: mirror_player_id,
                name: name.clone(),
            },
            Self::PlayerDisconnected { player_id: _ } => GameEvent::PlayerDisconnected {
                player_id: mirror_player_id,
            },
            Self::Roll { player_id: _ } => GameEvent::Roll {
                player_id: mirror_player_id,
            },
            Self::RollResult { player_id: _, dice } => GameEvent::RollResult {
                player_id: mirror_player_id,
                dice: *dice,
            },
            Self::Mark {
                player_id: _,
                points,
            } => GameEvent::Mark {
                player_id: mirror_player_id,
                points: *points,
            },
            Self::Go { player_id: _ } => GameEvent::Go {
                player_id: mirror_player_id,
            },
            Self::Move {
                player_id: _,
                moves: (move1, move2),
            } => Self::Move {
                player_id: mirror_player_id,
                moves: (move1.mirror(), move2.mirror()),
            },
            Self::BeginGame { goes_first } => GameEvent::BeginGame {
                goes_first: (if *goes_first == 1 { 2 } else { 1 }),
            },
            Self::EndGame { reason } => GameEvent::EndGame { reason: *reason },
            Self::PlayError => GameEvent::PlayError,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn init_test_gamestate(turn: TurnStage) -> GameState {
        let mut state = GameState::default();
        state.add_player(1, Player::new("player1".into(), Color::White));
        state.add_player(2, Player::new("player2".into(), Color::Black));
        state.active_player_id = 1;
        state.turn_stage = turn;
        state
    }

    #[test]
    fn to_string_id() {
        let state = init_test_gamestate(TurnStage::RollDice);
        let string_id = state.to_string_id();
        // println!("string_id : {}", string_id);
        assert_eq!(string_id, "Pz84AAAABz8/AAAAAAgAASAG");
        let new_state = GameState::from_string_id(&string_id).unwrap();
        assert_eq!(state.board, new_state.board);
        assert_eq!(state.active_player_id, new_state.active_player_id);
        assert_eq!(state.turn_stage, new_state.turn_stage);
        assert_eq!(state.dice, new_state.dice);
        assert_eq!(
            state.get_white_player().unwrap().points,
            new_state.get_white_player().unwrap().points
        );
    }

    #[test]
    fn hold_or_go() {
        let mut game_state = init_test_gamestate(TurnStage::MarkPoints);
        game_state.schools_enabled = true;
        let pid = game_state.active_player_id;
        let _ = game_state.consume(
            &(GameEvent::Mark {
                player_id: pid,
                points: 13,
            }),
        );
        let player = game_state.get_active_player().unwrap();
        assert_eq!(player.points, 1);
        assert_eq!(player.holes, 2); // because can bredouille
        assert_eq!(game_state.turn_stage, TurnStage::HoldOrGoChoice);

        // Go
        let _ = game_state.consume(
            &(GameEvent::Go {
                player_id: game_state.active_player_id,
            }),
        );
        assert_eq!(game_state.active_player_id, pid);
        let player = game_state.get_active_player().unwrap();
        assert_eq!(player.points, 0);
        assert_eq!(game_state.turn_stage, TurnStage::RollDice);

        // Hold
        let mut game_state = init_test_gamestate(TurnStage::MarkPoints);
        game_state.schools_enabled = true;
        let pid = game_state.active_player_id;
        let _ = game_state.consume(
            &(GameEvent::Mark {
                player_id: pid,
                points: 13,
            }),
        );
        let moves = (
            CheckerMove::new(1, 3).unwrap(),
            CheckerMove::new(1, 3).unwrap(),
        );
        let _ = game_state.consume(
            &(GameEvent::Move {
                player_id: game_state.active_player_id,
                moves,
            }),
        );
        assert_ne!(game_state.active_player_id, pid);
        assert_eq!(game_state.players.get(&pid).unwrap().points, 1);
        assert_eq!(game_state.get_active_player().unwrap().points, 0);
        assert_eq!(game_state.turn_stage, TurnStage::MarkAdvPoints);
    }
}
