/*
    Hurry Curry! - a game about cooking
    Copyright (C) 2025 Hurry Curry! Contributors

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, version 3 of the License only.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

*/
use crate::{
    ConnectionID,
    entity::{Entities, EntityContext, construct_entity},
    interaction::{interact, tick_slot},
    random_float,
    scoreboard::ScoreboardStore,
};
use anyhow::{Context, Result};
use hurrycurry_client_lib::{Game, Involvement, Item, Player, Tile, gamedata_index::GamedataIndex};
use hurrycurry_data::{Serverdata, index::DataIndex};
use hurrycurry_locale::{
    FALLBACK_LOCALE, TrError,
    message::{COLORED, MessageDisplayExt},
    tre,
};
use hurrycurry_protocol::{
    Character, Gamedata, Hand, ItemLocation, Menu, MessageTimeout, PacketC, PacketS, PlayerClass,
    PlayerID, Score, TileIndex,
    glam::{IVec2, Vec2},
    movement::MovementBase,
};
use log::{info, warn};
use std::{
    collections::{HashMap, HashSet, VecDeque},
    path::PathBuf,
    sync::Arc,
    time::{Duration, Instant},
};
use tokio::sync::broadcast::Sender;

#[derive(Debug, Default)]
pub struct ConnectionData {
    pub players: HashSet<PlayerID>,
    pub idle: bool,
    pub ready: bool,
}

pub enum AnnounceState {
    Queued,
    Running(f32),
    Done,
}

pub struct Server {
    pub tx: Sender<PacketC>,
    pub connections: HashMap<ConnectionID, ConnectionData>,
    pub paused: bool,
    pub announce_state: AnnounceState,

    pub game: Game,

    pub data: Arc<Serverdata>,
    pub entities: Entities,
    pub score_changed: bool,
    pub packet_loopback: VecDeque<PacketS>,
    pub last_movement_update: HashMap<PlayerID, Instant>,
    pub index: DataIndex,
    pub packet_out: VecDeque<PacketC>,
    pub scoreboard: ScoreboardStore,
    pub gamedata_index: GamedataIndex,
    pub editor_address: Option<String>,
}

pub trait GameServerExt {
    fn unload(&mut self, packet_out: &mut VecDeque<PacketC>);
    fn load(
        &mut self,
        gamedata: Gamedata,
        serverdata: &Serverdata,
        timer: Option<Duration>,
        packet_out: &mut VecDeque<PacketC>,
    );
    #[allow(clippy::too_many_arguments)]
    fn join_player(
        &mut self,
        id: PlayerID,
        name: String,
        character: Character,
        class: PlayerClass,
        serverdata: &Serverdata,
        custom_position: Option<Vec2>,
        packet_out: Option<&mut VecDeque<PacketC>>,
    );
    fn prime_client(&self) -> Vec<PacketC>;
    fn set_tile(&mut self, p: IVec2, t: Option<TileIndex>, packet_out: &mut VecDeque<PacketC>);
}
impl GameServerExt for Game {
    fn unload(&mut self, packet_out: &mut VecDeque<PacketC>) {
        packet_out.push_back(PacketC::SetIngame {
            state: false,
            lobby: false,
        });
        for (id, _) in self.players.drain() {
            packet_out.push_back(PacketC::RemovePlayer { id })
        }
        for (pos, _) in self.tiles.drain() {
            packet_out.push_back(PacketC::UpdateMap {
                tile: pos,
                kind: None,
                neighbors: [None, None, None, None],
            })
        }
        self.score = Score::default();
        self.end = None;
        self.environment_effects.clear();
        self.walkable.clear();
    }
    fn load(
        &mut self,
        gamedata: Gamedata,
        serverdata: &Serverdata,
        timer: Option<Duration>,
        packet_out: &mut VecDeque<PacketC>,
    ) {
        // TODO cleanup
        let players = self
            .players
            .iter()
            .filter(|(_, p)| p.class == PlayerClass::Chef)
            .map(|(id, p)| (*id, (p.name.to_owned(), p.character, p.class)))
            .collect::<HashMap<_, _>>();

        self.unload(packet_out);

        self.lobby = gamedata.current_map == "lobby";
        self.data = gamedata.into();
        self.data_index.update(&self.data);
        self.score = Score {
            time_remaining: timer.map(|dur| dur.as_secs_f64()).unwrap_or(0.),
            ..Default::default()
        };
        self.end = timer.map(|dur| Instant::now() + dur);

        for (&p, (tile, item)) in &serverdata.initial_map {
            self.tiles.insert(
                p,
                Tile {
                    kind: *tile,
                    item: item.map(|i| Item {
                        kind: i,
                        active: None,
                    }),
                },
            );
            if !self.data_index.tile_collide[tile.0] {
                self.walkable.insert(p);
            }
        }
        for (id, (name, character, class)) in players {
            self.join_player(id, name, character, class, serverdata, None, None);
        }

        packet_out.extend(self.prime_client());
    }

