use crate::casino::cashier::chips::Stack;
use crate::prelude::{PlayerState, PlayerStateCell};
use crate::util::name::Name;
use crate::{Agency, PKError};
use std::cell::Cell;
use std::fmt::{Display, Formatter};
use uuid::Uuid;

#[derive(Clone, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
pub struct Player {
    pub id: Uuid,
    pub handle: String,
    pub chips: Stack,
    pub bet: Stack,
    pub chips_in_play: Cell<usize>,
    pub state: PlayerStateCell,
}

impl Player {}

impl Player {
    #[must_use]
    pub fn new(handle: String) -> Player {
        Player {
            id: Uuid::new_v4(),
            handle,
            chips: Stack::default(),
            bet: Stack::default(),
            chips_in_play: Cell::new(0),
            state: PlayerStateCell::new(PlayerState::YetToAct),
        }
    }

    #[must_use]
    pub fn new_with_chips(handle: String, stack: usize) -> Player {
        Player {
            id: Uuid::new_v4(),
            handle,
            chips: Stack::new(stack),
            bet: Stack::default(),
            chips_in_play: Cell::new(0),
            state: PlayerStateCell::new(PlayerState::YetToAct),
        }
    }

    /// The control flow of the `bet_internal` is forgiving general to specific, but throwing errors
    /// when the specific isn't right. If you say all in, and you aren't all in, that's an error. Now
    /// the table mechanism can simply reject the bet or it can force the player to bet all it. It
    /// up to them.
    ///
    /// # Errors
    ///
    /// * `PKError::InsufficientChips` - if the player does not have enough chips to make the bet
    /// * `PKError::InvalidTableAction` - throws if the player is already all in when all in is passed..
    fn act_bet_internal(&self, bet_type: PlayerState) -> Result<usize, PKError> {
        if bet_type.amount() == 0 {
            log::warn!("InvalidAction: Player can't make a bet of zero.");
            Err(PKError::InvalidAction)
        } else if bet_type.amount() > self.total_chip_count() {
            log::warn!("InsufficientChips: Bet amount is greater than total chips.");
            Err(PKError::InsufficientChips)
        } else if !self.state.is_active() {
            log::warn!("InvalidTableAction: Player is not in hand.");
            Err(PKError::InvalidTableAction)
        } else {
            if self.bet.count() > 0 {
                log::debug!("Player has already bet {} this round.", self.bet.count());
            }
            // How many chips are there above what's already committed to the round?
            let additional_bet = bet_type.amount().saturating_sub(self.bet.count());

            // Throw an error if the result is 0, meaning they aren't betting anything.
            if additional_bet == 0 {
                log::warn!("InsufficientChips: Bet amount already placed.");
                return Err(PKError::InsufficientChips);
            }

            let bet_chips = self.chips.bet(additional_bet)?;
            self.bet.add_to(bet_chips);
            log::trace!(
                "Player {} bets {} for a total bet of {}",
                self.handle,
                additional_bet,
                self.bet.count()
            );

            if self.is_all_in() {
                log::debug!("Resetting PlayerState to AllIn");
                self.state.set(PlayerState::AllIn(self.bet.count()));
            } else {
                if matches!(bet_type, PlayerState::AllIn(_)) {
                    // If they aren't all in, throw an error.
                    log::warn!("InvalidTableAction: Player is already all in.");
                    return Err(PKError::InvalidTableAction);
                }
                self.state.set(bet_type);
            }

            self.chips_in_play.set(self.chips_in_play.get() + additional_bet);

            log::debug!("Player {} {}", self.state, self.handle);

            Ok(self.chips.count())
        }
    }

