/*
    Hurry Curry! - a game about cooking
    Copyright (C) 2026 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},
    scoreboard::{GameConfigScoreboardCheckExt, ScoreboardStore},
    vote::VoteState,
};
use anyhow::{Context, Result};
use hurrycurry_data::{PrivateGamedata, build_gamedata, load_map_metadata};
use hurrycurry_game_core::{Game, Item, Player};
use hurrycurry_locale::{
    GLOBAL_LOCALE, TrError,
    message::{COLORED, MessageDisplayExt},
    tre, trm,
};
use hurrycurry_protocol::{
    Character, GameConfig, Gamedata, ItemLocation, Menu, MessageTimeout, PacketC, PacketS,
    PlayerClass, PlayerID, Score, Serverdata, VoteSubject, glam::Vec2, movement::MovementBase,
};
use log::{info, warn};
use std::{
    collections::{HashMap, HashSet, VecDeque},
    path::PathBuf,
    sync::Arc,
    time::{Duration, Instant},
};
use tokio::sync::{broadcast, mpsc};

#[derive(Debug)]
pub struct ConnectionData {
    pub players: HashSet<PlayerID>,
    pub idle: bool,
    pub ready: bool,
    pub keepalive_timer: f32,
    pub replies: mpsc::Sender<PacketC>,
}

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

pub struct ServerConfig {
    pub inactivity_timeout: f32,
    pub vote_cast_cooldown: f32,
    pub vote_initiate_cooldown: f32,
    pub vote_timeout: f32,
    pub skip_voting: bool,
    pub lobby: String,
    pub data_path: PathBuf,
    pub motd: Option<String>,
    pub name: String,
}

pub struct Server {
    pub config: Arc<ServerConfig>,
    pub tick_perf: (Duration, usize),
    pub broadcast: broadcast::Sender<PacketC>,
    pub connections: HashMap<ConnectionID, ConnectionData>,
    pub paused: bool,
    pub announce_state: AnnounceState,

    pub game: Game,
    pub vote_state: VoteState,
    pub data: Box<Serverdata>,
    pub priv_gamedata: Arc<PrivateGamedata>,
    pub entities: Entities,
    pub score_changed: bool,
    pub packet_loopback: VecDeque<PacketS>,
    pub last_movement_update: HashMap<PlayerID, Instant>,
    pub player_inactivity_timers: HashMap<PlayerID, f32>,
    pub packet_out: VecDeque<PacketC>,
    pub scoreboard: ScoreboardStore,
}

impl Server {
    pub fn new(config: ServerConfig, tx: broadcast::Sender<PacketC>) -> Result<Self> {
        let config = Arc::new(config);
        Ok(Self {
            data: Box::new(Serverdata {
                bot_algos: vec![
                    "waiter".to_string(),
                    "simple".to_string(),
                    "dishwasher".to_string(),
                    "frank".to_string(),
                ],
                maps: load_map_metadata(&config.data_path).context("map metadata")?,
                motd: config.motd.clone(),
                name: config.name.clone(),
            }),
            vote_state: VoteState::new(config.clone()),
            config,
            game: Game::default(),
            tick_perf: (Duration::ZERO, 0),
            broadcast: tx,
            announce_state: AnnounceState::Done,
            packet_out: VecDeque::new(),
            connections: HashMap::new(),
            priv_gamedata: PrivateGamedata::default().into(),
            entities: Vec::new(),
            score_changed: false,
            packet_loopback: VecDeque::new(),
            last_movement_update: HashMap::default(),
            player_inactivity_timers: HashMap::new(),
            scoreboard: ScoreboardStore::load().context("Failed to load scoreboards")?,
            paused: false,
        })
    }
}
impl Default for ServerConfig {
    fn default() -> Self {
        Self {
            data_path: "data".into(),
            inactivity_timeout: 60.,
            lobby: "lobby".to_string(),
            name: "A Hurry Curry! Server".to_string(),
            motd: None,
            skip_voting: false,
            vote_cast_cooldown: 20.,
            vote_initiate_cooldown: 30.,
            vote_timeout: 20.,
        }
    }
}

pub trait GameServerExt {
    fn load(
        &mut self,
        gamedata: Gamedata,
        serverdata: &PrivateGamedata,
        packet_out: &mut VecDeque<PacketC>,
        is_lobby: bool,
    );
    #[allow(clippy::too_many_arguments)]
    fn join_player(
        &mut self,
        id: PlayerID,
        name: String,
        character: Character,
        class: PlayerClass,
        serverdata: &PrivateGamedata,
        custom_position: Option<Vec2>,
    );
}
impl GameServerExt for Game {
    fn load(
        &mut self,
        gamedata: Gamedata,
        serverdata: &PrivateGamedata,
        packet_out: &mut VecDeque<PacketC>,
        is_lobby: bool,
    ) {
        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 = is_lobby;
        self.data = gamedata.into();
        self.data_index.update(&self.data);
        self.score = Score {
            time_remaining: serverdata.timer,
            ..Default::default()
        };

        for (&pos, (parts, item)) in &serverdata.initial_map {
            self.set_tile(pos, parts.to_vec());
            if item.is_some() {
                self.set_item(pos, *item);
            }
        }

        packet_out.extend(self.prime_client());

        for (id, (name, character, class)) in players {
            self.join_player(id, name, character, class, serverdata, None);
        }
    }

    fn join_player(
        &mut self,
        id: PlayerID,
        name: String,
        character: Character,
        class: PlayerClass,
        serverdata: &PrivateGamedata,
        custom_position: Option<Vec2>,
    ) {
        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(rand::random::<f32>(), rand::random::<f32>()) - 0.5);
        self.players.insert(
            id,
            Player {
                items: (0..self.data.hand_count).map(|_| None).collect(),
                character,
                class,
                communicate_persist: None,
                interacting: None,
                name: name.clone(),
                movement: MovementBase::new(position),
                movement_input_changed: false,
                movement_must_sync: false,
            },
        );
        self.score.players = self.score.players.max(self.players.len());
        self.events.push_back(PacketC::AddPlayer {
            id,
            position,
            class,
            character,
            name,
        });
    }
}

impl Server {
    pub fn load(&mut self, config: GameConfig) -> Result<(), TrError> {
        if self.game.data.metadata.name == config.map {
            info!("Reloading map {:?}", config.map);
        } else {
            info!("Changing map to {:?}", config.map);
        }
        let (gamedata, serverdata) = build_gamedata(&self.config.data_path, &config, true)
            .map_err(|m| tre!("s.error.map_load", s = format!("{m:#}")))?;

        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.priv_gamedata.as_ref(),
                replies: None,
                dt: 0.,
                load_map: &mut None,
            });
        }
        // Need to process loopback packets for entity despawn
        while let Some(p) = self.packet_loopback.pop_front() {
            if let Err(e) = self.packet_in(&mut vec![], p) {
                warn!("Internal entity destructor packet errored: {e}");
            }
        }
        let is_lobby = gamedata.metadata.name == self.config.lobby;
        self.game
            .load(gamedata, &serverdata, &mut self.packet_out, is_lobby);
        self.game.config = config;
        self.entities.clear();
        for ed in &serverdata.entity_decls {
            self.entities.push(construct_entity(ed));
        }
        if !self.game.config.is_valid_for_scoreboard() {
            self.packet_out.push_back(PacketC::ServerMessage {
                message: trm!("s.custom_game_warn"),
                error: false,
            });
        }
        self.priv_gamedata = 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.priv_gamedata,
                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();
        Ok(())
    }

    pub fn packet_in(
        &mut self,
        replies: &mut Vec<PacketC>,
        packet: PacketS,
    ) -> 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.priv_gamedata,
                    position,
                );
                info!("{player} joined");
                if class == PlayerClass::Chef {
                    self.vote_state.join(player);
                }
                replies.push(PacketC::Joined { id: player })
            }
            PacketS::Leave { player } => {
                info!("{player} left");
                if !self.game.remove_player(player) {
                    return Err(tre!("s.error.no_player"));
                }
                self.game.players_spatial_index.remove_entry(player);
                self.player_inactivity_timers.remove(&player);
                self.last_movement_update.remove(&player);
                self.vote_state.leave(player);
            }
            PacketS::Effect { effect, location } => {
                self.packet_out
                    .push_back(PacketC::Effect { effect, location });
            }
            PacketS::Movement {
                pos,
                boost,
                dir,
                player,
            } => {
                let pd = self
                    .game
                    .players
                    .get_mut(&player)
                    .ok_or(tre!("s.error.no_player"))?;

                if pd.movement.input_direction != dir || pd.movement.boosting != boost {
                    pd.movement_input_changed = true;
                }
                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().min(0.1));
                    if diff.length() > 1. {
                        pd.movement_must_sync = true;
                    }
                }
            }
            PacketS::Interact {
                target,
                player,
                hand,
            } => {
                let mut entity_handled = false;
                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.priv_gamedata,
                            scoreboard: &self.scoreboard,
                            replies: Some(replies),
                            dt: 0.,
                        },
                        target,
                        player,
                    )? {
                        // multiple entities might handle this in case of a button tutorial
                        entity_handled = true;
                    }
                }
                if entity_handled {
                    return Ok(());
                }

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

                let loc = target.map(|p| (p, hand));

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

                let pos = match loc {
                    ItemLocation::Tile(pos) => pos.as_vec2() + Vec2::splat(0.5),
                    ItemLocation::Player(p, _) => {
                        self.game
                            .players
                            .get(&p)
                            .ok_or_else(|| tre!("s.error.no_player"))?
                            .movement
                            .position
                    }
                };

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

                if edge && pos.distance(player.movement.position) > 2. {
                    return Err(tre!("s.error.interacting_too_far"));
                }

                player.interacting = if edge { Some((loc, hand)) } else { None };
                self.game
                    .interact(loc, ItemLocation::Player(pid, hand), edge)?;
            }
            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(&GLOBAL_LOCALE, &self.game.data, &COLORED);
                    if !player_data.name.is_empty() {
                        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 | PacketS::Keepalive => (),
            PacketS::Debug(d) => self.packet_out.push_back(PacketC::Debug(d)),
            p @ PacketS::CastVote { .. } => self.vote_state.packet_in(p)?,
            p @ PacketS::InitiateVote { .. } => self.vote_state.packet_in(p)?,
        }
        Ok(())
    }

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

        for loc in self.game.item_locations_index.clone() {
            if let Err(e) = self.game.tick_slot(loc, dt) {
                warn!("Slot tick failed: {}", e);
            }
        }

        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)
                    {
                        a.movement_input_changed = true;
                    }
                })
        });

        for (&pid, player) in &mut self.game.players {
            if player.movement_input_changed || player.movement_must_sync {
                player.movement_input_changed = false;
                self.packet_out.push_back(
                    player
                        .movement
                        .movement_packet_c(pid, player.movement_must_sync),
                );
                player.movement_must_sync = false;
            }
        }

        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((loc, hand)) = player.interacting {
                match loc {
                    ItemLocation::Tile(pos) => {
                        if 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));
                        }
                    }
                    ItemLocation::Player(_pid, _hand) => {
                        // TODO
                    }
                }
            }
        }
        for (player, hand) in players_auto_release.drain(..) {
            let _ = self.packet_in(
                &mut vec![],
                PacketS::Interact {
                    target: None,
                    player,
                    hand,
                },
            );
        }

        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.priv_gamedata,
                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.priv_gamedata,
                    replies: None,
                    dt: 0.,
                });
                false
            } else {
                true
            }
        });
        if let Some(config) = load_map {
            return Some(config);
        }

        self.vote_state.tick(dt);
        if let Some(subject) = self.vote_state.ratified.take() {
            match subject {
                VoteSubject::StartGame { config } => return Some(config),
                VoteSubject::EndGame => {
                    return Some(GameConfig {
                        map: self.config.lobby.to_owned(),
                        ..Default::default()
                    });
                }
                VoteSubject::RestartGame => {
                    return Some(GameConfig {
                        map: self.game.data.metadata.name.to_owned(),
                        ..Default::default()
                    });
                }
            }
        }

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

        if !self.game.lobby {
            self.game.score.time_remaining -= dt;
            if self.game.score.time_remaining < 0. {
                let relative_score =
                    (self.game.score.points * 100) / self.priv_gamedata.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.config,
                    self.game
                        .players
                        .values()
                        .filter(|m| m.class == PlayerClass::Chef)
                        .map(|m| m.name.clone())
                        .collect(),
                    self.game.score.clone(),
                );
                Some(GameConfig {
                    map: self.config.lobby.to_string(),
                    ..Default::default()
                })
            } else {
                None
            }
        } else {
            None
        }
    }

    pub fn count_spectators(&self) -> usize {
        self.connections
            .values()
            .filter(|c| c.players.is_empty())
            .count()
    }

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