    fn prime_client(&self) -> Vec<PacketC> {
        let mut out = Vec::new();
        out.push(PacketC::Data {
            data: {
                let mut k = self.data.as_ref().to_owned();
                k.recipes.clear();
                k.demands.clear();
                Box::new(k)
            },
        });
        out.push(PacketC::Environment {
            effects: self.environment_effects.clone(),
        });
        for (&id, player) in &self.players {
            out.push(PacketC::AddPlayer {
                id,
                class: player.class,
                position: player.movement.position,
                character: player.character,
                name: player.name.clone(),
            });
            for (i, item) in player.items.iter().enumerate() {
                if let Some(item) = &item {
                    out.push(PacketC::SetItem {
                        location: ItemLocation::Player(id, Hand(i)),
                        item: Some(item.kind),
                    });
                    if let Some(Involvement {
                        player,
                        position,
                        speed,
                        warn,
                        ..
                    }) = item.active
                    {
                        out.push(PacketC::SetProgress {
                            player,
                            item: ItemLocation::Player(id, Hand(i)),
                            position,
                            speed,
                            warn,
                        });
                    }
                }
            }
            if let Some((message, timeout)) = &player.communicate_persist {
                out.push(PacketC::Communicate {
                    player: id,
                    message: Some(message.to_owned()),
                    timeout: Some(*timeout),
                })
            }
        }
        for (&tile, tdata) in &self.tiles {
            out.push(PacketC::UpdateMap {
                tile,
                neighbors: [
                    self.tiles.get(&(tile + IVec2::NEG_Y)).map(|e| e.kind),
                    self.tiles.get(&(tile + IVec2::NEG_X)).map(|e| e.kind),
                    self.tiles.get(&(tile + IVec2::Y)).map(|e| e.kind),
                    self.tiles.get(&(tile + IVec2::X)).map(|e| e.kind),
                ],
                kind: Some(tdata.kind),
            });
            if let Some(item) = &tdata.item {
                out.push(PacketC::SetItem {
                    location: ItemLocation::Tile(tile),
                    item: Some(item.kind),
                });
                if let Some(Involvement {
                    player,
                    position,
                    speed,
                    warn,
                    ..
                }) = item.active
                {
                    out.push(PacketC::SetProgress {
                        player,
                        item: ItemLocation::Tile(tile),
                        position,
                        speed,
                        warn,
                    });
                }
            }
        }
        out.push(PacketC::FlushMap);
        out.push(PacketC::Score(self.score.clone()));
        out.push(PacketC::SetIngame {
            state: true,
            lobby: self.lobby,
        });
        out
    }

    fn join_player(
        &mut self,
        id: PlayerID,
        name: String,
        character: Character,
        class: PlayerClass,
        serverdata: &Serverdata,
        custom_position: Option<Vec2>,
        packet_out: Option<&mut VecDeque<PacketC>>,
    ) {
        let position = custom_position.unwrap_or(match class {
            PlayerClass::Customer => serverdata.customer_spawn.unwrap_or(serverdata.chef_spawn),
            PlayerClass::Bot | PlayerClass::Chef => serverdata.chef_spawn,
            PlayerClass::Tram => Vec2::ZERO, // should always have custom location
        }) + (Vec2::new(random_float(), random_float()) - 0.5);
        self.players.insert(
            id,
            Player {
                items: (0..self.data.hand_count).map(|_| None).collect(),
                character,
                class,
                movement: MovementBase::new(position),
                communicate_persist: None,
                interacting: None,
                name: name.clone(),
            },
        );
        self.score.players = self.score.players.max(self.players.len());
        if let Some(packet_out) = packet_out {
            packet_out.push_back(PacketC::AddPlayer {
                id,
                name,
                class,
                position,
                character,
            });
        }
    }