    /// ```
    /// use pkcore::prelude::*;
    ///
    /// let player = Player::new_with_chips("The Russian".to_string(), 1_000);
    ///
    /// let bet = player.act_bet(500);
    /// assert!(bet.is_ok());
    /// assert_eq!(500, bet.unwrap());
    /// assert_eq!(1_000, player.total_chip_count());
    /// assert_eq!(500, player.chips.count());
    /// assert_eq!(500, player.bet.count());
    /// assert_eq!(PlayerState::Bet(500), player.state.get());
    ///
    /// let all_in_bet = player.act_all_in();
    /// assert!(all_in_bet.is_ok());
    /// assert_eq!(PlayerState::AllIn(1_000), player.state.get());
    /// assert_eq!(0, player.chips.count());
    /// assert_eq!(1_000, player.bet.count());
    ///
    /// let another_all_in_bet = player.act_all_in();
    /// assert!(another_all_in_bet.is_err());
    /// assert_eq!(PKError::InvalidTableAction, another_all_in_bet.unwrap_err());
    /// assert_eq!(1_000, player.bet.count());
    /// ```
    /// # Errors
    ///
    /// * `PKError::InvalidTableAction` - throws if the player is already all in.
    pub fn act_all_in(&self) -> Result<usize, PKError> {
        if self.is_all_in() {
            log::warn!("InvalidTableAction: Player is already all in.");
            return Err(PKError::InvalidTableAction);
        }
        let amount = self.total_chip_count();
        let _ = self.act_bet_internal(PlayerState::AllIn(amount))?;
        Ok(amount)
    }

    /// Working with cells this way is a completely different way of coding in `Rust`. It turns
    /// your natural instinct to make everything mutable on its head. When I first coded this
    /// I made everything mutable even though I was working with a `Cell`.
    ///
    /// **UPDATE:** The original version of this code simply removed chips from the player's stack,
    /// and placed them in the best stack, erroring out of there weren't enough chips for the bet.
    ///
    /// I've updated it to reflect how play works in reality. When a player bets, and then raises
    /// in a round, they announce the total amount they are betting. So if they bet 50, and then
    /// raise to 100, they are only putting in an additional 50 chips, not 100 more.
    ///
    /// Now, the function, subtracts the amount already bet from the bet amount, and processes
    /// the bet based on the total chips committed to the pot for that round.
    ///
    /// NOTE: At the
    ///
    /// ```
    /// use pkcore::prelude::*;
    ///
    /// let player = Player::new_with_chips("The Russian".to_string(), 1_000);
    ///
    /// let first_bet = player.act_bet(50);
    /// assert!(first_bet.is_ok());
    /// assert_eq!(950, first_bet.unwrap());
    ///
    /// let second_bet = player.act_bet(100);
    /// assert!(second_bet.is_ok());
    /// assert_eq!(900, second_bet.unwrap());
    ///
    /// // The 3rd bet has the same amount as the last, so throws an error.
    /// let third_bet = player.act_bet(100);
    /// assert!(!third_bet.is_ok());
    /// assert_eq!(PKError::InsufficientChips, third_bet.unwrap_err());
    ///
    /// assert_eq!(100, player.bet.count());
    /// assert_eq!(900, player.chips.count());
    /// ```
    ///
    /// # Errors
    ///
    /// * `PKError::InsufficientChips` - if the player does not have enough chips to make the bet
    pub fn act_bet(&self, amount: usize) -> Result<usize, PKError> {
        self.act_bet_internal(PlayerState::Bet(amount))
    }

    /// Posts a forced blind bet of `amount`.
    ///
    /// If the player's total chip count is less than `amount`, they are posted
    /// all-in for their remaining stack (short blind rule).
    ///
    /// # Errors
    ///
    /// * `PKError::InvalidTableAction` - if the player is not active.
    ///
    /// # Examples
    ///
    /// ```
    /// use pkcore::prelude::*;
    ///
    /// // Full blind.
    /// let p = Player::new_with_chips("Frank".to_string(), 1_000);
    /// p.act_bet_blind(100).unwrap();
    /// assert_eq!(PlayerState::Blind(100), p.state.get());
    ///
    /// // Short blind — player goes all-in for their remaining stack.
    /// let p = Player::new_with_chips("Short".to_string(), 20);
    /// p.act_bet_blind(50).unwrap();
    /// assert_eq!(PlayerState::AllIn(20), p.state.get());
    /// ```
    pub fn act_bet_blind(&self, amount: usize) -> Result<usize, PKError> {
        if self.total_chip_count() < amount {
            return self.act_all_in();
        }
        self.act_bet_internal(PlayerState::Blind(amount))
    }

