use super::{
    constants::*,
    jersey::Jersey,
    position::{GamePosition, GamePositionUtils, NUM_GAME_POSITIONS},
    resources::Resource,
    role::CrewRole,
    skill::*,
    types::{PlayerLocation, Population, Pronoun, Region, TrainingFocus},
    utils::{skill_linear_interpolation, PLAYER_DATA},
    world::World,
};
use crate::{
    core::{PlayerOpinion, PlayerOpinionMap, PlayerOpinionMapDescription},
    game_engine::types::GameStats,
    image::{player::PlayerImage, utils::Gif},
    types::{
        AppResult, HashMapWithResult, PlanetId, PlayerId, StorableResourceMap, SystemTimeTick,
        TeamId, TeamMap, Tick,
    },
};
use anyhow::anyhow;
use itertools::Itertools;
use libp2p::PeerId;
use rand::{
    seq::{IndexedRandom, IteratorRandom, SliceRandom},
    RngExt, SeedableRng,
};
use rand_chacha::ChaCha8Rng;
use rand_distr::{
    num_traits::{Signed, Zero},
    Distribution, Normal,
};
use serde::{de::Visitor, ser::SerializeStruct, Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use strum_macros::Display;

const HOOK_MAX_BALL_HANDLING: f32 = 4.0;
const EYE_PATCH_MAX_VISION: f32 = 4.0;
const WOODEN_LEG_MAX_QUICKNESS: f32 = 4.0;

#[derive(Debug, Clone, Default, PartialEq)]
struct PlayerBuildData {
    position: Option<GamePosition>,
    base_level: f32,
    extra_potential: Skill,
    population: Option<Population>,
    opinions: PlayerOpinionMap,
}

#[derive(Debug, Clone, PartialEq)]
pub struct Player {
    pub id: PlayerId,
    pub peer_id: Option<PeerId>,
    pub version: u64,
    pub info: InfoStats,
    pub team: Option<TeamId>,
    pub joined_team_on: Option<Tick>,
    pub special_trait: Option<Trait>,
    pub reputation: Skill,
    pub potential: Skill,
    pub athletics: Athletics,
    pub offense: Offense,
    pub defense: Defense,
    pub technical: Technical,
    pub mental: Mental,
    pub image: PlayerImage,
    pub current_location: PlayerLocation,
    pub skills_training: [Skill; 20],
    pub previous_skills: [Skill; 20], // This is for displaying purposes to show the skills that were recently modified
    pub previous_game_position_fitness: [Skill; NUM_GAME_POSITIONS as usize], // This is for displaying purposes to show the game positions that were recently modified
    pub game_position_fitness: [Skill; NUM_GAME_POSITIONS as usize],
    pub game_position_fitness_training: [Skill; NUM_GAME_POSITIONS as usize],
    pub training_focus: Option<TrainingFocus>,
    pub tiredness: Skill,
    pub morale: Skill,
    pub drunkenness: Skill,
    pub historical_stats: GameStats,
    build_data: PlayerBuildData, // Intermediate state used to build the random player. Not serialized
    pub opinions: PlayerOpinionMap,
}

impl Default for Player {
    fn default() -> Self {
        Player {
            id: PlayerId::new_v4(),
            peer_id: None,
            version: 0,
            info: InfoStats::default(),
            team: None,
            joined_team_on: None,
            special_trait: None,
            reputation: Skill::default(),
            potential: Skill::default(),
            athletics: Athletics::default(),
            offense: Offense::default(),
            defense: Defense::default(),
            technical: Technical::default(),
            mental: Mental::default(),
            image: PlayerImage::default(),
            current_location: PlayerLocation::default(),
            skills_training: [Skill::default(); 20],
            previous_skills: [Skill::default(); 20],
            previous_game_position_fitness: [Skill::default(); NUM_GAME_POSITIONS as usize],
            game_position_fitness: [Skill::default(); NUM_GAME_POSITIONS as usize],
            game_position_fitness_training: [Skill::default(); NUM_GAME_POSITIONS as usize],
            training_focus: None,
            tiredness: Skill::default(),
            morale: Skill::default(),
            drunkenness: Skill::default(),
            historical_stats: GameStats::default(),
            build_data: PlayerBuildData::default(),
            opinions: PlayerOpinionMap::default(),
        }
    }
}

impl Serialize for Player {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        // Don't serialize athletics, offense, technical, defense, mental
        // and serialize them in a vector which is then deserialized
        // into the corresponding fields
        let compact_skills = self.current_skill_array().to_vec();
        let mut state = serializer.serialize_struct("Player", 22)?;
        state.serialize_field("id", &self.id)?;
        state.serialize_field("peer_id", &self.peer_id)?;
        state.serialize_field("version", &self.version)?;
        state.serialize_field("info", &self.info)?;
        state.serialize_field("team", &self.team)?;
        state.serialize_field("joined_team_on", &self.joined_team_on)?;
        state.serialize_field("special_trait", &self.special_trait)?;
        state.serialize_field("reputation", &self.reputation)?;
        state.serialize_field("potential", &self.potential)?;
        state.serialize_field("image", &self.image)?;
        state.serialize_field("current_location", &self.current_location)?;
        state.serialize_field("previous_skills", &self.previous_skills)?;
        state.serialize_field("skills_training", &self.skills_training)?;
        state.serialize_field("tiredness", &self.tiredness)?;
        state.serialize_field("morale", &self.morale)?;
        state.serialize_field("drunkenness", &self.drunkenness)?;
        state.serialize_field("compact_skills", &compact_skills)?;
        state.serialize_field(
            "previous_game_position_fitness",
            &self.previous_game_position_fitness,
        )?;
        state.serialize_field("game_position_fitness", &self.game_position_fitness)?;
        state.serialize_field(
            "game_position_fitness_training",
            &self.game_position_fitness_training,
        )?;
        state.serialize_field("training_focus", &self.training_focus)?;
        state.serialize_field("historical_stats", &self.historical_stats)?;
        state.serialize_field("opinions", &self.opinions.iter().collect_vec())?;
        state.end()
    }
}

impl<'de> Deserialize<'de> for Player {
    // Deserialize compact_skills into the corresponding fields.
    // compact_skills is a vector of 20 skills.
    // The first 4 skills are athletics, the next 4 are offense, etc.
    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        enum Field {
            Id,
            PeerId,
            Version,
            Info,
            Team,
            JoinedTeamOn,
            SpecialTrait,
            Reputation,
            Potential,
            Image,
            CurrentLocation,
            PreviousSkills,
            PreviousGamePositionFitness,
            SkillsTraining,
            GamePositionFitness,
            GamePositionFitnessTraining,
            TrainingFocus,
            Tiredness,
            Morale,
            Drunkenness,
            CompactSkills,
            HistoricalStats,
            Opinions,
        }

        impl<'de> Deserialize<'de> for Field {
            fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
                struct FieldVisitor;

                impl<'de> Visitor<'de> for FieldVisitor {
                    type Value = Field;

                    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                        formatter.write_str("field name")
                    }

                    fn visit_str<E>(self, value: &str) -> Result<Field, E>
                    where
                        E: serde::de::Error,
                    {
                        match value {
                            "id" => Ok(Field::Id),
                            "peer_id" => Ok(Field::PeerId),
                            "version" => Ok(Field::Version),
                            "info" => Ok(Field::Info),
                            "team" => Ok(Field::Team),
                            "joined_team_on" => Ok(Field::JoinedTeamOn),
                            "special_trait" => Ok(Field::SpecialTrait),
                            "reputation" => Ok(Field::Reputation),
                            "potential" => Ok(Field::Potential),
                            "image" => Ok(Field::Image),
                            "current_location" => Ok(Field::CurrentLocation),
                            "previous_skills" => Ok(Field::PreviousSkills),
                            "previous_game_position_fitness" => {
                                Ok(Field::PreviousGamePositionFitness)
                            }
                            "skills_training" => Ok(Field::SkillsTraining),
                            "game_position_fitness" => Ok(Field::GamePositionFitness),
                            "game_position_fitness_training" => {
                                Ok(Field::GamePositionFitnessTraining)
                            }
                            "training_focus" => Ok(Field::TrainingFocus),
                            "tiredness" => Ok(Field::Tiredness),
                            "morale" => Ok(Field::Morale),
                            "drunkenness" => Ok(Field::Drunkenness),
                            "compact_skills" => Ok(Field::CompactSkills),
                            "historical_stats" => Ok(Field::HistoricalStats),
                            "opinions" => Ok(Field::Opinions),
                            _ => Err(serde::de::Error::unknown_field(value, FIELDS)),
                        }
                    }
                }

