use std::{
    collections::{HashMap, HashSet},
    fmt::Display,
};

use super::prelude::*;

#[derive(Debug)]
pub struct KillData {
    pub killed: CreatureId,
    pub killer: Option<CreatureId>,
}

#[derive(Clone, Debug)]
pub struct Attack {
    pub to_hit_modifier: i8,
    pub threat_treshold: i8,
    pub armor_class: i8,
}

#[derive(Debug)]
pub enum AttackResult {
    CritMiss,
    Miss,
    Hit,
    Crit,
}

#[derive(Clone, Debug)]
pub struct AttackRoll {
    pub attack: Attack,
    pub roll: i8,
    pub threat_roll: Option<i8>,
}

impl AttackRoll {
    pub fn roll(attack: Attack, roll_fn: fn() -> i8) -> Self {
        let roll = roll_fn();
        let mut threat_roll = None;
        if roll >= attack.threat_treshold {
            threat_roll = Some(roll_fn());
        }
        Self {
            attack,
            roll,
            threat_roll,
        }
    }

    pub fn is_crit_miss(&self) -> bool {
        self.roll == 1
    }

    pub fn is_auto_hit(&self) -> bool {
        self.roll == 20
    }

    pub fn is_hit(&self) -> bool {
        if self.is_crit_miss() {
            return false;
        }
        if self.is_auto_hit() {
            return true;
        }
        (self.roll + self.attack.to_hit_modifier) >= self.attack.armor_class
    }

    pub fn is_crit(&self) -> bool {
        match self.threat_roll {
            Some(roll) => (roll + self.attack.to_hit_modifier) >= self.attack.armor_class,
            None => false,
        }
    }

    pub fn result(&self) -> AttackResult {
        if self.is_crit_miss() {
            return AttackResult::CritMiss;
        }
        match self.is_hit() {
            true => {
                if self.is_crit() {
                    AttackResult::Crit
                } else {
                    AttackResult::Hit
                }
            }
            false => AttackResult::Miss,
        }
    }
}

#[derive(Debug)]
pub struct AttackData {
    pub target: CreatureId,
    pub attacker: Option<CreatureId>,
    pub attack_roll: AttackRoll,
    pub damage: Option<i32>,
    pub is_deathblow: bool,
}

impl Display for AttackData {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if let Some(damage) = self.damage {
            write!(f, "{damage} damage ")?;
        }
        match self.attack_roll.result() {
            AttackResult::CritMiss => write!(f, "*Critical Miss* ")?,
            AttackResult::Miss => write!(f, "*Miss* ")?,
            AttackResult::Crit => write!(f, "!Critical hit! ")?,
            _ => (),
        };
        let mod_symbol = if self.attack_roll.attack.to_hit_modifier >= 0 {
            '+'
        } else {
            '-'
        };
        write!(
            f,
            ": {total} ({calc_roll} {calc_symbol} {calc_mod}) vs. {ac}",
            total = self.attack_roll.roll + self.attack_roll.attack.to_hit_modifier,
            calc_roll = self.attack_roll.roll,
            calc_symbol = mod_symbol,
            calc_mod = self.attack_roll.attack.to_hit_modifier.abs(),
            ac = self.attack_roll.attack.armor_class,
        )?;
        write!(
            f,
            "{}",
            if self.is_deathblow {
                " - Killing blow!"
            } else {
                ""
            }
        )
    }
}

#[derive(Debug)]
pub struct CombatResults {
    pub kills: Vec<KillData>,
    pub attacks: Vec<AttackData>,
}

#[derive(Debug)]
struct CombatState {
    creature: Creature,
    damage: i32,
    enemies: HashSet<CreatureId>,
}

impl CombatState {
    pub fn is_alive(&self) -> bool {
        self.damage < self.creature.health()
    }
}

impl CombatState {
    pub fn from_creature(creature: Creature) -> Self {
        Self {
            creature,
            damage: 0,
            enemies: HashSet::new(),
        }
    }
}

pub fn resolve_combat(combatants: &[Creature], max_rounds: u16) -> Option<CombatResults> {
    let mut participants: HashMap<CreatureId, CombatState> = HashMap::new();
    for index_1 in 0..combatants.len() {
        for index_2 in (index_1 + 1)..combatants.len() {
            if index_1 == index_2 {
                continue;
            }
            let c1 = &combatants[index_1];
            let c2 = &combatants[index_2];
            // Simplest friend-or-foe detection
            let is_enemy = c1.definition.id != c2.definition.id;
            if is_enemy {
                participants
                    .entry(c1.id())
                    .or_insert(CombatState::from_creature(c1.clone()));
                participants
                    .entry(c2.id())
                    .or_insert(CombatState::from_creature(c2.clone()));
                participants
                    .get_mut(&c1.id())
                    .unwrap()
                    .enemies
                    .insert(c2.id());
                participants
                    .get_mut(&c2.id())
                    .unwrap()
                    .enemies
                    .insert(c1.id());
            }
        }
    }

    if participants.is_empty() {
        // No enemies in group :)
        return None;
    }

    // Time for violence
    let mut order: Vec<CreatureId> = participants.keys().copied().collect();
    fastrand::shuffle(&mut order);
    let mut kills: Vec<_> = vec![];
    let mut attacks: Vec<_> = vec![];

    for _ in 0..max_rounds {
        let should_continue = participants.iter().any(|(_, combat)| {
            combat.is_alive() && combat.enemies.iter().any(|id| participants[id].is_alive())
        });
        if !should_continue {
            break;
        }

        for id in order.iter() {
            for _ in 0..participants[id].creature.definition.attack_count() {
                if !participants[id].is_alive() {
                    continue;
                }
                let targets: Vec<_> = participants[id]
                    .enemies
                    .iter()
                    .filter(|enemy_id| participants[*enemy_id].is_alive())
                    .collect();
                let target = match fastrand::choice(targets.iter()) {
                    Some(target) => **target,
                    None => continue,
                };

                let attack = Attack {
                    to_hit_modifier: participants[id].creature.definition.to_hit_modifier,
                    // Hard-coded
                    threat_treshold: 20,
                    armor_class: participants[&target].creature.definition.armor_class,
                };

                let attack_roll = AttackRoll::roll(attack.clone(), roll_d20);
                let mut damage = match attack_roll.result() {
                    AttackResult::CritMiss | AttackResult::Miss => None,
                    AttackResult::Hit => Some(participants[id].creature.definition.damage_roll()),
                    AttackResult::Crit => Some(
                        participants[id].creature.definition.max_damage
                            + participants[id].creature.definition.damage_roll(),
                    ),
                };
                if let Some(damage) = damage.as_mut() {
                    if participants
                        .get(&target)
                        .unwrap()
                        .creature
                        .definition
                        .invulnerable
                    {
                        *damage = 0;
                    }
                }
                let is_deathblow = match damage {
                    Some(damage) => {
                        let enemy = participants.get_mut(&target).unwrap();
                        enemy.damage = enemy.damage.saturating_add(damage);
                        !enemy.is_alive()
                    }
                    None => false,
                };
                if is_deathblow {
                    kills.push(KillData {
                        killed: target,
                        killer: Some(*id),
                    })
                }
                let attack_data = AttackData {
                    attacker: Some(*id),
                    target,
                    attack_roll,
                    damage,
                    is_deathblow,
                };
                attacks.push(attack_data);
            }
        }
    }

    Some(CombatResults { kills, attacks })
}