    /// Dont forget to flush
    fn set_tile(
        &mut self,
        tile: IVec2,
        kind: Option<TileIndex>,
        packet_out: &mut VecDeque<PacketC>,
    ) {
        if let Some(kind) = kind {
            self.tiles.insert(tile, Tile::from(kind));
            if self.data_index.tile_collide[kind.0] {
                self.walkable.remove(&tile);
            } else {
                self.walkable.insert(tile);
            }
        } else {
            self.tiles.remove(&tile);
            self.walkable.remove(&tile);
        }
        packet_out.push_back(PacketC::UpdateMap {
            tile,
            kind,
            neighbors: [
                self.tiles.get(&(tile + IVec2::NEG_Y)).map(|e| e.kind),
                self.tiles.get(&(tile + IVec2::NEG_X)).map(|e| e.kind),
                self.tiles.get(&(tile + IVec2::Y)).map(|e| e.kind),
                self.tiles.get(&(tile + IVec2::X)).map(|e| e.kind),
            ],
        });
    }
}

impl Server {
    pub fn new(data_path: PathBuf, tx: Sender<PacketC>) -> Result<Self> {
        Ok(Self {
            game: Game::default(),
            index: DataIndex::new(data_path).context("Failed to load data index")?,
            tx,
            announce_state: AnnounceState::Done,
            packet_out: VecDeque::new(),
            connections: HashMap::new(),
            data: Serverdata::default().into(),
            gamedata_index: GamedataIndex::default(),
            entities: Vec::new(),
            score_changed: false,
            packet_loopback: VecDeque::new(),
            last_movement_update: HashMap::default(),
            scoreboard: ScoreboardStore::load().context("Failed to load scoreboards")?,
            editor_address: None,
            paused: false,
        })
    }
}

impl Server {
    pub fn load(
        &mut self,
        (gamedata, serverdata): (Gamedata, Serverdata),
        timer: Option<Duration>,
    ) {
        info!("Changing map to {:?}", gamedata.current_map);
        for mut e in self.entities.drain(..) {
            e.destructor(EntityContext {
                game: &mut self.game,
                packet_out: &mut self.packet_out,
                packet_in: &mut self.packet_loopback,
                score_changed: &mut self.score_changed,
                scoreboard: &self.scoreboard,
                serverdata: self.data.as_ref(),
                replies: None,
                dt: 0.,
                load_map: &mut None,
            });
        }
        self.tick(0.); // TODO ?
        self.game.load(
            gamedata,
            &serverdata,
            timer.or(serverdata.default_timer),
            &mut self.packet_out,
        );
        self.gamedata_index.update(&self.game.data);
        self.entities.clear();
        for ed in &serverdata.entity_decls {
            self.entities.push(construct_entity(ed));
        }
        self.data = serverdata.into();
        for e in &mut self.entities {
            e.constructor(EntityContext {
                game: &mut self.game,
                packet_out: &mut self.packet_out,
                packet_in: &mut self.packet_loopback,
                score_changed: &mut self.score_changed,
                load_map: &mut None,
                serverdata: &self.data,
                scoreboard: &self.scoreboard,
                replies: None,
                dt: 0.,
            });
        }
        self.connections.values_mut().for_each(|c| c.ready = false);
        self.announce_state = if self.game.lobby {
            AnnounceState::Done
        } else {
            AnnounceState::Queued
        };
        self.update_paused();
    }