                deserializer.deserialize_identifier(FieldVisitor)
            }
        }

        struct PlayerVisitor;

        impl<'de> Visitor<'de> for PlayerVisitor {
            type Value = Player;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str("struct Player")
            }

            fn visit_map<V>(self, mut map: V) -> Result<Player, V::Error>
            where
                V: serde::de::MapAccess<'de>,
            {
                let mut id: Option<uuid::Uuid> = None;
                let mut peer_id = None;
                let mut version = None;
                let mut info = None;
                let mut team = None;
                let mut joined_team_on = None;
                let mut special_trait = None;
                let mut reputation = None;
                let mut potential = None;
                let mut image = None;
                let mut current_location = None;
                let mut skills_training = None;
                let mut previous_skills = None;
                let mut previous_game_position_fitness = None;
                let mut game_position_fitness = None;
                let mut game_position_fitness_training = None;
                let mut training_focus = None;
                let mut tiredness = None;
                let mut morale = None;
                let mut drunkenness = None;
                let mut compact_skills: Option<Vec<Skill>> = None;
                let mut historical_stats = None;
                let mut opinions = None;

                while let Some(key) = map.next_key()? {
                    match key {
                        Field::Id => {
                            if id.is_some() {
                                return Err(serde::de::Error::duplicate_field("id"));
                            }
                            id = Some(map.next_value()?);
                        }
                        Field::PeerId => {
                            if peer_id.is_some() {
                                return Err(serde::de::Error::duplicate_field("peer_id"));
                            }
                            peer_id = Some(map.next_value()?);
                        }
                        Field::Version => {
                            if version.is_some() {
                                return Err(serde::de::Error::duplicate_field("version"));
                            }
                            version = Some(map.next_value()?);
                        }
                        Field::Info => {
                            if info.is_some() {
                                return Err(serde::de::Error::duplicate_field("info"));
                            }
                            info = Some(map.next_value()?);
                        }
                        Field::Team => {
                            if team.is_some() {
                                return Err(serde::de::Error::duplicate_field("team"));
                            }
                            team = Some(map.next_value()?);
                        }
                        Field::JoinedTeamOn => {
                            if joined_team_on.is_some() {
                                return Err(serde::de::Error::duplicate_field("joined_team_on"));
                            }
                            joined_team_on = Some(map.next_value()?);
                        }
                        Field::SpecialTrait => {
                            if special_trait.is_some() {
                                return Err(serde::de::Error::duplicate_field("special_trait"));
                            }
                            special_trait = Some(map.next_value()?);
                        }
                        Field::Reputation => {
                            if reputation.is_some() {
                                return Err(serde::de::Error::duplicate_field("reputation"));
                            }
                            reputation = Some(map.next_value()?);
                        }
                        Field::Potential => {
                            if potential.is_some() {
                                return Err(serde::de::Error::duplicate_field("potential"));
                            }
                            potential = Some(map.next_value()?);
                        }
                        Field::Image => {
                            if image.is_some() {
                                return Err(serde::de::Error::duplicate_field("image"));
                            }
                            image = Some(map.next_value()?);
                        }
                        Field::CurrentLocation => {
                            if current_location.is_some() {
                                return Err(serde::de::Error::duplicate_field("current_location"));
                            }
                            current_location = Some(map.next_value()?);
                        }
                        Field::SkillsTraining => {
                            if skills_training.is_some() {
                                return Err(serde::de::Error::duplicate_field("skills_training"));
                            }
                            skills_training = Some(map.next_value()?);
                        }
                        Field::PreviousSkills => {
                            if previous_skills.is_some() {
                                return Err(serde::de::Error::duplicate_field("previous_skills"));
                            }
                            previous_skills = Some(map.next_value()?);
                        }
                        Field::PreviousGamePositionFitness => {
                            if previous_game_position_fitness.is_some() {
                                return Err(serde::de::Error::duplicate_field(
                                    "previous_game_position_fitness",
                                ));
                            }
                            previous_game_position_fitness = Some(map.next_value()?);
                        }
                        Field::GamePositionFitness => {
                            if game_position_fitness.is_some() {
                                return Err(serde::de::Error::duplicate_field(
                                    "game_position_fitness",
                                ));
                            }
                            game_position_fitness = Some(map.next_value()?);
                        }
                        Field::GamePositionFitnessTraining => {
                            if game_position_fitness_training.is_some() {
                                return Err(serde::de::Error::duplicate_field(
                                    "game_position_fitness_training",
                                ));
                            }
                            game_position_fitness_training = Some(map.next_value()?);
                        }
                        Field::TrainingFocus => {
                            if training_focus.is_some() {
                                return Err(serde::de::Error::duplicate_field("training_focus"));
                            }
                            training_focus = Some(map.next_value()?);
                        }
                        Field::Tiredness => {
                            if tiredness.is_some() {
                                return Err(serde::de::Error::duplicate_field("tiredness"));
                            }
                            tiredness = Some(map.next_value()?);
                        }
                        Field::Morale => {
                            if morale.is_some() {
                                return Err(serde::de::Error::duplicate_field("morale"));
                            }
                            morale = Some(map.next_value()?);
                        }
                        Field::Drunkenness => {
                            if drunkenness.is_some() {
                                return Err(serde::de::Error::duplicate_field("drunkenness"));
                            }
                            drunkenness = Some(map.next_value()?);
                        }
                        Field::CompactSkills => {
                            if compact_skills.is_some() {
                                return Err(serde::de::Error::duplicate_field("compact_skills"));
                            }
                            compact_skills = Some(map.next_value()?);
                        }

                        Field::HistoricalStats => {
                            if historical_stats.is_some() {
                                return Err(serde::de::Error::duplicate_field("historical_statis"));
                            }
                            historical_stats = Some(map.next_value()?);
                        }

                        Field::Opinions => {
                            if opinions.is_some() {
                                return Err(serde::de::Error::duplicate_field("opinions"));
                            }
                            opinions = Some(
                                map.next_value::<Vec<(PlayerOpinion, (Tick, Skill))>>()?
                                    .into_iter()
                                    .collect(),
                            );
                        }
                    }
                }

                let id = id.ok_or_else(|| serde::de::Error::missing_field("id"))?;
                let peer_id = peer_id.ok_or_else(|| serde::de::Error::missing_field("peer_id"))?;
                let version = version.ok_or_else(|| serde::de::Error::missing_field("version"))?;
                let info = info.ok_or_else(|| serde::de::Error::missing_field("info"))?;
                let team = team.ok_or_else(|| serde::de::Error::missing_field("team"))?;
                let joined_team_on = joined_team_on.unwrap_or_default();
                let special_trait = special_trait
                    .ok_or_else(|| serde::de::Error::missing_field("special_trait"))?;
                let reputation =
                    reputation.ok_or_else(|| serde::de::Error::missing_field("reputation"))?;
                let potential =
                    potential.ok_or_else(|| serde::de::Error::missing_field("potential"))?;
                let image = image.ok_or_else(|| serde::de::Error::missing_field("image"))?;
                let current_location = current_location
                    .ok_or_else(|| serde::de::Error::missing_field("current_location"))?;
                let skills_training = skills_training
                    .ok_or_else(|| serde::de::Error::missing_field("skills_training"))?;
                let previous_skills = previous_skills
                    .ok_or_else(|| serde::de::Error::missing_field("previous_skills"))?;

                let game_position_fitness = game_position_fitness.unwrap_or_default();
                // If absent (old save), fall back to the current fitness so nothing is
                // spuriously highlighted as a recent change.
                let previous_game_position_fitness =
                    previous_game_position_fitness.unwrap_or(game_position_fitness);
                let game_position_fitness_training =
                    game_position_fitness_training.unwrap_or_default();
                let training_focus = training_focus.unwrap_or_default();
                let tiredness =
                    tiredness.ok_or_else(|| serde::de::Error::missing_field("tiredness"))?;
                let morale = morale.ok_or_else(|| serde::de::Error::missing_field("morale"))?;
                let drunkenness = drunkenness.unwrap_or_default();
                let compact_skills = compact_skills
                    .ok_or_else(|| serde::de::Error::missing_field("compact_skills"))?;
                let historical_stats = historical_stats.unwrap_or_default();
                let opinions = opinions.unwrap_or_default();

                let mut player = Player {
                    id,
                    peer_id,
                    version,
                    info,
                    team,
                    joined_team_on,
                    special_trait,
                    reputation,
                    potential,
                    athletics: Athletics::default(),
                    offense: Offense::default(),
                    defense: Defense::default(),
                    technical: Technical::default(),
                    mental: Mental::default(),
                    image,
                    current_location,
                    skills_training,
                    previous_skills,
                    previous_game_position_fitness,
                    game_position_fitness,
                    game_position_fitness_training,
                    training_focus,
                    tiredness,
                    morale,
                    drunkenness,
                    historical_stats,
                    build_data: PlayerBuildData::default(),
                    opinions,
                };

                player.athletics = Athletics {
                    quickness: compact_skills[0],
                    vertical: compact_skills[1],
                    strength: compact_skills[2],
                    stamina: compact_skills[3],
                };
                player.offense = Offense {
                    brawl: compact_skills[4],
                    close_range: compact_skills[5],
                    medium_range: compact_skills[6],
                    long_range: compact_skills[7],
                };
                player.defense = Defense {
                    steal: compact_skills[8],
                    block: compact_skills[9],
                    perimeter_defense: compact_skills[10],
                    interior_defense: compact_skills[11],
                };
                player.technical = Technical {
                    passing: compact_skills[12],
                    ball_handling: compact_skills[13],
                    post_moves: compact_skills[14],
                    rebounds: compact_skills[15],
                };
                player.mental = Mental {
                    vision: compact_skills[16],
                    aggression: compact_skills[17],
                    intuition: compact_skills[18],
                    charisma: compact_skills[19],
                };

                Ok(player)
            }
        }

        const FIELDS: &[&str] = &[
            "id",
            "peer_id",
            "version",
            "info",
            "team",
            "joined_team_on",
            "special_trait",
            "reputation",
            "potential",
            "image",
            "current_location",
            "skills_training",
            "previous_skills",
            "previous_game_position_fitness",
            "game_position_fitness",
            "training_focus",
            "tiredness",
            "morale",
            "drunkenness",
            "compact_skills",
            "historical_stats",
            "opinions",
        ];
        deserializer.deserialize_struct("Player", FIELDS, PlayerVisitor)
    }
}

