use super::challenge::Challenge;
use super::trade::Trade;
use crate::core::planet::Planet;
use crate::core::position::{GamePosition, NUM_GAME_POSITIONS};
use crate::core::skill::Skill;
use crate::game_engine::timer::Timer;
use crate::game_engine::types::GameStats;
use crate::game_engine::{Tournament, TournamentId};
use crate::network::network_store_data::NetworkStoreData;
use crate::types::{HashMapWithResult, PlanetId, PlayerMap, Tick};
use crate::{
    core::{player::Player, team::Team, world::World},
    game_engine::types::TeamInGame,
    types::{AppResult, GameId, TeamId},
};
use anyhow::anyhow;
use itertools::Itertools;
use libp2p::PeerId;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::Debug;
use strum_macros::Display;

#[derive(Debug, Display, Serialize, Deserialize, Clone, PartialEq)]
pub enum TournamentRequestState {
    RegistrationRequest,
    RegistrationDeclined { reason: String },
    RegistrationOk,
    ConfirmationRequest,
    ConfirmationDeclined { reason: String },
    ParticipationRequest,
    ParticipationDeclined { reason: String },
    ParticipationOk,
    Cancellation { reason: String },
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[allow(clippy::large_enum_variant)]
pub enum NetworkData {
    Team {
        timestamp: Tick,
        team: NetworkTeam,
    },
    Challenge {
        timestamp: Tick,
        challenge: Challenge,
    },
    Trade {
        timestamp: Tick,
        trade: Trade,
    },
    Message {
        timestamp: Tick,
        from_peer_id: PeerId,
        author: String,
        message: String,
    },
    Game {
        timestamp: Tick,
        game: NetworkGame,
    },
    SeedInfo {
        timestamp: Tick,
        seed_info: SeedInfo,
    },
    RelayerMessageToTeam {
        timestamp: Tick,
        message: String,
        team_id: Option<TeamId>,
    },
    SyncRequest,
    TournamentRegistrationRequest {
        timestamp: Tick,
        tournament_id: TournamentId,
        team_id: TeamId,
        team_data: Option<(Team, PlayerMap)>,
        request_state: TournamentRequestState,
    },
    Tournament {
        timestamp: Tick,
        tournament: Tournament,
    },
}

#[derive(Debug, Clone, Display, Default, Serialize, Deserialize, PartialEq, Hash)]
pub enum NetworkRequestState {
    #[default]
    Syn,
    SynAck,
    Ack,
    Failed {
        error_message: String,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct NetworkTeam {
    pub team: Team,
    pub players: PlayerMap,
    pub asteroids: Vec<Planet>,
}

impl NetworkTeam {
    pub fn new(team: Team, players: PlayerMap, asteroids: Vec<Planet>) -> Self {
        Self {
            team,
            players,
            asteroids,
        }
    }

    pub fn from_team_id(world: &World, team_id: &TeamId, peer_id: PeerId) -> AppResult<Self> {
        let mut team = world.teams.get_or_err(team_id)?.clone();
        let mut players = World::get_team_players(&world.players, &team)?;
        let asteroids = team
            .asteroid_ids
            .iter()
            .map(|asteroid_id| {
                let mut asteroid = world
                    .planets
                    .get_or_err(asteroid_id)
                    .expect("Asteroid should be part of world")
                    .clone();
                asteroid.peer_id = Some(peer_id);
                asteroid
            })
            .collect_vec();

        // Set the peer_id on players and convert the Vec<&Player> into a PlayerMap
        let mut team_players = PlayerMap::new();
        for player in players.iter_mut() {
            let mut team_player = player.clone();
            team_player.peer_id = Some(peer_id);
            team_players.insert(player.id, team_player);
        }

        // Set the peer_id for team we are sending out
        // This means that the team can be challenged online and it will not be stored.
        team.peer_id = Some(peer_id);

        Ok(Self::new(team, team_players, asteroids))
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct NetworkGame {
    pub id: GameId,
    pub home_team_in_game: TeamInGame,
    pub away_team_in_game: TeamInGame,
    pub location: PlanetId,
    pub attendance: u32,
    pub starting_at: Tick,
    pub timer: Timer,
    pub part_of_tournament: Option<TournamentId>,
}

impl NetworkGame {
    pub fn from_game_id(world: &World, game_id: &GameId) -> AppResult<Self> {
        let game = world.games.get_or_err(game_id)?.clone();

        let mut home_team_in_game = game.home_team_in_game.clone();
        // Reset stats
        let mut stats = HashMap::new();
        for (idx, player_id) in home_team_in_game.initial_positions.iter().enumerate() {
            // Set position in stats to initial one
            let mut player_stats = GameStats::default();
            if (idx as GamePosition) < NUM_GAME_POSITIONS {
                player_stats.position = Some(idx as GamePosition);
            }
            stats.insert(*player_id, player_stats.clone());

            let player = home_team_in_game
                .players
                .get_mut(player_id)
                .ok_or_else(|| anyhow!("Cannot get player for home team in game".to_string()))?;
            // Reset tiredness to initial one
            let tiredness = home_team_in_game.initial_tiredness[idx];
            player.tiredness = tiredness;
            // Reset morale to initial one
            let morale = home_team_in_game.initial_morale[idx];
            player.morale = morale;
            // Reset drunkenness to initial one.
            // Safe indexing since games from older versions have an empty vector.
            player.drunkenness = home_team_in_game
                .initial_drunkenness
                .get(idx)
                .copied()
                .unwrap_or_default();
        }
        home_team_in_game.stats = stats;
        home_team_in_game.rum = home_team_in_game.initial_rum;

        let mut away_team_in_game = game.away_team_in_game.clone();
        let mut stats = HashMap::new();
        for (idx, player_id) in away_team_in_game.initial_positions.iter().enumerate() {
            let mut player_stats = GameStats::default();
            if (idx as GamePosition) < NUM_GAME_POSITIONS {
                player_stats.position = Some(idx as GamePosition);
            }
            stats.insert(*player_id, player_stats.clone());

            let player = away_team_in_game
                .players
                .get_mut(player_id)
                .ok_or_else(|| anyhow!("Cannot get player for away team in game".to_string()))?;
            // Reset tiredness to initial one
            let tiredness = away_team_in_game.initial_tiredness[idx];
            player.tiredness = tiredness;
            // Reset morale to initial one
            let morale = away_team_in_game.initial_morale[idx];
            player.morale = morale;
            // Reset drunkenness to initial one.
            // Safe indexing since games from older versions have an empty vector.
            player.drunkenness = away_team_in_game
                .initial_drunkenness
                .get(idx)
                .copied()
                .unwrap_or_default();
        }
        away_team_in_game.stats = stats;
        away_team_in_game.rum = away_team_in_game.initial_rum;

        Ok(Self {
            id: game.id,
            home_team_in_game,
            away_team_in_game,
            location: game.location,
            attendance: game.attendance,
            starting_at: game.starting_at,
            timer: game.timer,
            part_of_tournament: game.part_of_tournament,
        })
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TeamRanking {
    pub team: Team,
    pub timestamp: Tick,
    pub player_ratings: Vec<Skill>,
}

impl TeamRanking {
    pub fn from_network_team(timestamp: Tick, network_team: &NetworkTeam) -> Self {
        Self {
            team: network_team.team.clone(),
            timestamp,
            player_ratings: network_team
                .players
                .values()
                .map(|p| p.average_skill())
                .collect_vec(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PlayerRanking {
    pub player: Player,
    pub team_name: String,
    pub timestamp: Tick,
}

impl PlayerRanking {
    pub fn new(timestamp: Tick, player: Player, team_name: String) -> Self {
        Self {
            player,
            team_name,
            timestamp,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct ChatHistoryEntry {
    pub timestamp: Tick,
    pub from_peer_id: PeerId,
    pub author: String,
    pub message: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SeedInfo {
    pub version: [usize; 3],
    #[serde(default)]
    pub network_store_data: NetworkStoreData,
}

impl SeedInfo {
    pub fn new(network_store_data: NetworkStoreData) -> AppResult<Self> {
        Ok(Self {
            version: [
                env!("CARGO_PKG_VERSION_MAJOR").parse()?,
                env!("CARGO_PKG_VERSION_MINOR").parse()?,
                env!("CARGO_PKG_VERSION_PATCH").parse()?,
            ],
            network_store_data,
        })
    }
}
