/*
    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::{
    entity::tutorial::Tutorial,
    server::{AnnounceState, Server},
};
use anyhow::Result;
use clap::{Parser, ValueEnum};
use hurrycurry_bot::algos::ALGO_CONSTRUCTORS;
use hurrycurry_locale::{TrError, tre, trm};
use hurrycurry_protocol::{
    Effect, GameConfig, Hand, ItemLocation, Menu, Message, PacketC, PacketS, PlayerID, VoteSubject,
};
use std::{fmt::Write, str::FromStr};

#[derive(Parser)]
#[clap(multicall = true)]
enum Command {
    /// Start a new game
    #[clap(alias = "s")]
    Start {
        /// Map name
        #[arg(default_value = "junior")]
        map: String,

        /// Number of hands per player
        #[arg(short = 'c', long)]
        hand_count: Option<usize>,

        /// Duration in seconds
        #[arg(short, long)]
        timer: Option<f32>,

        /// Add bot by name
        #[arg(short, long)]
        bot: Vec<String>,
    },
    /// Shows the best entries of the scoreboard for this map.
    #[clap(alias = "top", alias = "top5")]
    Scoreboard {
        /// Name of the map, default: current
        map: Option<String>,
        /// Send text instead of document
        #[arg(short, long)]
        text: bool,
    },
    #[clap(alias = "v")]
    Vote { agree: YesNo },
    #[clap(alias = "mapinfo")]
    Info {
        /// Name of the map, default: current
        map: Option<String>,
    },
    /// Abort the current game
    End,
    /// Send an item message
    Item { name: String },
    /// Send a tile message
    Tile { name: String },
    /// Show all possible demands for this map
    Demands,
    /// Play effect
    Effect { name: String },
    /// Ready yourself
    Ready {
        #[arg(short, long)]
        force: bool,
        #[arg(short, long)]
        unready: bool,
    },
    /// Reload the current map
    #[clap(alias = "r")]
    Reload,
    /// Shows the recipe book
    Book,
    /// Start an interactive tutorial for some item
    #[clap(alias = "tutorial")]
    StartTutorial { item: String },
    /// End the tutorial unfinished
    EndTutorial,
    #[clap(alias = "tr")]
    /// Manually send a translated message
    TranslateMessage {
        message_id: String,
        arguments: Vec<String>,
    },
    /// Add bots to the current game (CHEAT)
    #[cfg(feature = "cheats")]
    #[clap(alias = "summon", alias = "bot")]
    CreateBot { algo: String, name: Option<String> },
    /// Set your players hand item (CHEAT)
    #[cfg(feature = "cheats")]
    #[clap(alias = "give")]
    SetItem { name: String },
    /// Set the tile under your player (CHEAT)
    #[cfg(feature = "cheats")]
    #[clap(alias = "setblock")]
    SetTile { name: String },
}

#[derive(ValueEnum, Clone)]
enum DownloadType {
    Map,
    Recipes,
}

impl Server {
    pub fn handle_command_parse(
        &mut self,
        player: PlayerID,
        command: &str,
    ) -> Result<Vec<PacketC>, TrError> {
        let mut replies = Vec::new();
        for line in command.split("\n") {
            self.handle_command(
                player,
                Command::try_parse_from(
                    shlex::split(line)
                        .ok_or(tre!("s.error.quoting_invalid"))?
                        .into_iter(),
                )
                .map_err(|e| TrError::Plain(e.to_string()))?,
                &mut replies,
            )?;
        }
        Ok(replies)
    }
    fn handle_command(
        &mut self,
        player: PlayerID,
        command: Command,
        replies: &mut Vec<PacketC>,
    ) -> Result<(), TrError> {
        match command {
            Command::Start {
                map,
                hand_count,
                timer,
                bot,
            } => {
                for b in &bot {
                    ALGO_CONSTRUCTORS
                        .iter()
                        .find(|(name, _)| *name == b)
                        .ok_or(tre!("s.error.algo_not_found", s = b.to_string()))?;
                }
                self.packet_in(
                    replies,
                    PacketS::InitiateVote {
                        player,
                        subject: VoteSubject::StartGame {
                            config: GameConfig {
                                hand_count,
                                map,
                                timer,
                                bots: bot,
                            },
                        },
                    },
                )?;
            }
            Command::End => {
                self.packet_in(
                    replies,
                    PacketS::InitiateVote {
                        player,
                        subject: VoteSubject::EndGame,
                    },
                )?;
            }
            Command::Vote { agree } => {
                self.packet_in(
                    replies,
                    PacketS::CastVote {
                        player,
                        agree: agree.0,
                    },
                )?;
            }
            Command::Reload => {
                if self.count_chefs() > 1 {
                    return Err(tre!("s.error.must_be_alone"));
                }
                self.load(GameConfig {
                    map: self.game.data.metadata.name.clone(),
                    ..Default::default()
                })?;
                self.announce_state = AnnounceState::Done
            }
            Command::Ready { force, unready } => {
                for c in self.connections.values_mut() {
                    if c.players.contains(&player) || force {
                        c.ready = true;
                    }
                    if unready && c.players.contains(&player) {
                        c.ready = false;
                    }
                }
                self.update_paused();
            }
            Command::Book => {
                replies.push(PacketC::Menu(Menu::Book(self.priv_gamedata.book.clone())))
            }
            Command::Item { name } => {
                let item = self
                    .game
                    .data
                    .get_item_by_name(&name)
                    .ok_or(tre!("s.error.item_not_found", s = name))?;
                self.packet_in(
                    replies,
                    PacketS::Communicate {
                        player,
                        pin: None,
                        timeout: None,
                        message: Some(Message::Item(item)),
                    },
                )?;
            }
            Command::Tile { name } => {
                let tile = self
                    .game
                    .data
                    .get_tile_by_name(&name)
                    .ok_or(tre!("s.error.no_tile", s = name))?;
                self.packet_in(
                    replies,
                    PacketS::Communicate {
                        player,
                        message: Some(Message::Tile(tile)),
                        timeout: None,
                        pin: None,
                    },
                )?;
            }
            Command::Scoreboard { map, text } => {
                let mapname = map.as_ref().unwrap_or(&self.game.data.metadata.name);
                let mapname_pretty = &self
                    .data
                    .maps
                    .iter()
                    .find(|m| m.name == *mapname)
                    .map(|m| &m.display_name)
                    .ok_or(tre!("s.error.scoreboard_disabled"))?;
                if let Some(board) = self.scoreboard.get(mapname) {
                    if text {
                        let mut o = format!("Scoreboard for {mapname_pretty}:\n");
                        for (i, entry) in board.best.iter().take(5).enumerate() {
                            writeln!(
                                o,
                                "  {i}. {} points by {}",
                                entry.score.points,
                                entry.players.clone().join(", ")
                            )
                            .unwrap();
                        }
                        replies.push(PacketC::ServerMessage {
                            message: Message::Text(o),
                            error: false,
                        });
                    } else {
                        let mut board = board.to_owned();
                        board.map = Some(mapname_pretty.to_string());
                        replies.push(PacketC::Menu(Menu::Scoreboard(board)));
                    }
                } else {
                    replies.push(PacketC::ServerMessage {
                        message: trm!("c.menu.scoreboard.no_finish", s = mapname.to_string()),
                        error: false,
                    });
                }
            }
            Command::Info { map } => {
                let mapname = map.as_ref().unwrap_or(&self.game.data.metadata.name);
                let info = &self
                    .data
                    .maps
                    .iter()
                    .find(|m| m.name == *mapname)
                    .ok_or(tre!("s.error.no_info"))?;
                replies.push(PacketC::ServerMessage {
                    message: Message::Text(format!(
                        "{:?}\nRecommended player count: {}\nDifficulty: {}",
                        info.name, info.difficulty, info.players
                    )),
                    error: false,
                });
            }
            Command::StartTutorial { item } => {
                if self.entities.iter().any(|e| {
                    <dyn std::any::Any>::downcast_ref::<Tutorial>(e.as_ref())
                        .is_some_and(|t| t.player == player)
                }) {
                    return Err(tre!("s.error.tutorial_already_running"));
                }
                if matches!(
                    item.as_str(),
                    "book" | "map-selector" | "button:reject" | "button:accept"
                ) {
                    let tile = self
                        .game
                        .data
                        .get_tile_by_name(&item)
                        .ok_or(tre!("s.error.tile_not_found", s = item))?;
                    self.entities
                        .push(Box::new(Tutorial::new_button(player, tile)));
                } else {
                    let item = self
                        .game
                        .data
                        .get_item_by_name(&item)
                        .ok_or(tre!("s.error.item_not_found", s = item))?;
                    self.entities
                        .push(Box::new(Tutorial::new_item(player, item)));
                }
            }
            Command::EndTutorial => {
                if let Some(tutorial) = self
                    .entities
                    .iter_mut()
                    .find_map(|e| <dyn std::any::Any>::downcast_mut::<Tutorial>(e.as_mut()))
                {
                    tutorial.ended = true;
                } else {
                    return Err(tre!("s.error.tutorial_no_running"));
                }
            }
            Command::TranslateMessage {
                message_id,
                arguments,
            } => {
                self.packet_out.push_back(PacketC::Communicate {
                    player,
                    message: Some(Message::Translation {
                        id: message_id,
                        params: arguments.into_iter().map(Message::Text).collect(),
                    }),
                    timeout: None,
                });
            }
            Command::Demands => {
                replies.push(PacketC::ServerMessage {
                    message: Message::Text(
                        self.game
                            .data
                            .demands
                            .iter()
                            .map(|d| self.game.data.item_name(d.input).to_owned())
                            .collect::<Vec<String>>()
                            .join("\n"),
                    ),
                    error: false,
                });
            }
            Command::Effect { name } => {
                let effect = match name.as_str() {
                    "angry" => Effect::Angry,
                    "satisfied" => Effect::Satisfied,
                    _ => return Err(tre!("s.error.no_effect")),
                };
                self.packet_out.push_back(PacketC::Effect {
                    effect,
                    location: ItemLocation::Player(player, Hand(0)),
                });
            }
            #[cfg(feature = "cheats")]
            Command::CreateBot { algo, name } => {
                let (aname, cons) = ALGO_CONSTRUCTORS
                    .iter()
                    .find(|(name, _)| *name == algo.as_str())
                    .ok_or(tre!("s.error.algo_not_found", s = algo))?;
                self.entities
                    .push(Box::new(crate::entity::bot::BotDriver::new(
                        format!("{}-bot", name.unwrap_or((*aname).to_owned())),
                        hurrycurry_protocol::Character {
                            color: 0,
                            hairstyle: 0,
                            headwear: 0,
                        },
                        hurrycurry_protocol::PlayerClass::Bot,
                        cons,
                    )));
            }
            #[cfg(feature = "cheats")]
            Command::SetItem { name } => {
                use hurrycurry_game_core::Item;
                use hurrycurry_protocol::{Hand, ItemLocation};

                let item = self
                    .game
                    .data
                    .get_item_by_name(&name)
                    .ok_or(tre!("s.error.item_not_found", s = name))?;
                let player_data = self
                    .game
                    .players
                    .get_mut(&player)
                    .ok_or(tre!("s.error.no_player"))?;
                player_data.items[0] = Some(Item {
                    active: None,
                    kind: item,
                });
                self.game.events.push_back(PacketC::SetItem {
                    location: ItemLocation::Player(player, Hand(0)),
                    item: Some(item),
                });
            }
            #[cfg(feature = "cheats")]
            Command::SetTile { name } => {
                let tile = self
                    .game
                    .data
                    .get_tile_by_name(&name)
                    .ok_or(tre!("s.error.no_tile", s = name))?;
                let pos = self
                    .game
                    .players
                    .get(&player)
                    .ok_or(tre!("s.error.no_player"))?
                    .movement
                    .position
                    .as_ivec2();
                self.game.add_tile_part(pos, tile);
            }
        }
        Ok(())
    }
}

#[derive(Debug, Clone)]
struct YesNo(bool);
impl FromStr for YesNo {
    type Err = &'static str;
    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        Ok(match s {
            "y" | "yes" | "true" | "1" => Self(true),
            "n" | "no" | "false" | "0" => Self(false),
            _ => return Err("expected y, yes, true, 1 or their opposites"),
        })
    }
}