impl Player {
    pub fn randomize(mut self, rng: Option<&mut ChaCha8Rng>) -> Self {
        let rng = if let Some(r) = rng {
            r
        } else {
            &mut ChaCha8Rng::from_rng(&mut rand::rng())
        };

        let mut build_base_level = self.build_data.base_level;
        let position = if let Some(pos) = self.build_data.position {
            pos
        } else {
            rng.random_range(0..NUM_GAME_POSITIONS)
        };

        self.info.population = self.build_data.population.unwrap_or_else(|| {
            Population::all_peoples()
                .into_iter()
                .choose(rng)
                .unwrap_or_default()
        });

        self.info.randomize_for_position(position, rng);

        // Base level modifier increases linearly from (0,0) to (PEAK_PERFORMANCE_RELATIVE_AGE, 1.0),
        // then decreases linearly from (PEAK_PERFORMANCE_RELATIVE_AGE, 1.0) to (1.0, 0.0).
        let base_level_modifier = if PEAK_PERFORMANCE_RELATIVE_AGE >= self.info.relative_age() {
            self.info.relative_age() / PEAK_PERFORMANCE_RELATIVE_AGE
        } else {
            (self.info.relative_age() - 1.0) / (PEAK_PERFORMANCE_RELATIVE_AGE - 1.0)
        };
        build_base_level *= base_level_modifier;

        self.athletics = Athletics::for_position(position, rng, build_base_level);
        self.offense = Offense::for_position(position, rng, build_base_level);
        self.technical = Technical::for_position(position, rng, build_base_level);
        self.defense = Defense::for_position(position, rng, build_base_level);
        self.mental = Mental::for_position(position, rng, build_base_level);

        self.image = PlayerImage::from_info(&self.info, rng);

        self.apply_population_skill_modifiers();
        self.apply_info_skill_modifiers();

        if self.athletics.quickness < WOODEN_LEG_MAX_QUICKNESS {
            self.image.set_wooden_leg(rng);
            self.mental.charisma = (self.mental.charisma + 1.25).bound();
            self.technical.post_moves = (self.technical.post_moves + 0.75).bound();
        }
        if self.mental.vision < EYE_PATCH_MAX_VISION {
            self.image.set_eye_patch(rng, self.info.population);
            self.mental.charisma = (self.mental.charisma + 2.0).bound();
        }

        if self.technical.ball_handling < HOOK_MAX_BALL_HANDLING {
            self.image.set_hook(rng, self.info.population);
            self.athletics.strength = (self.athletics.strength + 1.25).bound();
            self.mental.charisma = (self.mental.charisma + 0.75).bound();
        }

        if self.athletics.strength > 15.0 && rng.random_bool(TRAIT_PROBABILITY) {
            self.special_trait = Some(Trait::Killer);
        } else if self.mental.charisma > 15.0 && rng.random_bool(TRAIT_PROBABILITY) {
            self.special_trait = Some(Trait::Showpirate);
        } else if self.mental.intuition > 10.0 && rng.random_bool(TRAIT_PROBABILITY) {
            self.special_trait = Some(Trait::Spugna);
        } else if self.athletics.stamina > 15.0 && rng.random_bool(TRAIT_PROBABILITY) {
            self.special_trait = Some(Trait::Relentless);
        }

        self.previous_skills = self.current_skill_array();

        self.set_initial_game_position_fitness(Some(rng));
        self.previous_game_position_fitness = self.game_position_fitness;
        self.set_initial_opinions();

        let extra_potential = {
            // Extra potential has a variance that depends on current age
            let mut std_dev = 1.0 + 2.0 * (1.0 - self.info.relative_age());
            if self.build_data.extra_potential > Skill::default() {
                std_dev += self.build_data.extra_potential;
            }
            let normal =
                Normal::new(0.0, std_dev).expect("Should create valid normal distribution");
            normal.sample(rng).abs()
        };

        self.potential =
            (self.average_skill() * AVG_SKILL_TO_POTENTIAL_MODIFIER + extra_potential).bound();
        self.reputation = (self.average_skill() / 6.0 + self.info.relative_age() * 4.5).bound();

        self
    }

    pub fn with_name(
        mut self,
        first_name: impl Into<String>,
        last_name: impl Into<String>,
    ) -> Self {
        self.info.first_name = first_name.into();
        self.info.last_name = last_name.into();
        self
    }

    pub fn with_opinion(mut self, opinion: PlayerOpinion, value: Skill) -> Self {
        self.build_data
            .opinions
            .insert(opinion, (Tick::now(), value));
        self
    }

    pub fn with_current_location_on_planet(mut self, planet_id: PlanetId) -> Self {
        self.current_location = if self.team.is_some() {
            PlayerLocation::WithTeam
        } else {
            PlayerLocation::OnPlanet { planet_id }
        };

        self
    }

    pub fn with_population(mut self, population: Population) -> Self {
        self.info.population = population;
        self.build_data.population = Some(population);

        self
    }

    pub fn with_position(mut self, position: Option<GamePosition>) -> Self {
        self.build_data.position = position;

        self
    }

    pub fn with_base_level(mut self, base_level: f32) -> Self {
        self.build_data.base_level = base_level;

        self
    }

    pub fn with_extra_potential(mut self, extra_potential: Skill) -> Self {
        self.build_data.extra_potential = extra_potential;

        self
    }