    /// Posts a forced blind, going all-in for the remaining stack when chips are
    /// insufficient to cover the full required amount.
    ///
    /// On success returns the amount actually posted.
    ///
    /// # Errors
    ///
    /// * `PKError::InsufficientChips` if the player has zero chips.
    pub fn act_blind_or_all_in(&self, required_amount: usize) -> Result<usize, PKError> {
        let actual = required_amount.min(self.total_chip_count());
        if actual == 0 {
            return Err(PKError::InsufficientChips);
        }
        self.act_bet_internal(PlayerState::Blind(actual))?;
        Ok(actual)
    }

    /// Removes and returns the chips from the player's bet stack and sets their state to `YetToAct`.
    ///
    /// # Errors
    ///
    /// * `PKError::InvalidTableAction` - throws if the player is not active in the hand.
    pub fn act_bring_it_in(&self) -> Result<Stack, PKError> {
        let player_bet = self.bet.take();

        if self.state.is_active() && self.chips.count() > 0 {
            self.state.set(PlayerState::YetToAct);
        }

        log::trace!("{} brings in {} chips", self.handle, player_bet);

        Ok(player_bet)
    }

    /// Removes and returns the chips from the player's bet stack, but doesn't change their state,
    /// since there is no further action possible in the hand.
    ///
    /// # Errors
    ///
    /// * `PKError::InvalidTableAction` - throws if the player is not active in the hand.
    pub fn act_bring_it_in_frozen(&self) -> Result<Stack, PKError> {
        let player_bet = self.bet.take();
        log::trace!("{} brings in {} chips", self.handle, player_bet);
        Ok(player_bet)
    }

    /// ```
    /// use pkcore::prelude::*;
    ///
    /// let player = Player::new_with_chips("The Russian".to_string(), 1_000);
    ///
    /// let call = player.act_call(500);
    ///
    /// assert!(call.is_ok());
    /// assert_eq!(500, call.unwrap());
    /// assert_eq!(1_000, player.total_chip_count());
    /// assert_eq!(500, player.chips.count());
    /// assert_eq!(500, player.bet.count());
    /// assert_eq!(PlayerState::Call(500), player.state.get());
    /// ```
    ///
    /// # Errors
    ///
    ///  * `PKError::InsufficientChips` - if the player does not have enough chips to make the bet
    ///  * `PKError::InvalidTableAction` - throws if the player is not able to call.
    pub fn act_call(&self, amount: usize) -> Result<usize, PKError> {
        self.act_bet_internal(PlayerState::Call(amount))
    }

    /// NOTE: When to switch to `pub(crate)`
    pub(crate) fn act_close_it_out(&self) -> Result<Stack, PKError> {
        if self.state.is_active() {
            let state = PlayerState::Showdown(self.chips_in_play.get());
            self.state.set(state);
            log::trace!(
                "{} closing out the hand with {} chips in the hand",
                self.handle,
                self.chips_in_play.get()
            );
        } else {
            log::warn!("InvalidTableAction: Player is not active in the hand and cannot close it out.");
            return Err(PKError::InvalidTableAction);
        }

        let player_bet = self.bet.take();
        log::trace!("{} brings in {} chips", self.handle, player_bet);

        Ok(player_bet)
    }

    /// ```
    /// use pkcore::prelude::*;
    ///
    /// let player = Player::new_with_chips("The Russian".to_string(), 1_000);
    ///
    /// // Check
    /// let check = player.act_check();
    /// assert_eq!(PlayerState::Check, player.state.get());
    ///
    /// let folds = player.act_fold();
    /// // Now the check should return a `PKError::InvalidTableAction`
    /// let check = player.act_check();
    /// assert!(check.is_err());
    /// assert_eq!(PKError::InvalidTableAction, check.unwrap_err());
    /// ```
    /// # Errors
    ///
    /// * `PKError::InvalidTableAction` - throws if the player is already all in.
    pub fn act_check(&self) -> Result<(), PKError> {
        if !self.state.is_active() {
            log::warn!("InvalidTableAction: Player is not active and cannot check.");
            return Err(PKError::InvalidTableAction);
        }
        if self.state.set(PlayerState::Check).is_none() {
            Err(PKError::InvalidTableAction)
        } else {
            log::debug!("Player {} checks", self.handle);
            Ok(())
        }
    }