    pub fn packet_in(
        &mut self,
        conn: Option<ConnectionID>,
        packet: PacketS,
        replies: &mut Vec<PacketC>,
    ) -> Result<(), TrError> {
        match packet {
            PacketS::Join {
                name,
                character,
                class,
                position,
                id,
            } => {
                if name.chars().count() > 32 || name.len() > 64 {
                    return Err(tre!(
                        "s.error.username_length_limit",
                        s = "32".to_string(),
                        s = "64".to_string()
                    ));
                }
                if self.game.players.len() > 64 {
                    return Err(tre!("s.error.too_many_players"));
                }

                let player = id.unwrap_or_else(|| self.game.get_unused_player_id());
                self.game.join_player(
                    player,
                    name,
                    character,
                    class,
                    &self.data,
                    position,
                    Some(&mut self.packet_out),
                );
                if let Some(conn) = conn {
                    info!("{player} joined (owned by {conn})");
                } else {
                    info!("Server adds {player}");
                }
                replies.push(PacketC::Joined { id: player })
            }
            PacketS::Leave { player } => {
                if let Some(conn) = conn {
                    info!("{player} left (owned by {conn})");
                } else {
                    info!("Server removes {player}");
                }
                let p = self
                    .game
                    .players
                    .remove(&player)
                    .ok_or(tre!("s.error.no_player"))?;

                self.game.players_spatial_index.remove_entry(player);

                // TODO if holding two, one is destroyed
                for item in p.items.into_iter().flatten() {
                    let pos = p.movement.position.floor().as_ivec2();
                    if let Some(tile) = self.game.tiles.get_mut(&pos)
                        && tile.item.is_none()
                    {
                        self.packet_out.push_back(PacketC::SetItem {
                            location: ItemLocation::Tile(pos),
                            item: Some(item.kind),
                        });
                        tile.item = Some(item);
                    }
                }
                self.packet_out
                    .push_back(PacketC::RemovePlayer { id: player })
            }
            PacketS::Effect { player, name } => {
                self.packet_out.push_back(PacketC::Effect { name, player });
            }
            PacketS::Movement {
                pos,
                boost,
                dir,
                player,
            } => {
                let pd = self
                    .game
                    .players
                    .get_mut(&player)
                    .ok_or(tre!("s.error.no_player"))?;

                pd.movement.input(dir, boost);

                if let Some(pos) = pos {
                    let last_position_update = self
                        .last_movement_update
                        .entry(player)
                        .or_insert_with(Instant::now);
                    let dt = last_position_update.elapsed();
                    *last_position_update += dt;
                    let diff = pos - pd.movement.position;
                    pd.movement.position += diff.clamp_length_max(dt.as_secs_f32());
                    if diff.length() > 1. {
                        replies.push(PacketC::MovementSync { player });
                    }
                }
            }
            PacketS::Interact { pos, player, hand } => {
                for e in &mut self.entities {
                    if e.interact(
                        EntityContext {
                            game: &mut self.game,
                            packet_out: &mut self.packet_out,
                            packet_in: &mut self.packet_loopback,
                            score_changed: &mut self.score_changed,
                            load_map: &mut None,
                            serverdata: &self.data,
                            scoreboard: &self.scoreboard,
                            replies: Some(replies),
                            dt: 0.,
                        },
                        pos,
                        player,
                    )? {
                        return Ok(());
                    }
                }

                let pid = player;
                let player = self
                    .game
                    .players
                    .get_mut(&pid)
                    .ok_or(tre!("s.error.no_player"))?;

                let pos = pos.map(|p| (p, hand));

                let ((pos, hand), edge) = match (pos, player.interacting) {
                    (None, None) => return Ok(()), // this is silent because of auto release
                    (None, Some(pos)) => (pos, false),
                    (Some(pos), None) => (pos, true),
                    (Some(_), Some(_)) => return Err(tre!("s.error.already_interacting")),
                };

                let entpos = pos.as_vec2() + Vec2::splat(0.5);
                if edge && entpos.distance(player.movement.position) > 2. {
                    return Err(tre!("s.error.interacting_too_far"));
                }

                let tile = self
                    .game
                    .tiles
                    .get_mut(&pos)
                    .ok_or(tre!("s.error.no_tile"))?;

                // No going back from here on

                player.interacting = if edge { Some((pos, hand)) } else { None };

                // Dont try interacting with player it tile is interactable
                let other_pid = if self
                    .game
                    .data
                    .tile_placeable_items
                    .get(&tile.kind)
                    // TODO check for hand item
                    .is_none_or(|p| p.is_empty())
                {
                    self.game
                        .players
                        .iter()
                        .find(|(id, p)| **id != pid && p.movement.position.distance(entpos) < 0.7)
                        .map(|(&id, _)| id)
                } else {
                    None
                };

                if let Some(base_pid) = other_pid {
                    if pid == base_pid {
                        return Err(tre!("s.error.self_interact"));
                    }
                    let [Some(other), Some(this)] =
                        self.game.players.get_disjoint_mut([&pid, &base_pid])
                    else {
                        return Err(tre!("s.error.self_interact"));
                    };

                    if this.class == PlayerClass::Customer || other.class == PlayerClass::Customer {
                        return Err(tre!("s.error.customer_interact"));
                    }

                    let this_hslot = this.items.get_mut(hand.0).ok_or(tre!("s.error.no_hand"))?;
                    let other_hslot = other.items.get_mut(hand.0).ok_or(tre!("s.error.no_hand"))?;

                    interact(
                        &self.game.data,
                        edge,
                        None,
                        Some(pid),
                        this_hslot,
                        ItemLocation::Player(base_pid, hand),
                        other_hslot,
                        ItemLocation::Player(pid, hand),
                        &mut self.game.score,
                        &mut self.score_changed,
                        false,
                        &mut self.packet_out,
                    )
                } else {
                    let player = self
                        .game
                        .players
                        .get_mut(&pid)
                        .ok_or(tre!("s.error.no_player"))?;

                    let hslot = player
                        .items
                        .get_mut(hand.0)
                        .ok_or(tre!("s.error.no_hand"))?;

                    interact(
                        &self.game.data,
                        edge,
                        Some(tile.kind),
                        Some(pid),
                        &mut tile.item,
                        ItemLocation::Tile(pos),
                        hslot,
                        ItemLocation::Player(pid, hand),
                        &mut self.game.score,
                        &mut self.score_changed,
                        false,
                        &mut self.packet_out,
                    )
                }
            }
            PacketS::Communicate {
                message,
                timeout,
                player,
                pin,
            } => {
                let Some(player_data) = self.game.players.get_mut(&player) else {
                    return Ok(());
                };
                if let Some(message) = &message {
                    let body = message.display_message(&FALLBACK_LOCALE, &self.game.data, &COLORED);
                    if player_data.name != "" {
                        info!("[{player} {:?}] {body}", player_data.name);
                    } else {
                        info!("[{player}] {body}",);
                    }
                }
                let pin = pin.unwrap_or(false);
                let timeout = if let Some(timeout) = timeout {
                    let mut timeout = MessageTimeout {
                        initial: timeout,
                        remaining: timeout,
                        pinned: pin,
                    };
                    if let Some((_, t)) = &player_data.communicate_persist {
                        timeout.initial = t.initial;
                    };
                    player_data.communicate_persist = message.clone().map(|m| (m, timeout));
                    Some(timeout)
                } else {
                    None
                };
                self.packet_out.push_back(PacketC::Communicate {
                    player,
                    message,
                    timeout,
                });
            }
            PacketS::ReplaceHand { item, player, hand } => {
                let pdata = self.game.players.get_mut(&player).ok_or(tre!(""))?;
                if let Some(slot) = pdata.items.get_mut(hand.0) {
                    *slot = item.map(|i| Item {
                        kind: i,
                        active: None,
                    });
                }
                self.packet_out.push_back(PacketC::SetItem {
                    location: ItemLocation::Player(player, hand),
                    item,
                })
            }
            PacketS::ApplyScore(score) => {
                self.game.score.demands_completed += score.demands_completed;
                self.game.score.demands_failed += score.demands_failed;
                self.game.score.points += score.points;
                self.score_changed = true;
            }
            PacketS::ReplayTick { .. } => return Err(tre!("s.error.packet_not_supported")),
            PacketS::Idle { .. } | PacketS::Ready => (),
        }
        Ok(())
    }