    pub fn set_initial_opinions(&mut self) {
        const MAX_INITIAL_OPINIONS: usize = 4;
        let rng = &mut ChaCha8Rng::seed_from_u64(self.id.as_u64_pair().1);

        let mut candidates: Vec<(PlayerOpinion, Skill)> = vec![];

        for population in Population::all_peoples() {
            if matches!(population, Population::Human { .. }) {
                continue;
            }
            let value = if population == self.info.population {
                match self.info.population {
                    Population::Galdari => MAX_SKILL,
                    _ => 18.0,
                }
            } else if self.info.population == Population::Galdari {
                OPINION_NEUTRAL_VALUE - rng.random_range(0.0..4.0)
            } else {
                continue;
            };
            candidates.push((PlayerOpinion::Populations { population }, value));
        }

        if self.info.population == Population::Galdari {
            candidates.push((
                PlayerOpinion::AllHumans,
                OPINION_NEUTRAL_VALUE - rng.random_range(0.0..4.0),
            ));
        }

        let std_dev = 3.0;
        let mut values = [5.0, 10.0, 15.0].map(|mean| {
            Normal::new(mean, std_dev)
                .expect("Should create valid normal distribution")
                .sample(rng)
                .bound()
        });
        values.shuffle(rng);
        candidates.push((PlayerOpinion::Space, values[0]));
        candidates.push((PlayerOpinion::Drinking, values[1]));
        candidates.push((PlayerOpinion::Games, values[2]));

        let money_value = {
            let normal = Normal::new(2.0, 2.0).expect("Should create valid normal distribution");
            (OPINION_NEUTRAL_VALUE + normal.sample(rng)).bound()
        };
        candidates.push((PlayerOpinion::Gold, money_value));

        candidates.sort_by(|(_, a), (_, b)| {
            (b - OPINION_NEUTRAL_VALUE)
                .abs()
                .total_cmp(&(a - OPINION_NEUTRAL_VALUE).abs())
        });

        for (opinion, value) in candidates.into_iter().take(MAX_INITIAL_OPINIONS) {
            self.opinions.insert(opinion, (Tick::now(), value));
        }

        for (opinion, (_, value)) in self.build_data.opinions.iter() {
            self.opinions.insert(opinion.clone(), (Tick::now(), *value));
        }

        if self.team.is_some() {
            self.opinions
                .insert(PlayerOpinion::OwnTeam, (Tick::now(), OPINION_NEUTRAL_VALUE));
        }
    }

    pub fn population_opinion_modifier(&self, target: Population) -> f32 {
        let opinion = if matches!(target, Population::Human { .. }) {
            PlayerOpinion::AllHumans
        } else {
            PlayerOpinion::Populations { population: target }
        };
        self.opinions.modifier(opinion)
    }

    fn set_initial_game_position_fitness(&mut self, rng: Option<&mut ChaCha8Rng>) {
        let rng = if let Some(r) = rng {
            r
        } else {
            &mut ChaCha8Rng::from_rng(&mut rand::rng())
        };
        let positions_by_skill_fitness = (0..NUM_GAME_POSITIONS)
            .sorted_by(|&a, &b| {
                self.position_skill_rating(b)
                    .partial_cmp(&self.position_skill_rating(a))
                    .expect("There should be an ordering")
            })
            .collect_vec();

        let std_dev = 4.0;
        for rank in 0..NUM_GAME_POSITIONS {
            // NOTE: this function is called only during initialization, so the relevant value is the intuition at initialization
            // (potential does not change).
            let position = positions_by_skill_fitness[rank as usize];
            let bonus_fitness = 3.0 * (0.5 * self.mental.intuition + 0.5 * self.potential)
                / MAX_SKILL
                + self.info.population.bonus_game_position_fitness(position);
            let mean = BASE_FITNESS[rank as usize] + bonus_fitness; //Can be larger than MAX_SKILL
            let normal =
                Normal::new(mean, std_dev).expect("Should create valid normal distribution");
            let value = normal.sample(rng).bound();
            self.game_position_fitness[position as usize] = value;
        }
    }

    // Skill-only rating, ignoring game position fitness. Used to order positions
    // when assigning the initial fitness (which is not set yet at that point) and
    // by the game engine to apply the fitness with the fluidity exponent on top.
    pub(crate) fn position_skill_rating(&self, position: GamePosition) -> Skill {
        let mut rating = 0 as f32;
        let weights = position.weights();
        let mut total_weight = 0 as f32;
        for i in 0..self.current_skill_array().len() {
            let w = weights[i].powf(4.0);
            rating += w * self.current_skill_array()[i];
            total_weight += w;
        }

        (rating / total_weight).bound()
    }

    pub fn position_rating(&self, position: GamePosition) -> Skill {
        let fitness = self
            .game_position_fitness
            .get(position as usize)
            .copied()
            .unwrap_or_default()
            / MAX_SKILL;
        self.position_skill_rating(position) * fitness
    }

    pub fn best_position(&self) -> GamePosition {
        let mut best = 0;
        let mut best_rating = 0.0;
        for i in 0..NUM_GAME_POSITIONS {
            let rating = self.position_rating(i);
            if rating > best_rating {
                best = i;
                best_rating = rating;
            }
        }
        best
    }

    pub fn current_skill_array(&self) -> [Skill; 20] {
        (0..20)
            .map(|idx| self.skill_at_index(idx))
            .collect::<Vec<Skill>>()
            .try_into()
            .expect("There should be 20 skills")
    }

