/*
    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::{
    entity::{bot::BotDriver, 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::{Character, Menu, Message, PacketC, PlayerClass, PlayerID};
use std::{fmt::Write, time::Duration};

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

        /// Skip announement and pause at game start
        #[arg(short = 's', long)]
        skip_announce: bool,

        /// Duration in seconds
        timer: Option<u64>,
    },
    /// 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 = "mapinfo")]
    Info {
        /// Name of the map, default: current
        map: Option<String>,
    },
    /// Abort the current game
    End,
    /// Download recipe/map's source declaration
    Download {
        /// Resource kind
        #[arg(value_enum)]
        r#type: DownloadType,
        /// Name
        name: String,
    },
    /// List all recipes and maps
    List,
    /// Send an effect
    Effect { name: String },
    /// Send an item message
    Item { name: String },
    /// Send a tile message
    Tile { name: String },
    /// Show all possible demands for this map
    Demands,
    /// Ready yourself
    Ready {
        #[arg(short, long)]
        force: bool,
        #[arg(short, long)]
        unready: bool,
    },
    /// Reload the resource index
    ReloadIndex,
    #[clap(alias = "summon", alias = "bot")]
    CreateBot { algo: String, name: Option<String> },
    /// 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>,
    },
    /// Return to the map editor
    #[clap(alias = "e", alias = "editor")]
    Edit,
    #[clap(hide = true)]
    SetEditorAddress { url: 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 {
                spec,
                timer,
                skip_announce,
            } => {
                if !self.game.lobby {
                    self.tx
                        .send(PacketC::ServerMessage {
                            message: trm!(
                                "s.state.game_aborted",
                                s = self
                                    .game
                                    .players
                                    .get(&player)
                                    .ok_or(tre!("s.error.no_player"))?
                                    .name
                                    .clone()
                            ),
                            error: false,
                        })
                        .ok();
                }
                let data = self
                    .index
                    .generate_with_book(&spec)
                    .map_err(|e| TrError::Plain(e.to_string()))?;
                self.load(data, timer.map(Duration::from_secs));
                if skip_announce {
                    self.announce_state = AnnounceState::Done
                }
            }
            Command::End => {
                self.tx
                    .send(PacketC::ServerMessage {
                        message: trm!(
                            "s.state.game_aborted",
                            s = self
                                .game
                                .players
                                .get(&player)
                                .ok_or(tre!("s.error.no_player"))?
                                .name
                                .clone()
                        ),
                        error: false,
                    })
                    .ok();
                self.load(
                    self.index
                        .generate_with_book("lobby")
                        .map_err(|e| TrError::Plain(e.to_string()))?,
                    None,
                );
            }
            Command::Reload => {
                if self.count_chefs() > 1 {
                    return Err(tre!("s.error.must_be_alone"));
                }
                self.load(
                    self.index
                        .generate_with_book(&self.game.data.current_map)
                        .map_err(|e| TrError::Plain(e.to_string()))?,
                    None,
                );
                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::ReloadIndex => {
                self.index
                    .reload()
                    .map_err(|e| TrError::Plain(e.to_string()))?;
            }
            Command::Book => replies.push(PacketC::Menu(Menu::Book(self.data.book.clone()))),
            Command::Download { r#type, name } => {
                let source = match r#type {
                    DownloadType::Map => self.index.read_map(&name),
                    DownloadType::Recipes => self.index.read_recipes(&name),
                }
                .map_err(|e| TrError::Plain(e.to_string()))?;
                replies.push(PacketC::ServerMessage {
                    message: Message::Text(source),
                    error: false,
                });
            }
            Command::List => replies.push(PacketC::ServerMessage {
                message: Message::Text(format!(
                    "Maps: {:?}\nRecipes: {:?}",
                    self.index.maps.keys().collect::<Vec<_>>(),
                    self.index.recipes
                )),
                error: false,
            }),
            Command::Effect { name } => {
                self.tx.send(PacketC::Effect { name, player }).ok();
            }
            Command::Item { name } => {
                let item = self
                    .game
                    .data
                    .get_item_by_name(&name)
                    .ok_or(tre!("s.error.item_not_found", s = name))?;
                self.tx
                    .send(PacketC::Communicate {
                        player,
                        message: Some(Message::Item(item)),
                        timeout: None,
                    })
                    .ok();
            }
            Command::Tile { name } => {
                let tile = self
                    .game
                    .data
                    .get_tile_by_name(&name)
                    .ok_or(tre!("s.error.no_tile", s = name))?;
                self.tx
                    .send(PacketC::Communicate {
                        player,
                        message: Some(Message::Tile(tile)),
                        timeout: None,
                    })
                    .ok();
            }
            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))?;
                let algo = cons();
                self.entities.push(Box::new(BotDriver::new(
                    format!("{}-bot", name.unwrap_or((*aname).to_owned())),
                    Character {
                        color: 0,
                        hairstyle: 0,
                        headwear: 0,
                    },
                    PlayerClass::Bot,
                    algo,
                )));
            }
            Command::Scoreboard { map, text } => {
                let mapname = map.as_ref().unwrap_or(&self.game.data.current_map);

                if let Some(board) = self.scoreboard.get(mapname) {
                    if text {
                        let mut o = format!("Scoreboard for {mapname}:\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.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.current_map);
                let info = self
                    .index
                    .maps
                    .get(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 } => {
                let item = self
                    .game
                    .data
                    .get_item_by_name(&item)
                    .ok_or(tre!("s.error.item_not_found", s = 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"));
                }
                self.entities.push(Box::new(Tutorial::new(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::Edit => {
                let addr = self
                    .editor_address
                    .clone()
                    .ok_or(tre!("s.error.not_editor_session"))?;
                replies.push(PacketC::Redirect { uri: vec![addr] });
            }
            Command::SetEditorAddress { url } => {
                self.editor_address = Some(url);
            }
            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,
                });
            }
        }
        Ok(())
    }
}
