/*
    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::server::ServerConfig;
use hurrycurry_locale::{
    GLOBAL_LOCALE, TrError,
    message::{COLORED, MessageDisplayExt},
    tre, trm,
};
use hurrycurry_protocol::{Message, PacketC, PacketS, PlayerID, VoteSubject};
use log::info;
use std::{
    collections::{HashMap, HashSet, VecDeque},
    sync::Arc,
};

pub struct VoteState {
    config: Arc<ServerConfig>,
    initiate_cooldown: HashMap<PlayerID, f32>,
    cast_cooldown: HashMap<PlayerID, f32>,
    voting_players: HashSet<PlayerID>,
    active: Option<ActiveVote>,
    pub ratified: Option<VoteSubject>,
    pub packet_out: VecDeque<PacketC>,
}
pub struct ActiveVote {
    timeout: f32,
    initiated_by: PlayerID,
    ballots: HashMap<PlayerID, bool>,
    subject: VoteSubject,
}

impl VoteState {
    pub fn new(config: Arc<ServerConfig>) -> Self {
        Self {
            config,
            initiate_cooldown: Default::default(),
            cast_cooldown: Default::default(),
            voting_players: Default::default(),
            active: Default::default(),
            ratified: Default::default(),
            packet_out: Default::default(),
        }
    }
    pub fn join(&mut self, player: PlayerID) {
        self.cast_cooldown
            .insert(player, self.config.vote_cast_cooldown);
        self.initiate_cooldown
            .insert(player, self.config.vote_initiate_cooldown);
        self.update();
    }
    pub fn leave(&mut self, player: PlayerID) {
        self.cast_cooldown.remove(&player);
        self.initiate_cooldown.remove(&player);
        self.voting_players.remove(&player);
        if let Some(a) = &mut self.active {
            a.ballots.remove(&player);
        }
        self.update();
    }
    pub fn prime_client(&self, init: &mut Vec<PacketC>) {
        if let Some(active) = &self.active {
            init.push(PacketC::VoteStarted {
                initiated_by: active.initiated_by,
                subject: active.subject.clone(),
                message: subject_to_message(&active.subject),
                timeout: active.timeout,
            });
        }
    }
    pub fn packet_in(&mut self, p: PacketS) -> Result<(), TrError> {
        match p {
            PacketS::InitiateVote { player, subject } => {
                if self.active.is_some() {
                    return Err(tre!("s.vote.error.active"));
                }
                if let Some(t) = self.initiate_cooldown.get(&player) {
                    return Err(tre!(
                        "s.vote.error.initiate_cooldown",
                        s = format!("{t:.0}")
                    ));
                }
                info!(
                    "{player} initiates vote for {}",
                    subject_to_message(&subject).display_nodata(&GLOBAL_LOCALE, &COLORED)
                );
                if self.voting_players.len() == 1 {
                    info!("Vote passed (skipped)");
                    self.ratified = Some(subject);
                    return Ok(());
                }
                self.initiate_cooldown
                    .insert(player, self.config.vote_initiate_cooldown);
                self.active = Some(ActiveVote {
                    timeout: self.config.vote_timeout,
                    initiated_by: player,
                    ballots: [(player, true)].into_iter().collect(), // assuming initiating player agrees with their own vote
                    subject: subject.clone(),
                });
                self.packet_out.push_back(PacketC::VoteStarted {
                    initiated_by: player,
                    message: subject_to_message(&subject),
                    subject,
                    timeout: self.config.vote_timeout,
                });
                self.update();
            }
            PacketS::CastVote { player, agree } => {
                let Some(active) = &mut self.active else {
                    return Err(tre!("s.vote.error.not_active"));
                };
                if let Some(t) = self.cast_cooldown.get(&player) {
                    return Err(tre!("s.vote.error.cast_cooldown", s = format!("{t:.0}")));
                }
                if active.initiated_by == player && !agree {
                    self.active = None;
                    self.packet_out
                        .push_back(PacketC::VoteEnded { result: false });
                    return Ok(());
                }
                active.ballots.insert(player, agree);
                self.update();
            }
            _ => (),
        }
        Ok(())
    }

    pub fn tick(&mut self, dt: f32) {
        let mut need_update = false;
        self.initiate_cooldown.retain(|_, c| {
            *c -= dt;
            *c > 0.
        });
        self.cast_cooldown.retain(|p, c| {
            *c -= dt;
            if *c <= 0. {
                self.voting_players.insert(*p);
                need_update = true;
            }
            *c > 0.
        });
        if let Some(v) = &mut self.active {
            v.timeout -= dt;
            need_update |= v.timeout <= 0.
        }
        if need_update {
            self.update();
        }
    }

    pub fn update(&mut self) {
        // skip cooldowns if single player
        if self.voting_players.len() + self.cast_cooldown.len() == 1 {
            self.voting_players.extend(self.cast_cooldown.keys());
            self.cast_cooldown.clear();
            self.initiate_cooldown.clear();
        }
        if let Some(a) = &self.active {
            let total = self.voting_players.len();
            let mut agree = 0;
            let mut reject = 0;
            for &v in a.ballots.values() {
                if v {
                    agree += 1;
                } else {
                    reject += 1;
                }
            }
            if agree * 2 > total || reject * 2 > total || a.timeout <= 0. || self.config.skip_voting
            {
                let result = agree > reject;
                if result {
                    info!("Vote passed");
                    self.ratified = Some(a.subject.clone());
                } else {
                    info!("Vote rejected");
                }
                self.active = None;
                self.packet_out.push_back(PacketC::VoteEnded { result });
            } else {
                self.packet_out.push_back(PacketC::VoteUpdated {
                    total,
                    agree,
                    reject,
                });
            }
        }
    }
}

fn subject_to_message(s: &VoteSubject) -> Message {
    match s {
        VoteSubject::StartGame { config } => {
            trm!("s.vote.subject.start_game", s = config.map.clone())
        }
        VoteSubject::EndGame => trm!("s.vote.subject.end_game"),
        VoteSubject::RestartGame => trm!("s.vote.subject.restart_game"),
    }
}

#[cfg(test)]
mod test {
    use crate::{server::ServerConfig, vote::VoteState};
    use hurrycurry_protocol::{PacketS, PlayerID, VoteSubject};

    #[test]
    fn initial_cast_cooldown() {
        let mut vs = VoteState::new(ServerConfig::default().into());
        assert_eq!(vs.voting_players.len(), 0);
        vs.join(PlayerID(0));
        assert_eq!(vs.voting_players.len(), 1);
        vs.join(PlayerID(1));
        assert_eq!(vs.voting_players.len(), 1);
        vs.tick(60.);
        assert_eq!(vs.voting_players.len(), 2);
    }

    #[test]
    fn initial_initiate_cooldown() {
        let mut vs = VoteState::new(ServerConfig::default().into());
        assert_eq!(vs.initiate_cooldown.len(), 0);
        vs.join(PlayerID(0));
        assert_eq!(vs.initiate_cooldown.len(), 0);
        vs.join(PlayerID(1));
        assert_eq!(vs.initiate_cooldown.len(), 1);
        vs.tick(60.);
        assert_eq!(vs.initiate_cooldown.len(), 0);
    }

    #[test]
    fn singleplayer_vote_skip() {
        let mut vs = VoteState::new(ServerConfig::default().into());
        vs.join(PlayerID(0));
        vs.packet_in(PacketS::InitiateVote {
            player: PlayerID(0),
            subject: VoteSubject::EndGame,
        })
        .unwrap();
        assert_eq!(vs.ratified, Some(VoteSubject::EndGame));
    }

    #[test]
    fn vote_timeout() {
        let mut vs = VoteState::new(ServerConfig::default().into());
        vs.join(PlayerID(0));
        vs.join(PlayerID(1));
        vs.tick(60.);
        vs.packet_in(PacketS::InitiateVote {
            player: PlayerID(0),
            subject: VoteSubject::EndGame,
        })
        .unwrap();
        assert_eq!(vs.ratified, None);
        vs.tick(60.);
        assert_eq!(vs.ratified, Some(VoteSubject::EndGame));
    }

    #[test]
    fn accept_on_leave() {
        let mut vs = VoteState::new(ServerConfig::default().into());
        vs.join(PlayerID(0));
        vs.join(PlayerID(1));
        vs.tick(60.);
        vs.packet_in(PacketS::InitiateVote {
            player: PlayerID(0),
            subject: VoteSubject::EndGame,
        })
        .unwrap();
        vs.leave(PlayerID(1));
        assert_eq!(vs.ratified, Some(VoteSubject::EndGame));
    }
}