    // If the player is currently playing a game, returns the in-game copy of the player,
    // which carries the live tiredness, morale and drunkenness.
    fn in_game_copy<'a>(&self, world: &'a World) -> Option<&'a Player> {
        let team_id = self.team?;
        let team = world.teams.get(&team_id)?;
        let game = world.games.get(&team.current_game?)?;
        if game.home_team_in_game.team_id == team_id {
            game.home_team_in_game.players.get(&self.id)
        } else {
            game.away_team_in_game.players.get(&self.id)
        }
    }

    pub fn current_tiredness(&self, world: &World) -> f32 {
        self.in_game_copy(world)
            .map_or(self.tiredness, |p| p.tiredness)
    }

    pub fn current_morale(&self, world: &World) -> f32 {
        self.in_game_copy(world).map_or(self.morale, |p| p.morale)
    }

    pub fn current_drunkenness(&self, world: &World) -> f32 {
        self.in_game_copy(world)
            .map_or(self.drunkenness, |p| p.drunkenness)
    }

    pub fn can_drink(&self, teams: &TeamMap) -> AppResult<()> {
        if self.team.is_none() {
            return Err(anyhow!("Player has no team, so no rum to drink"));
        }

        let team = teams.get_or_err(&self.team.expect("Player should have team"))?;

        if team.current_game.is_some() {
            return Err(anyhow!("Can't drink during game"));
        }

        if self.is_knocked_out() {
            return Err(anyhow!("Too wasted to drink"));
        }

        if team.resources.value(&Resource::RUM).is_zero() {
            return Err(anyhow!("No rum to drink"));
        }

        Ok(())
    }

    pub fn can_receive_gold(&self, teams: &TeamMap) -> AppResult<()> {
        if self.team.is_none() {
            return Err(anyhow!("Player has no team, so no gold to get"));
        }

        let team = teams.get_or_err(&self.team.expect("Player should have team"))?;

        if team.current_game.is_some() {
            return Err(anyhow!("Can't get gold during game"));
        }

        if self.is_knocked_out() {
            return Err(anyhow!("Too wasted to get gold"));
        }

        if team.resources.value(&Resource::GOLD).is_zero() {
            return Err(anyhow!("No gold to get"));
        }

        Ok(())
    }

    pub fn satisfy_opinion(&mut self, opinion: PlayerOpinion) {
        let Some((last_event, _)) = self.opinions.get_mut(&opinion) else {
            return;
        };
        let now = Tick::now();
        let recency = now
            .saturating_sub(*last_event)
            .min(SATISFACTION_OPINION_RECOVERY_TIME) as f32
            / SATISFACTION_OPINION_RECOVERY_TIME as f32;
        *last_event = now;

        let factor = 1.0 + SATISFACTION_OPINION_MODIFIER_WEIGHT * self.opinions.modifier(opinion);
        let bonus = if factor > 0.0 {
            factor * recency * opinion.satisfaction_modifier()
        } else {
            factor
        };
        self.add_team_satisfaction(SATISFACTION_PER_OPINION_EVENT * bonus);
    }

    /// Drink one liter of rum, increasing drunkenness and boosting morale.
    /// There is a chance, growing with drunkenness, that the player gets fully drunk:
    /// in that case tiredness goes to max (the player is wasted), drunkenness resets
    /// and morale is unaffected (happy to get drunk ;) ). Returns whether the player got fully drunk.
    pub fn drink(&mut self, rng: &mut ChaCha8Rng) -> bool {
        let amount = if self.special_trait == Some(Trait::Spugna) {
            // Spugna gets drunk 10 times as slow.
            SPUGNA_DRUNKENNESS_PER_DRINK
        } else {
            DRUNKENNESS_PER_DRINK
        };
        self.drunkenness = (self.drunkenness + amount).bound();

        self.satisfy_opinion(PlayerOpinion::Drinking);

        // Probability equal to the new drunkenness over MAX_SKILL,
        // mitigated by stamina: high-stamina players hold their liquor.
        let drunk_probability = BASE_DRUNK_PROBABILITY
            * ((self.drunkenness / MAX_SKILL) / (1.0 + self.athletics.stamina / MAX_SKILL)) as f64;
        if rng.random_bool(drunk_probability) {
            // Wasted: tiredness is set directly rather than through add_tiredness,
            // since morale must be unaffected and trait caps must not prevent it.
            self.tiredness = MAX_SKILL;
            self.drunkenness = if self.special_trait == Some(Trait::Spugna) {
                // Spugna gets less drunk.
                SPUGNA_DRUNKENNESS_ON_GETTING_DRUNK
            } else {
                DRUNKENNESS_ON_GETTING_DRUNK
            };
            return true;
        }

        self.add_morale(MORALE_DRINK_BONUS);
        false
    }

    pub fn get_gold(&mut self) {
        self.satisfy_opinion(PlayerOpinion::Gold);
        self.add_morale(MORALE_GOLD_BONUS);
    }

    pub fn team_satisfaction(&self) -> Option<Skill> {
        self.opinions
            .get(&PlayerOpinion::OwnTeam)
            .map(|(_, value)| *value)
    }

    pub fn salary(&self) -> u32 {
        // Crumiros work for free
        if matches!(self.special_trait, Some(Trait::Crumiro)) {
            return 0;
        }
        const SALARY_MOD: f32 = 0.0175;
        let opinion_modifier = 1.0 + self.opinions.modifier(PlayerOpinion::Gold);
        let avg_skill_modifier = (1.0 + self.average_skill() / MAX_SKILL).powf(1.35);
        let reputation_modifier = (1.0 + self.reputation / MAX_SKILL).powf(1.25);
        (SALARY_MOD
            * self.hire_cost() as f32
            * opinion_modifier
            * avg_skill_modifier
            * reputation_modifier) as u32
    }

    pub fn hire_cost(&self) -> u32 {
        const COST_PER_VALUE: f32 = 815.0;

        const HIRE_AGE_MODIFIER_AT_PEAK: f32 = 1.0;
        const HIRE_AGE_MODIFIER_AT_RETIREMENT: f32 = 0.425;
        const SPECIAL_TRAIT_VALUE_BONUS: f32 = 1.4;

        let bare_value = {
            let hire_age_modifier_at_birth: f32 =
                (1.25_f32).powf(1.0 + 2.1 * self.potential / MAX_SKILL);
            // Age modifier: linear between birth, peak, and retirement values.
            let relative_age = self.info.relative_age();
            let age_modifier = if relative_age <= PEAK_PERFORMANCE_RELATIVE_AGE {
                hire_age_modifier_at_birth
                    - (hire_age_modifier_at_birth - HIRE_AGE_MODIFIER_AT_PEAK)
                        * (relative_age / PEAK_PERFORMANCE_RELATIVE_AGE)
            } else {
                let progress = (relative_age - PEAK_PERFORMANCE_RELATIVE_AGE)
                    / (1.0 - PEAK_PERFORMANCE_RELATIVE_AGE);
                HIRE_AGE_MODIFIER_AT_PEAK
                    - (HIRE_AGE_MODIFIER_AT_PEAK - HIRE_AGE_MODIFIER_AT_RETIREMENT) * progress
            };

            let special_trait_extra = if self.special_trait.is_some() {
                SPECIAL_TRAIT_VALUE_BONUS * (1.0 + self.reputation / MAX_SKILL).powf(0.5)
            } else {
                1.0
            };

            let reputation_modifier = 1.0 + self.reputation / MAX_SKILL;
            let avg_skill = self.average_skill();
            let avg_skill_modifier = (1.0 + avg_skill / MAX_SKILL).powf(2.6);

            let mut sorted_fitness = self.game_position_fitness;
            sorted_fitness.sort_unstable_by(|a, b| b.total_cmp(a));

            let position_modifier = 0.75 / MAX_SKILL
                * (sorted_fitness[0]
                    + 0.25 * sorted_fitness[1]
                    + 0.05 * sorted_fitness[2]
                    + 0.01 * sorted_fitness[3]
                    + 0.0025 * sorted_fitness[4]);

            avg_skill
                * avg_skill_modifier
                * position_modifier
                * age_modifier
                * special_trait_extra
                * reputation_modifier
        };
        (COST_PER_VALUE * bare_value).max(1.0) as u32
    }

    fn apply_population_skill_modifiers(&mut self) {
        match self.info.population {
            Population::Human { region } => match region {
                Region::Italy => self.info.height = (self.info.height * 1.02).min(225.0),
                Region::Germany => self.info.height = (self.info.height * 1.05).min(225.0),
                Region::Spain => self.info.height = (self.info.height * 1.00).min(225.0),
                Region::Greece => self.info.height = (self.info.height * 0.96).min(225.0),
                Region::Nigeria => self.info.height = (self.info.height * 1.05).min(225.0),
                Region::India => self.info.height = (self.info.height * 0.95).min(225.0),
                Region::Euskadi => self.info.height = (self.info.height * 0.98).min(225.0),
                Region::Kurdistan => self.info.height = (self.info.height * 0.96).min(225.0),
                Region::Palestine => self.info.height = (self.info.height * 0.96).min(225.0),
                Region::Japan => self.info.height = (self.info.height * 0.94).min(225.0),
            },
            Population::Yardalaim => {
                self.info.weight = (self.info.weight * 1.5).min(255.0);
                self.offense.brawl = (self.offense.brawl * 1.2).bound();
                self.athletics.strength = (self.athletics.strength * 1.35).bound();
            }
            Population::Polpett => {
                self.info.height *= 0.95;
                self.mental.aggression = (self.mental.aggression * 1.35).bound();
                self.defense.steal = (self.defense.steal * 1.2).bound();
            }
            Population::Juppa => {
                self.info.height = (self.info.height * 1.09).min(225.0);
                self.offense.long_range = (self.offense.long_range * 1.23).bound();
            }
            Population::Galdari => {
                self.info.height = (self.info.height * 1.02).min(225.0);
                self.mental.charisma = (self.mental.charisma * 1.15).bound();
                self.mental.vision = (self.mental.vision * 1.35).bound();
                self.defense.steal = (self.defense.steal * 1.2).bound();
            }
            Population::Pupparoll => {
                self.athletics.quickness = (self.athletics.quickness * 0.95).bound();
                self.athletics.vertical = (self.athletics.vertical * 1.25).bound();
                self.technical.rebounds = (self.technical.rebounds * 1.25).bound();
                self.mental.aggression = (self.mental.aggression * 0.85).bound();
                self.offense.brawl = (self.offense.brawl * 1.15).bound();
            }
            Population::Octopulp => {
                self.athletics.quickness = (self.athletics.quickness * 0.95).bound();
                self.mental.vision = (self.mental.vision * 0.75).bound();
                self.defense.steal = (self.defense.steal * 1.2).bound();
                self.offense.brawl = (self.offense.brawl * 1.1).bound();
                self.offense.close_range = (self.offense.close_range * 1.35).bound();
                self.info.weight = (self.info.weight * 1.3).min(255.0);
            }
        }
    }

    fn apply_info_skill_modifiers(&mut self) {
        self.athletics.quickness = skill_linear_interpolation(
            self.athletics.quickness,
            self.info.weight,
            [90.0, 1.0, 130.0, 0.5],
        );
        self.athletics.vertical = skill_linear_interpolation(
            self.athletics.vertical,
            self.info.weight,
            [90.0, 1.0, 130.0, 0.5],
        );
        self.athletics.strength = skill_linear_interpolation(
            self.athletics.strength,
            self.info.weight,
            [75.0, 0.75, 135.0, 1.25],
        );
        self.athletics.stamina = skill_linear_interpolation(
            self.athletics.stamina,
            self.info.weight,
            [90.0, 1.0, 130.0, 0.8],
        );
        self.technical.rebounds = skill_linear_interpolation(
            self.technical.rebounds,
            self.info.height,
            [190.0, 0.75, 215.0, 1.25],
        );
        self.defense.block = skill_linear_interpolation(
            self.defense.block,
            self.info.height,
            [190.0, 0.75, 215.0, 1.25],
        );

        self.mental.vision = skill_linear_interpolation(
            self.mental.vision,
            self.info.relative_age(),
            [0.0, 0.75, 1.0, 1.25],
        );
        self.mental.charisma = skill_linear_interpolation(
            self.mental.charisma,
            self.info.relative_age(),
            [0.0, 0.75, 1.0, 1.25],
        );

        self.athletics.stamina = skill_linear_interpolation(
            self.athletics.stamina,
            self.info.relative_age(),
            [0.0, 1.1, 1.0, 0.75],
        );
    }

    pub fn skill_at_index(&self, idx: usize) -> Skill {
        match idx {
            0 => self.athletics.quickness,
            1 => self.athletics.vertical,
            2 => self.athletics.strength,
            3 => self.athletics.stamina,
            4 => self.offense.brawl,
            5 => self.offense.close_range,
            6 => self.offense.medium_range,
            7 => self.offense.long_range,
            8 => self.defense.steal,
            9 => self.defense.block,
            10 => self.defense.perimeter_defense,
            11 => self.defense.interior_defense,
            12 => self.technical.passing,
            13 => self.technical.ball_handling,
            14 => self.technical.post_moves,
            15 => self.technical.rebounds,
            16 => self.mental.vision,
            17 => self.mental.aggression,
            18 => self.mental.intuition,
            19 => self.mental.charisma,
            _ => panic!("Invalid skill index"),
        }
    }

    pub fn set_jersey(&mut self, jersey: &Jersey) {
        self.image.set_jersey(jersey, &self.info);
        self.version += 1;
    }

    pub fn compose_image(&self) -> AppResult<Gif> {
        self.image.compose(&self.info)
    }

    pub fn is_on_planet(&self) -> Option<PlanetId> {
        match self.current_location {
            PlayerLocation::OnPlanet { planet_id } => Some(planet_id),
            _ => None,
        }
    }

    pub fn average_skill(&self) -> Skill {
        (0..20).map(|idx| self.skill_at_index(idx)).sum::<Skill>() / 20.0
    }

    pub fn has_hat(&self) -> bool {
        self.image.hat.is_some()
    }

    pub fn has_wooden_leg(&self) -> bool {
        self.image.wooden_leg.is_some()
    }

    pub fn has_eye_patch(&self) -> bool {
        self.image.eye_patch.is_some()
    }

    pub fn has_hook(&self) -> bool {
        self.image.hook.is_some()
    }

    pub fn is_knocked_out(&self) -> bool {
        self.tiredness == MAX_SKILL
    }

    pub fn add_tiredness(&mut self, tiredness: f32) {
        let max_tiredness = if self.special_trait == Some(Trait::Relentless) {
            0.8 * MAX_SKILL
        } else if self.special_trait == Some(Trait::Crumiro) {
            0.85 * MAX_SKILL
        } else {
            MAX_SKILL
        };

        self.tiredness = (self.tiredness + tiredness / (1.0 + self.athletics.stamina / MAX_SKILL))
            .min(max_tiredness)
            .bound();

        if self.is_knocked_out() {
            self.morale = MIN_SKILL;
            // A wasted player sobers up completely.
            self.drunkenness = MIN_SKILL;
        }
    }

    pub fn add_team_satisfaction(&mut self, satisfaction: f32) {
        if self.team.is_none() {
            return;
        }
        let min_satisfaction = if self.special_trait == Some(Trait::Crumiro) {
            SATISFACTION_THRESHOLD_FOR_LEAVING
        } else {
            MIN_SKILL
        };
        let entry = self
            .opinions
            .entry(PlayerOpinion::OwnTeam)
            .or_insert((Tick::now(), OPINION_NEUTRAL_VALUE));
        entry.1 = (entry.1 + satisfaction).max(min_satisfaction).bound();
    }

    pub fn reset_team_satisfaction_on_hire(&mut self) {
        let Some(team_id) = self.team else {
            return;
        };
        let value = match self.opinions.remove(&PlayerOpinion::Team { team_id }) {
            Some((_, remembered)) => (remembered + PLAYER_OPINION_ON_HIRE).bound(),
            None => OPINION_NEUTRAL_VALUE + PLAYER_OPINION_ON_HIRE,
        };
        self.opinions
            .insert(PlayerOpinion::OwnTeam, (Tick::now(), value));
    }

    pub fn add_morale(&mut self, morale: f32) {
        let min_morale = if self.special_trait == Some(Trait::Crumiro) {
            0.15 * MAX_SKILL
        } else {
            MIN_SKILL
        };

        let satisfaction = self.team_satisfaction().unwrap_or(MAX_SKILL);

        let mod_morale = if morale >= 0.0 {
            morale
        } else {
            // If morale is a malus, the player satisfaction reduces the malus (up to 2/3).
            morale / (1.0 + 0.5 * satisfaction / MAX_SKILL)
        };

        let morale_was_not_minimum = self.morale > MIN_SKILL;

        self.morale = (self.morale + mod_morale).max(min_morale).bound();
        if self.morale == MIN_SKILL && morale_was_not_minimum {
            self.add_team_satisfaction(SATISFACTION_MALUS_FOR_MORALE_DROP);
        }
    }

    pub fn age_modifier_to_skill_update(&self, idx: usize) -> f32 {
        // Age modifier:
        //   Young: linear from 0.75 at birth to 1.0 at peak.
        //   Old:   linear from 1.0 at peak to max_modifier at retirement.
        //          Athletics (idx 0-3):  max 3.15
        //          Mental (16-19):       max 1.55
        //          Off/Def/Tech (4-15):  max 2.55
        let relative_age = self.info.relative_age();
        if relative_age <= PEAK_PERFORMANCE_RELATIVE_AGE {
            0.75 + 0.25 * (relative_age / PEAK_PERFORMANCE_RELATIVE_AGE)
        } else {
            let progress = (relative_age - PEAK_PERFORMANCE_RELATIVE_AGE)
                / (1.0 - PEAK_PERFORMANCE_RELATIVE_AGE);
            let max_modifier = if idx < 4 {
                3.15
            } else if idx > 15 {
                1.55
            } else {
                2.55
            };
            1.0 + progress * (max_modifier - 1.0)
        }
    }

    pub fn modify_skill(&mut self, idx: usize, mut value: f32) {
        if value.is_positive() {
            // Quickness cannot improve beyond WOODEN_LEG_MAX_QUICKNESS if player has a wooden leg
            if self.has_wooden_leg()
                && idx == 0
                && self.athletics.quickness >= WOODEN_LEG_MAX_QUICKNESS
            {
                return;
            }
            // Vision cannot improve beyond EYE_PATCH_MAX_VISION if player has an eye patch
            if self.has_eye_patch() && idx == 16 && self.mental.vision >= EYE_PATCH_MAX_VISION {
                return;
            }

            // Ball handling cannot improve beyond HOOK_MAX_BALL_HANDLING if player has a hook
            if self.has_hook()
                && idx == 13
                && self.technical.ball_handling >= HOOK_MAX_BALL_HANDLING
            {
                return;
            }

            // Charisma improves quicker if player has an eye patch
            if self.has_eye_patch() && idx == 19 {
                value *= 1.5;
            }

            // Strength improves quicker if player has a hook
            if self.has_hook() && idx == 2 {
                value *= 1.5;
            }
        }

        let new_value = (self.skill_at_index(idx) + value).bound();
        match idx {
            0 => self.athletics.quickness = new_value,
            1 => self.athletics.vertical = new_value,
            2 => self.athletics.strength = new_value,
            3 => self.athletics.stamina = new_value,
            4 => self.offense.brawl = new_value,
            5 => self.offense.close_range = new_value,
            6 => self.offense.medium_range = new_value,
            7 => self.offense.long_range = new_value,
            8 => self.defense.steal = new_value,
            9 => self.defense.block = new_value,
            10 => self.defense.perimeter_defense = new_value,
            11 => self.defense.interior_defense = new_value,
            12 => self.technical.passing = new_value,
            13 => self.technical.ball_handling = new_value,
            14 => self.technical.post_moves = new_value,
            15 => self.technical.rebounds = new_value,
            16 => self.mental.vision = new_value,
            17 => self.mental.aggression = new_value,
            18 => self.mental.intuition = new_value,
            19 => self.mental.charisma = new_value,
            _ => unreachable!("Invalid skill index {idx}"),
        }
    }

    pub fn update_skills_training(
        &mut self,
        experience_at_position: [u32; NUM_GAME_POSITIONS as usize],
        training_bonus: f32,
        training_focus: Option<TrainingFocus>,
    ) {
        // potential_modifier:
        // Below the cap, growth is boosted by a sublinear curve of the gap
        // (potential - tot_skill)/MAX_SKILL. powf(1.75) keeps the catch-up
        // boost near zero in the mid-range and only ramps up for players
        // genuinely far below their potential, avoiding the rubber-band
        // effect that a linear slope produced.
        // Above the cap, growth decays sharply via powf(30).
        let tot_skill = AVG_SKILL_TO_POTENTIAL_MODIFIER * self.average_skill();
        let potential_modifier = if tot_skill > self.potential {
            (1.0 + (self.potential - tot_skill) / MAX_SKILL)
                .max(0.0)
                .powf(30.0)
        } else {
            1.0 + ((self.potential - tot_skill) / MAX_SKILL)
                .max(0.0)
                .powf(1.5)
        };

        for p in 0..NUM_GAME_POSITIONS as usize {
            if experience_at_position[p] == 0 {
                continue;
            }

            let base_increase = experience_at_position[p] as f32
                * EXPERIENCE_PER_SKILL_MULTIPLIER
                * training_bonus
                * potential_modifier;

            let position_increase = if matches!(training_focus, Some(TrainingFocus::GamePosition(pos)) if pos == p as u8)
            {
                1.25 * base_increase
            } else {
                0.5 * base_increase
            };

            self.game_position_fitness_training[p] += position_increase;
            // Cap to maximum skill increase per day.
            self.game_position_fitness_training[p] =
                self.game_position_fitness_training[p].min(MAX_SKILL_INCREASE_PER_LONG_TICK);

            for (idx, &w) in (p as GamePosition).weights().iter().enumerate() {
                let training_focus_bonus = match training_focus {
                    Some(focus) => focus.bonus_for_skill(idx),
                    None => 1.0,
                };

                let skill = self.skill_at_index(idx);
                let cap_factor = 1.25 * (1.0 - skill / MAX_SKILL).powf(0.5).max(0.75);

                self.skills_training[idx] += w * training_focus_bonus * cap_factor * base_increase;

                log::debug!(
                    "Experience increase: {:.3}={}x{}x{}x{}x{}x{:.2}",
                    w * training_focus_bonus * base_increase,
                    experience_at_position[p] as f32,
                    w,
                    EXPERIENCE_PER_SKILL_MULTIPLIER,
                    training_bonus,
                    training_focus_bonus,
                    potential_modifier
                );

                // Cap to maximum skill increase per day.
                self.skills_training[idx] =
                    self.skills_training[idx].min(MAX_SKILL_INCREASE_PER_LONG_TICK);
            }
        }

        log::debug!("Total Experience increase: {:#?}", self.skills_training);
    }
}