    /// ```
    /// use pkcore::prelude::*;
    ///
    /// let player = Player::new_with_chips("The Russian".to_string(), 1_000);
    ///
    /// // Bet and then fold.
    /// let bet = player.act_bet(500);
    /// let folds = player.act_fold();
    ///
    /// assert_eq!(500, player.total_chip_count());
    /// assert_eq!(500, player.chips.count());
    /// assert_eq!(0, player.bet.count());
    /// assert_eq!(PlayerState::Fold, player.state.get());
    ///
    /// // Trying to fold again should trigger a `PKError::InvalidTableAction`
    /// let folds_again = player.act_fold();
    /// assert!(folds_again.is_err());
    /// assert_eq!(PKError::InvalidTableAction, folds_again.unwrap_err());
    /// ```
    /// # Errors
    ///
    /// * `PKError::InvalidTableAction` - throws if the player is already all in.
    pub fn act_fold(&self) -> Result<Stack, PKError> {
        if !self.state.is_active() {
            log::warn!("InvalidTableAction: Player is not active and cannot fold.");
            return Err(PKError::InvalidTableAction);
        }
        self.state.set(PlayerState::Fold);
        Ok(self.bet.take())
    }

    /// ```
    /// use pkcore::prelude::*;
    ///
    /// let player = Player::new_with_chips("The Russian".to_string(), 1_000);
    ///
    /// let bet = player.act_bet(50);
    /// let raise = player.act_raise(100);
    ///
    /// assert!(bet.is_ok());
    /// assert!(raise.is_ok());
    /// assert_eq!(950, bet.unwrap());
    /// assert_eq!(900, raise.unwrap());
    /// assert_eq!(100, player.bet.count());
    /// assert_eq!(900, player.chips.count());
    /// assert_eq!(PlayerState::Raise(100), player.state.get());
    /// ```
    ///
    /// # Errors
    ///
    /// * `PKError::InsufficientChips` - if the player does not have enough chips to make the bet
    pub fn act_raise(&self, amount: usize) -> Result<usize, PKError> {
        self.act_bet_internal(PlayerState::Raise(amount))
    }

    /// The only difference between the different bets from a logic perspective is the stored state.
    ///
    /// ```
    /// use pkcore::prelude::*;
    ///
    /// let player = Player::new_with_chips("The Russian".to_string(), 1_000);
    ///
    /// let bet = player.act_bet(50);
    /// assert!(bet.is_ok());
    /// assert_eq!(950, bet.unwrap());
    ///
    /// let raise = player.act_raise(100);
    /// assert!(raise.is_ok());
    /// assert_eq!(900, raise.unwrap());
    ///
    /// let reraise = player.act_reraise(150);
    /// assert!(reraise.is_ok());
    /// assert_eq!(850, reraise.unwrap());
    ///
    /// assert_eq!(150, player.bet.count());
    /// assert_eq!(850, player.chips.count());
    /// ```
    ///
    /// # Errors
    ///
    /// * `PKError::InsufficientChips` - if the player does not have enough chips to make the bet
    pub fn act_reraise(&self, amount: usize) -> Result<usize, PKError> {
        self.act_bet_internal(PlayerState::ReRaise(amount))
    }

    pub fn is_active(&self) -> bool {
        self.state.is_active()
    }

    /// Returns `true` when the player is marked as ready for a hand.
    ///
    /// ```
    /// use pkcore::prelude::*;
    ///
    /// let player = Player::new("Bugsy".to_string());
    /// assert!(!player.is_ready());
    ///
    /// player.state.set(PlayerState::Ready);
    /// assert!(player.is_ready());
    /// ```
    pub fn is_ready(&self) -> bool {
        self.state.is_ready()
    }