    /// Returns true if the game should end
    pub fn tick(&mut self, dt: f32) -> Option<(String, Option<Duration>)> {
        if self.score_changed {
            self.score_changed = false;
            self.packet_out
                .push_back(PacketC::Score(self.game.score.clone()));
        }

        for (&pos, tile) in &mut self.game.tiles {
            tick_slot(
                dt,
                &self.game.data,
                &self.gamedata_index,
                Some(tile.kind),
                &mut tile.item,
                ItemLocation::Tile(pos),
                &mut self.game.score,
                &mut self.score_changed,
                &mut self.packet_out,
            );
        }

        for (&pid, player) in &mut self.game.players {
            player.movement.update(&self.game.walkable, dt);

            self.game
                .players_spatial_index
                .update_entry(pid, player.movement.position);
        }

        self.game.players_spatial_index.all(|p1, pos1| {
            self.game
                .players_spatial_index
                .query(pos1, 2., |p2, _pos2| {
                    if p1 != p2
                        && let [Some(a), Some(b)] = self.game.players.get_disjoint_mut([&p1, &p2])
                    {
                        a.movement.collide(&mut b.movement, dt)
                    }
                })
        });

        for (&pid, player) in &mut self.game.players {
            self.packet_out.push_back(PacketC::Movement {
                player: pid,
                pos: player.movement.position,
                dir: player.movement.input_direction,
                boost: player.movement.boosting,
                rot: player.movement.rotation,
            });

            for (i, item) in player.items.iter_mut().enumerate() {
                tick_slot(
                    dt,
                    &self.game.data,
                    &self.gamedata_index,
                    None,
                    item,
                    ItemLocation::Player(pid, Hand(i)),
                    &mut self.game.score,
                    &mut self.score_changed,
                    &mut self.packet_out,
                );
            }
        }

        let mut players_auto_release = Vec::new();
        for (pid, player) in &mut self.game.players {
            if let Some((_, timeout)) = &mut player.communicate_persist {
                timeout.remaining -= dt;
                if timeout.remaining < 0. {
                    player.communicate_persist = None;
                }
            }
            if let Some((pos, hand)) = player.interacting
                && let Some(tile) = self.game.tiles.get(&pos)
                && let Some(item) = &tile.item
                && let Some(involvement) = &item.active
                && involvement.position >= 1.
            {
                players_auto_release.push((*pid, hand));
            }
        }
        for (player, hand) in players_auto_release.drain(..) {
            let _ = self.packet_in(
                None,
                PacketS::Interact {
                    pos: None,
                    player,
                    hand,
                },
                &mut vec![],
            );
        }

        let mut load_map = None;
        for entity in self.entities.iter_mut() {
            if let Err(e) = entity.tick(EntityContext {
                game: &mut self.game,
                load_map: &mut load_map,
                packet_out: &mut self.packet_out,
                score_changed: &mut self.score_changed,
                packet_in: &mut self.packet_loopback,
                scoreboard: &self.scoreboard,
                serverdata: &self.data,
                replies: None,
                dt,
            }) {
                warn!("Entity tick failed: {e}")
            }
        }
        self.entities.retain_mut(|e| {
            if e.finished() {
                e.destructor(EntityContext {
                    game: &mut self.game,
                    load_map: &mut load_map,
                    packet_out: &mut self.packet_out,
                    score_changed: &mut self.score_changed,
                    packet_in: &mut self.packet_loopback,
                    scoreboard: &self.scoreboard,
                    serverdata: &self.data,
                    replies: None,
                    dt: 0.,
                });
                false
            } else {
                true
            }
        });

        if let Some(map) = load_map {
            return Some((map, None));
        }

        while let Some(p) = self.packet_loopback.pop_front() {
            if let Err(e) = self.packet_in(None, p, &mut vec![]) {
                warn!("Internal packet errored: {e}");
            }
        }

        let now = Instant::now();

        if let Some(end) = self.game.end {
            self.game.score.time_remaining = (end - now).as_secs_f64();
            if end < now {
                let relative_score =
                    (self.game.score.points * 100) / self.data.score_baseline.max(1);
                self.game.score.stars = match relative_score {
                    100.. => 3,
                    70.. => 2,
                    40.. => 1,
                    _ => 0,
                };
                self.packet_out
                    .push_back(PacketC::Menu(Menu::Score(self.game.score.clone())));
                self.scoreboard.insert(
                    &self.game.data.current_map,
                    self.game
                        .players
                        .values()
                        .filter(|m| m.class == PlayerClass::Chef)
                        .map(|m| m.name.clone())
                        .collect(),
                    self.game.score.clone(),
                );
                Some(("lobby".to_string(), None))
            } else {
                None
            }
        } else {
            None
        }
    }

    pub fn count_chefs(&self) -> usize {
        self.game
            .players
            .values()
            .map(|p| if p.class == PlayerClass::Chef { 1 } else { 0 })
            .sum()
    }
}