impl Rated for Player {
    fn rating(&self) -> Skill {
        self.average_skill()
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InfoStats {
    pub first_name: String,
    pub last_name: String,
    pub crew_role: CrewRole,
    pub population: Population,
    pub age: f32,
    pub pronouns: Pronoun,
    pub height: f32,
    pub weight: f32,
}

impl Default for InfoStats {
    fn default() -> Self {
        let population = Population::default();
        Self {
            first_name: "Defaulto".to_string(),
            last_name: "Faultonio".to_string(),
            crew_role: CrewRole::default(),
            population,
            age: population.min_age(),
            pronouns: Pronoun::default(),
            height: 183.2,
            weight: 73.0,
        }
    }
}

impl InfoStats {
    pub fn short_name(&self) -> String {
        format!(
            "{}.{}",
            self.first_name.chars().next().unwrap_or_default(),
            self.last_name
        )
    }

    pub fn full_name(&self) -> String {
        format!("{} {}", self.first_name, self.last_name)
    }

    pub fn relative_age(&self) -> f32 {
        self.population.relative_age(self.age)
    }
    pub fn randomize_for_position(&mut self, position: GamePosition, rng: &mut ChaCha8Rng) {
        let p_data = PLAYER_DATA
            .get(&self.population)
            .unwrap_or_else(|| panic!("Player data should exist for {}", self.population));
        self.pronouns =
            if self.population == Population::Polpett || self.population == Population::Octopulp {
                Pronoun::They
            } else {
                Pronoun::random(rng)
            };
        self.first_name = match self.pronouns {
            Pronoun::He => p_data
                .first_names_he
                .choose(rng)
                .expect("No available name")
                .to_string(),
            Pronoun::She => p_data
                .first_names_she
                .choose(rng)
                .expect("No available name")
                .to_string(),
            Pronoun::They => match rng.random_bool(0.5) {
                true => p_data
                    .first_names_he
                    .choose(rng)
                    .expect("No available name")
                    .to_string(),
                false => p_data
                    .first_names_she
                    .choose(rng)
                    .expect("No available name")
                    .to_string(),
            },
        };
        self.last_name = p_data
            .last_names
            .choose(rng)
            .expect("No available name")
            .to_string();
        self.age = self.population.min_age()
            + rng.random_range(0.0..0.55) * (self.population.max_age() - self.population.min_age());
        self.height = Normal::new(192.0 + 3.5 * position as f32, 5.0)
            .unwrap()
            .sample(rng);
        let bmi = rng.random_range(12..22) as f32 + self.height / 20.0;
        self.weight = bmi * self.height * self.height / 10000.0;
    }
}

#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq, Display)]
#[repr(u8)]
pub enum Trait {
    Killer,
    Relentless,
    Showpirate,
    Spugna,
    Crumiro,
}

impl Trait {
    pub fn description(&self, player: &Player) -> String {
        match self {
            Trait::Killer => format!(
                "Better at brawling during games based on reputation (+{}), +25% team bonus to Weapons.",
                player.reputation.value()
            ),
            Trait::Relentless => "Cannot get exhausted, +25% team bonus to Recovery.".to_string(),
            Trait::Showpirate => {
                format!(
                    "Increase games attendance based on reputation (+{}%), more likely to dunk!",
                    player.reputation.value()
                )
            }
            Trait::Spugna => "Gets drunk 10 times as slow when drinking. It is said that a drunk pilot could bring you somewhere unexpected...".to_string(),
            Trait::Crumiro => "Legendary trait of the emperor's crew members".to_string(),
        }
    }
}

#[cfg(test)]
mod test {
    use super::{Player, Trait};
    use crate::{
        app::App,
        core::{
            constants::{
                DRUNKENNESS_ON_GETTING_DRUNK, DRUNKENNESS_PER_DRINK, SPUGNA_DRUNKENNESS_PER_DRINK,
            },
            skill::{Rated, MAX_SKILL, MIN_SKILL},
        },
        types::{AppResult, HashMapWithResult, TeamId},
    };
    use itertools::Itertools;
    use rand::SeedableRng;
    use rand_chacha::ChaCha8Rng;

    #[test]
    fn test_drink() -> AppResult<()> {
        let rng = &mut ChaCha8Rng::seed_from_u64(0);

        // Drinking increases drunkenness and boosts morale, but does not drain energy.
        // Morale is capped by satisfaction, so the player must be satisfied to gain any.
        let mut player = Player::default();
        player.athletics.stamina = MAX_SKILL;
        player.team = Some(TeamId::new_v4());
        player.reset_team_satisfaction_on_hire();
        let got_drunk = player.drink(rng);
        assert!(!got_drunk);
        assert!(player.drunkenness == DRUNKENNESS_PER_DRINK);
        assert!(player.morale > MIN_SKILL);
        assert!(player.tiredness == MIN_SKILL);

        // Spugna gets drunk 10 times as slow.
        let mut spugna = Player::default();
        spugna.athletics.stamina = MAX_SKILL;
        spugna.special_trait = Some(Trait::Spugna);
        let got_drunk = spugna.drink(rng);
        assert!(!got_drunk);
        assert!(spugna.drunkenness == SPUGNA_DRUNKENNESS_PER_DRINK);

        // At max drunkenness and zero stamina the drunk event is guaranteed:
        // the player gets wasted, drunkenness goes negative and morale is unaffected.
        let mut player = Player::default();
        player.drunkenness = MAX_SKILL;
        player.athletics.stamina = 0.0;
        player.morale = 10.0;
        let got_drunk = player.drink(rng);
        assert!(got_drunk);
        assert!(player.is_knocked_out());
        assert!(player.drunkenness == DRUNKENNESS_ON_GETTING_DRUNK);
        assert!(player.morale == 10.0);

        // Getting knocked out by tiredness also resets drunkenness.
        let mut player = Player::default();
        player.drunkenness = 10.0;
        player.add_tiredness(10.0 * MAX_SKILL);
        assert!(player.is_knocked_out());
        assert!(player.drunkenness == MIN_SKILL);
        assert!(player.morale == MIN_SKILL);

        Ok(())
    }

    #[test]
    fn test_bare_value() -> AppResult<()> {
        use crate::core::constants::{AGE_INCREASE_PER_LONG_TICK, SKILL_DECREMENT_PER_LONG_TICK};

        let mut app = App::test_default()?;

        let world = &mut app.world;

        let player_id = world
            .players
            .values()
            .next()
            .expect("There should be at least one player")
            .id;

        let mut player = world.players.get_or_err(&player_id)?.clone();
        player.info.age = player.info.population.min_age();
        player.skills_training = [0.0; 20];

        println!(
            "Population {:?} - Potential {:.2} - Trait {}",
            player.info.population,
            player.potential,
            player
                .special_trait
                .map(|t| t.to_string())
                .unwrap_or("None".to_string())
        );

        let mut next_log = 0.0;
        while player.info.relative_age() < 1.0 {
            if player.info.relative_age() >= next_log {
                println!(
                    "Relative age {:5.2} - Overall {:5.2} {} - Hire cost {}",
                    player.info.relative_age(),
                    player.average_skill(),
                    player.average_skill().stars(),
                    player.hire_cost()
                );
                next_log += 0.05;
            }

            // One game per day, 32 minutes played, distributed by fitness rank
            // as in test_player_evolution.
            let mut ranked: Vec<usize> = (0..5).collect();
            ranked.sort_by(|&a, &b| {
                player.game_position_fitness[b]
                    .partial_cmp(&player.game_position_fitness[a])
                    .unwrap()
            });
            let mut experience_at_position = [0u32; 5];
            experience_at_position[ranked[0]] = 1152;
            experience_at_position[ranked[1]] = 256;
            experience_at_position[ranked[2]] = 256;
            experience_at_position[ranked[3]] = 256;
            player.update_skills_training(experience_at_position, 1.5, None);

            for idx in 0..player.skills_training.len() {
                let age_modifier = player.age_modifier_to_skill_update(idx);
                player.modify_skill(idx, SKILL_DECREMENT_PER_LONG_TICK * age_modifier);
                let training = player.skills_training[idx];
                player.modify_skill(idx, training);
            }
            player.skills_training = [0.0; 20];
            player.info.age += AGE_INCREASE_PER_LONG_TICK;
        }

        println!(
            "Relative age {:5.2} - Overall {:5.2} {} - Hire cost {}",
            player.info.relative_age(),
            player.average_skill(),
            player.average_skill().stars(),
            player.hire_cost()
        );

        Ok(())
    }

    #[test]
    fn test_player_evolution() -> AppResult<()> {
        use crate::core::constants::{AGE_INCREASE_PER_LONG_TICK, SKILL_DECREMENT_PER_LONG_TICK};
        use crate::core::types::TrainingFocus;

        fn round_skills(skills: [f32; 20]) -> Vec<f32> {
            skills.iter().map(|v| (v * 100.0).round() / 100.0).collect()
        }

        let mut app = App::test_default()?;
        let world = &mut app.world;

        let sorted_ids = world
            .players
            .values()
            .sorted_by(|a, b| b.potential.partial_cmp(&a.potential).unwrap())
            .map(|p| p.id)
            .collect_vec();
        let top_id = sorted_ids[0];
        let median_id = sorted_ids[sorted_ids.len() / 2];
        let bottom_id = *sorted_ids.last().expect("at least one player");

        for (label, pid) in [
            ("TOP", top_id),
            ("MEDIAN", median_id),
            ("BOTTOM", bottom_id),
        ] {
            for focus in [
                None,
                Some(TrainingFocus::Offense),
                Some(TrainingFocus::GamePosition(0)),
            ] {
                let mut player = world.players.get_or_err(&pid)?.clone();
                player.info.age = player.info.population.min_age();
                player.skills_training = [0.0; 20];

                println!(
                    "\n=== {label} potential, focus={focus:?} | population={:?} ===",
                    player.info.population
                );
                let initial_best_pos = player
                    .game_position_fitness
                    .iter()
                    .enumerate()
                    .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
                    .map(|(idx, _)| idx)
                    .unwrap_or(0);
                println!(
                    "Initial:  rel_age={:.3} overall={:.2} potential={:.2} best_pos={}",
                    player.info.relative_age(),
                    player.average_skill(),
                    player.potential,
                    initial_best_pos
                );
                println!(
                    "Initial skills: {:?}",
                    round_skills(player.current_skill_array())
                );

                let mut logged_075 = false;
                let mut iterations = 0usize;

                loop {
                    let relative_age = player.info.relative_age();
                    if relative_age >= 1.0 || iterations > 5000 {
                        break;
                    }

                    // One game per day: 32 minutes = 32*60 = 1920 seconds played.
                    // Distribute by fitness rank: 60% to best position, 13.3% to each of
                    // the next three best, 0 to the worst. Better than uniform [384;5] and
                    // more realistic than all-on-best.
                    let mut ranked: Vec<usize> = (0..5).collect();
                    ranked.sort_by(|&a, &b| {
                        player.game_position_fitness[b]
                            .partial_cmp(&player.game_position_fitness[a])
                            .unwrap()
                    });
                    let mut experience_at_position = [0u32; 5];
                    experience_at_position[ranked[0]] = 1152; // 60%
                    experience_at_position[ranked[1]] = 256; //  13.3%
                    experience_at_position[ranked[2]] = 256;
                    experience_at_position[ranked[3]] = 256;
                    // ranked[4] gets 0.
                    player.update_skills_training(experience_at_position, 1.5, focus);

                    // Inline the relevant body of World::tick_players_update so we
                    // don't need to expose it.
                    for idx in 0..player.skills_training.len() {
                        let age_modifier = player.age_modifier_to_skill_update(idx);
                        player.modify_skill(idx, SKILL_DECREMENT_PER_LONG_TICK * age_modifier);
                        let training = player.skills_training[idx];
                        player.modify_skill(idx, training);
                    }
                    player.skills_training = [0.0; 20];
                    player.info.age += AGE_INCREASE_PER_LONG_TICK;
                    iterations += 1;

                    if !logged_075 && player.info.relative_age() >= 0.75 {
                        println!(
                            "@0.75:    rel_age={:.3} overall={:.2} {} (games={iterations})",
                            player.info.relative_age(),
                            player.average_skill(),
                            player.average_skill().stars()
                        );
                        println!(
                            "@0.75 skills: {:?}",
                            round_skills(player.current_skill_array())
                        );
                        logged_075 = true;
                    }
                }

                println!(
                    "@1.00:    rel_age={:.3} overall={:.2} {} (games={iterations})",
                    player.info.relative_age(),
                    player.average_skill(),
                    player.average_skill().stars()
                );
                println!(
                    "@1.00 skills: {:?}",
                    round_skills(player.current_skill_array())
                );
            }
        }

        Ok(())
    }
}