    pub fn is_all_in(&self) -> bool {
        self.state.is_all_in() || (self.chips.count() == 0 && self.bet.count() > 0)
    }

    pub fn is_check(&self) -> bool {
        self.state.is_check()
    }

    /// Returns true if there is no remaining state from a particular hand still in the struct.
    pub fn is_clear(&self) -> bool {
        self.state.is_yet_to_act() && self.bet.is_empty() && self.chips_in_play.get() == 0
    }

    pub fn is_in_hand(&self) -> bool {
        self.state.is_in_hand()
    }

    pub fn is_out(&self) -> bool {
        self.state.is_out()
    }

    pub fn is_tapped_out(&self) -> bool {
        self.chips.count() == 0 && self.bet.count() == 0
    }

    pub fn get_chips_in_play(&self) -> usize {
        self.chips_in_play.get()
    }

    pub fn has_bet(&self) -> bool {
        self.bet.count() > 0
    }

    pub fn lose_bet(&self) {}

    #[must_use]
    pub fn random(stack: usize) -> Player {
        Player {
            id: Uuid::new_v4(),
            handle: Name::generate(),
            chips: Stack::new(stack),
            bet: Stack::default(),
            chips_in_play: Cell::new(0),
            state: PlayerStateCell::default(),
        }
    }

    /// Resets the player's state and chips in play for a new hand. NOTE: Does not reset chips
    /// or bet, which must be done by Bring It In.
    pub fn reset(&self) {
        self.chips_in_play.set(0);
        self.state.set(PlayerState::YetToAct);
    }

    /// Returns the total count of the player that is in play.
    #[must_use]
    pub fn total_chip_count(&self) -> usize {
        self.chips.count() + self.bet.count()
    }
}

impl Agency for Player {
    fn can_act(&self) -> bool {
        self.state.can_act()
    }

    fn can_given(&self, next: &PlayerState) -> bool {
        self.state.can_given(next)
    }

    fn can_given_against(&self, next: &PlayerState, other: &PlayerState) -> bool {
        self.state.can_given_against(next, other)
    }
}

impl Display for Player {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}: {} chips / {} in play [{}]",
            self.handle,
            self.chips,
            self.chips_in_play.get(),
            self.state
        )
    }
}
#[cfg(test)]
#[allow(non_snake_case)]
mod casino__players__player_tests {
    use super::*;

    #[test]
    fn new() {
        let player = Player::new("Elmer".to_string());

        assert_eq!("Elmer", player.handle);
        assert_eq!(0, player.chips.count());
        assert_eq!("Elmer: 0 chips / 0 in play [Yet to act]", player.to_string());
    }

    #[test]
    fn new_with_chips() {
        let player = Player::new_with_chips("Bugsy".to_string(), 1_000_002);

        assert_eq!("Bugsy", player.handle);
        assert_eq!(1_000_002, player.chips.count());
        assert_eq!("Bugsy: 1,000,002 chips / 0 in play [Yet to act]", player.to_string());
    }

    #[test]
    fn default() {
        let player = Player::default();

        println!("{player:?}");

        assert_eq!("", player.handle);
        assert_eq!(0, player.chips.count());
        assert_eq!(": 0 chips / 0 in play [Out]", player.to_string());
    }

    #[test]
    fn act_bet() {
        let player = Player::new_with_chips("The Russian".to_string(), 1_000);

        let did_bet = player.act_bet(100);

        assert!(did_bet.is_ok());
        assert_eq!(900, did_bet.unwrap());
        assert_eq!(PlayerState::Bet(100), player.state.get());
    }

    #[test]
    fn act_bring_it_in() {
        let player = Player::new_with_chips("The Russian".to_string(), 1_000);

        let _ = player.act_bet(100);
        let did_bring_it_in = player.act_bring_it_in();

        assert!(did_bring_it_in.is_ok());
        assert_eq!(Stack::new(100), did_bring_it_in.unwrap());
        assert_eq!(0, player.bet.count());
        assert!(player.bet.is_empty());
        assert_eq!(100, player.chips_in_play.get());
        assert_eq!(900, player.chips.count());
        assert_eq!(PlayerState::YetToAct, player.state.get());
    }

    #[test]
    fn act_check() {
        let player = Player::new_with_chips("The Russian".to_string(), 1_000);

        let did_blind = player.act_bet_blind(100);

        assert!(did_blind.is_ok());
        assert_eq!(900, did_blind.unwrap());
        assert_eq!(PlayerState::Blind(100), player.state.get());

        let did_check = player.act_check();

        // It's valid to check after posting a blind, as long as the blind matches the current bet.
        // Since we don't have that context at this layer, we will let the `Seats` determine that.
        assert!(did_check.is_ok());
        assert_eq!(PlayerState::Check, player.state.get());
    }

    #[test]
    fn get_chips_in_play() {
        let player = Player::new_with_chips("The Mouth".to_string(), 10_000);

        player.act_bet_blind(100).expect("Blind bet failed");
        assert_eq!(100, player.get_chips_in_play());

        player.act_bet(300).expect("Bet failed");
        assert_eq!(300, player.get_chips_in_play());

        player.act_raise(500).expect("Raise failed");
        assert_eq!(500, player.get_chips_in_play());

        player.act_reraise(1500).expect("Reraise failed");
        assert_eq!(1500, player.get_chips_in_play());

        player.act_call(2000).expect("Call failed");
        assert_eq!(2000, player.get_chips_in_play());

        player.act_bring_it_in().expect("Bring It failed");
        assert_eq!(2000, player.get_chips_in_play());

        player.act_bet(100).expect("Bet failed");
        assert_eq!(2100, player.get_chips_in_play());

        player.act_raise(300).expect("Raise failed");
        assert_eq!(2300, player.get_chips_in_play());

        player.act_reraise(500).expect("Reraise failed");
        assert_eq!(2500, player.get_chips_in_play());

        player.act_call(1000).expect("Call failed");
        assert_eq!(3000, player.get_chips_in_play());

        player.act_all_in().expect("All In failed");
        assert_eq!(10_000, player.get_chips_in_play());
    }

    #[test]
    fn is_all_in() {
        let player = Player::new_with_chips("All In Andy".to_string(), 500);
        assert!(!player.is_all_in());

        let _ = player.act_bet(500);
        assert!(player.is_all_in());
        assert_eq!(PlayerState::AllIn(500), player.state.get());
    }

    #[test]
    fn is_clear() {
        let player = Player::new_with_chips("Clear Carl".to_string(), 500);
        assert!(player.is_clear());

        let _ = player.act_bet(100);
        assert!(!player.is_clear());

        let _ = player.act_bring_it_in();
        let _ = player.reset();
        assert!(player.is_clear());

        let _ = player.state.set(PlayerState::Fold);
        assert!(!player.is_clear());

        player.reset();
        assert!(player.is_clear());
    }

    #[test]
    fn is_in_hand() {
        let player = Player::new_with_chips("All In Andy".to_string(), 500);
        assert!(player.is_in_hand());

        let _ = player.act_fold();
        assert!(!player.is_in_hand());
    }

    #[test]
    fn is_tapped_out() {
        let player = Player::new_with_chips("Tapped Out Tom".to_string(), 0);
        assert!(player.is_tapped_out());

        let player2 = Player::new_with_chips("Not Tapped Out Nancy".to_string(), 100);
        assert!(!player2.is_tapped_out());

        let _ = player2.act_bet(100);
        assert!(!player2.is_tapped_out());
    }

    #[test]
    fn is_out() {
        let player = Player::new_with_chips("Out Olivia".to_string(), 500);
        assert!(!player.is_out());

        player.state.set(PlayerState::Out);
        assert!(player.is_out());

        player.state.set(PlayerState::Fold);
        assert!(!player.is_out());
    }

    #[test]
    fn is_ready() {
        let player = Player::new_with_chips("Ready Rita".to_string(), 500);
        assert!(!player.is_ready());

        player.state.set(PlayerState::Ready);
        assert!(player.is_ready());

        player.state.set(PlayerState::Fold);
        assert!(!player.is_ready());
    }
}
