//! Controllers (spec §5): the pluggable *intent* layer. The [`Actor`] is inert
//! state; a `Controller` decides what it tries to do each tick. Players and
//! monsters run the identical combat math — only the controller differs, so a
//! ladder of scripted policies of varying skill is exactly how we measure the
//! gradual→brutal difficulty curve (§12).
//!
//! A controller returns an [`Action`]; the engine validates affordability,
//! cooldown, and fizzle, then resolves it. Controllers only ever *read* the
//! world (`&View`) and draw from a deterministic, per-decision forked RNG, so the
//! whole decision step is reproducible (§10 step 0).

use crate::actor::{Actor, ActorId};
use crate::ability::Ability;
use crate::config::Rules;
use crate::effect::EffectKind;
use crate::fixed::{apply_bp, Milli, BP_ONE};
use crate::mitigation::MitigationStage;
use crate::rng::Rng;

/// What an actor tries to do this tick. The engine has final say (it may block an
/// unaffordable/on-cooldown choice and record the reason).
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Action {
    /// Wait — regen and watch.
    Idle,
    /// Begin `ability_id` against `targets` (resolved from the actor's loadout).
    Use { ability_id: String, targets: Vec<ActorId> },
}

/// A read-only snapshot the controller reasons over.
pub struct View<'a> {
    pub me: &'a Actor,
    pub actors: &'a [Actor],
    pub tick: u64,
    pub rules: &'a Rules,
}

impl<'a> View<'a> {
    /// Conscious enemies (different team, still in the fight).
    pub fn enemies(&self) -> Vec<&'a Actor> {
        self.actors
            .iter()
            .filter(|a| a.team != self.me.team && a.is_active())
            .collect()
    }

    /// Conscious allies other than me.
    pub fn allies(&self) -> Vec<&'a Actor> {
        self.actors
            .iter()
            .filter(|a| a.team == self.me.team && a.id != self.me.id && a.is_active())
            .collect()
    }

    /// The enemy closest to going down (lowest cap, ties broken by lowest fill
    /// then id) — the focus-fire / punish target.
    pub fn weakest_enemy(&self) -> Option<&'a Actor> {
        self.enemies()
            .into_iter()
            .min_by_key(|a| (a.cap(), a.vigor, a.id))
    }

    /// An enemy mid-cast (a wind-up worth reacting to defensively).
    pub fn enemy_casting(&self) -> Option<&'a Actor> {
        self.enemies().into_iter().find(|a| a.casting.is_some())
    }

    fn usable(&self, ab: &Ability) -> bool {
        ab.cost.fill <= self.me.vigor && !self.me.on_cooldown(&ab.id, self.tick)
    }
}

// --- ability introspection (role inference, no manual tagging) -------------

fn is_recovery(ab: &Ability) -> bool {
    ab.effects.iter().any(|e| {
        matches!(e.kind, EffectKind::Recovery { .. } | EffectKind::Hot { .. })
    })
}

fn is_active_defense(ab: &Ability) -> bool {
    ab.effects.iter().any(|e| {
        matches!(
            e.kind,
            EffectKind::MitigationBuff { stage: MitigationStage::ActiveDefense, .. }
        )
    })
}

/// Total fill-damage magnitude (bp of target max_vigor) this ability throws,
/// folding in potency — the rough "how big a hit" used to spot a punish.
fn fill_damage_bp(ab: &Ability) -> i64 {
    let raw: i64 = ab
        .effects
        .iter()
        .filter_map(|e| match e.kind {
            EffectKind::FillDamage { magnitude_bp } => Some(magnitude_bp),
            EffectKind::Dot { per_tick_bp, duration } => Some(per_tick_bp * duration as i64),
            _ => None,
        })
        .sum();
    apply_bp(raw, ab.potency_bp)
}

/// The actor's auto-attack: the cheapest offensive ability (fill cost), ties to
/// the lowest id-stable order. Falls back to any offensive ability.
fn auto_attack(me: &Actor) -> Option<&Ability> {
    me.loadout
        .iter()
        .filter(|a| a.is_offensive())
        .min_by_key(|a| (a.cost.fill, a.name.clone()))
}

/// The biggest affordable, off-cooldown offensive hit.
fn best_heavy<'a>(view: &View<'a>) -> Option<&'a Ability> {
    view.me
        .loadout
        .iter()
        .filter(|a| a.is_offensive() && view.usable(a))
        .max_by_key(|a| (fill_damage_bp(a), a.cost.fill))
}

/// A usable recovery ability, if any.
fn usable_recovery<'a>(view: &View<'a>) -> Option<&'a Ability> {
    view.me
        .loadout
        .iter()
        .find(|a| is_recovery(a) && view.usable(a))
}

/// A usable active-defense ability, if any.
fn usable_defense<'a>(view: &View<'a>) -> Option<&'a Ability> {
    view.me
        .loadout
        .iter()
        .find(|a| is_active_defense(a) && view.usable(a))
}

/// Estimate the landed magnitude of `ab` against `target` for punish timing —
/// con + armor only (defenses/buffs are unknowable to the attacker). Good enough
/// to judge "would this overrun their capacity?".
fn estimate_landed(view: &View, ab: &Ability, target: &Actor) -> Milli {
    let pre = apply_bp(target.max_vigor, fill_damage_bp(ab).max(0));
    let eff_level = view
        .me
        .effective_level(&ab.school, view.rules.competency_level_per_point_bp);
    let con = view.rules.con_mult_bp(eff_level, target.level);
    let after_con = apply_bp(pre, con);
    apply_bp(after_con, (BP_ONE - target.armor_bp).max(0))
}

/// Whether `landed` would push `target` past the daze threshold (overruns
/// capacity) — the moment to strike (§4/§5).
fn would_overrun(view: &View, landed: Milli, target: &Actor) -> bool {
    let lhs = landed as i128 * BP_ONE as i128;
    let rhs = target.vigor as i128 * view.rules.stagger.daze_threshold_bp as i128;
    lhs > rhs
}

/// The decision interface. `decide` is pure w.r.t. the world (`&View`) and draws
/// only from the deterministic `rng`.
pub trait Controller: std::fmt::Debug {
    fn decide(&mut self, view: &View, rng: &mut Rng) -> Action;
    fn label(&self) -> &str;
}

/// Dumb AI **and** the §12 auto-attack-only guardrail policy: only ever swings,
/// on cooldown, at the weakest enemy. Never touches an ability. If the fraction
/// of fights this wins is high, the interesting verbs are a tax (thesis broken).
#[derive(Debug, Clone)]
pub struct SwingOnCooldown;

impl Controller for SwingOnCooldown {
    fn decide(&mut self, view: &View, _rng: &mut Rng) -> Action {
        let Some(target) = view.weakest_enemy() else {
            return Action::Idle;
        };
        match auto_attack(view.me) {
            Some(ab) if view.usable(ab) => Action::Use {
                ability_id: ab.id.clone(),
                targets: vec![target.id],
            },
            _ => Action::Idle,
        }
    }
    fn label(&self) -> &str {
        "swing_on_cooldown"
    }
}

/// A "player" policy with a **skill** knob (0..100). It knows the right play —
/// recover when low, defend a telegraphed cast, punish a spent enemy with the
/// heavy, poke otherwise — but executes the optimal line only `skill`% of the
/// time, otherwise falling back to a plain swing. Sweeping `skill` traces the
/// difficulty curve (§5/§12).
#[derive(Debug, Clone)]
pub struct ScriptedPlayer {
    /// 0 = flails, 100 = always optimal.
    pub skill: i64,
    /// Recover when cap drops below this fraction of max (bp).
    pub recover_below_bp: i64,
}

impl ScriptedPlayer {
    pub fn new(skill: i64) -> Self {
        Self { skill: skill.clamp(0, 100), recover_below_bp: 3_500 }
    }

    fn plays_well(&self, rng: &mut Rng) -> bool {
        rng.chance_bp(self.skill * 100) // skill% as bp
    }
}

impl Controller for ScriptedPlayer {
    fn decide(&mut self, view: &View, rng: &mut Rng) -> Action {
        let Some(target) = view.weakest_enemy() else {
            return Action::Idle;
        };
        let optimal = self.plays_well(rng);
        let swing = || -> Action {
            match auto_attack(view.me) {
                Some(ab) if view.usable(ab) => Action::Use {
                    ability_id: ab.id.clone(),
                    targets: vec![target.id],
                },
                _ => Action::Idle,
            }
        };

        if !optimal {
            // Misplay: just swing (or idle if it can't).
            return swing();
        }

        // 1. Survival first: low ceiling → recover.
        let cap_frac = if view.me.max_vigor > 0 {
            (view.me.cap() as i128 * BP_ONE as i128 / view.me.max_vigor as i128) as i64
        } else {
            0
        };
        if cap_frac < self.recover_below_bp {
            if let Some(rec) = usable_recovery(view) {
                return Action::Use {
                    ability_id: rec.id.clone(),
                    targets: vec![view.me.id],
                };
            }
        }

        // 2. React to a telegraphed cast: defend.
        if view.enemy_casting().is_some() {
            if let Some(def) = usable_defense(view) {
                return Action::Use {
                    ability_id: def.id.clone(),
                    targets: vec![view.me.id],
                };
            }
        }

        // 3. Pressure with the heaviest affordable hit. A spent enemy means a
        //    punish (the hit overruns their capacity → stagger); otherwise it's
        //    just proactive damage while our own fill is healthy enough to spend.
        //    Real players lead with their big button — hoarding it for the exact
        //    overrun frame is not representative.
        let fill_frac = if view.me.max_vigor > 0 {
            (view.me.vigor as i128 * BP_ONE as i128 / view.me.max_vigor as i128) as i64
        } else {
            0
        };
        if let Some(heavy) = best_heavy(view) {
            let est = estimate_landed(view, heavy, target);
            let punish = would_overrun(view, est, target);
            if punish || fill_frac > 4_500 {
                return Action::Use {
                    ability_id: heavy.id.clone(),
                    targets: vec![target.id],
                };
            }
        }

        // 4. Otherwise poke with the cheapest usable offensive ability.
        swing()
    }

    fn label(&self) -> &str {
        "scripted_player"
    }
}

/// The hold-and-punish AI (§5): conserve, watch the target's fill, and unload the
/// biggest hit exactly when they've spent low enough to overrun their capacity.
/// Defends telegraphed casts. The timing meta-game, played by the machine.
#[derive(Debug, Clone)]
pub struct HoldAndPunish {
    /// Pure-poke fallback below this cap fraction (don't sit at death's door).
    pub panic_below_bp: i64,
}

impl Default for HoldAndPunish {
    fn default() -> Self {
        Self { panic_below_bp: 3_000 }
    }
}

impl Controller for HoldAndPunish {
    fn decide(&mut self, view: &View, _rng: &mut Rng) -> Action {
        let Some(target) = view.weakest_enemy() else {
            return Action::Idle;
        };

        // Self-preservation: if our own ceiling is collapsing, recover.
        let cap_frac = if view.me.max_vigor > 0 {
            (view.me.cap() as i128 * BP_ONE as i128 / view.me.max_vigor as i128) as i64
        } else {
            0
        };
        if cap_frac < self.panic_below_bp {
            if let Some(rec) = usable_recovery(view) {
                return Action::Use {
                    ability_id: rec.id.clone(),
                    targets: vec![view.me.id],
                };
            }
        }

        // Defend a telegraphed cast.
        if view.enemy_casting().is_some() {
            if let Some(def) = usable_defense(view) {
                return Action::Use {
                    ability_id: def.id.clone(),
                    targets: vec![view.me.id],
                };
            }
        }

        // The haymaker: fire the heaviest hit the instant it would overrun.
        if let Some(heavy) = best_heavy(view) {
            let est = estimate_landed(view, heavy, target);
            if would_overrun(view, est, target) {
                return Action::Use {
                    ability_id: heavy.id.clone(),
                    targets: vec![target.id],
                };
            }
        }

        // Not the moment — chip with a cheap swing, conserving the bar for the
        // punish window rather than burning the heavy early.
        match auto_attack(view.me) {
            Some(ab) if view.usable(ab) => Action::Use {
                ability_id: ab.id.clone(),
                targets: vec![target.id],
            },
            _ => Action::Idle,
        }
    }

    fn label(&self) -> &str {
        "hold_and_punish"
    }
}

/// Input-driven controller for a real-time front-end (spec §5 "player controller").
///
/// Intent lives behind a shared [`HumanHandle`] the UI mutates between ticks: a
/// queued one-shot action (a clicked ability) takes priority; otherwise, if
/// auto-attack is engaged, it swings at the current target. The engine drives it
/// like any other controller, so the player runs the identical combat math.
#[derive(Debug, Default)]
pub struct HumanIntent {
    /// A one-shot action set by the UI; consumed on the next decision.
    pub queued: Option<Action>,
    /// The actor the passive auto-attack swings at.
    pub target: Option<ActorId>,
    /// Whether auto-attack is engaged (classic always-on melee).
    pub auto_attack: bool,
}

/// A shared handle to a player's intent. The UI holds one clone, the controller
/// the other. Single-threaded (front-end / WASM), so `Rc<RefCell<…>>` is right.
pub type HumanHandle = std::rc::Rc<std::cell::RefCell<HumanIntent>>;

/// The player's controller. Build with [`HumanController::new`], then hand its
/// [`HumanController::handle`] to the UI to push intent.
#[derive(Debug, Clone)]
pub struct HumanController {
    handle: HumanHandle,
}

impl Default for HumanController {
    fn default() -> Self {
        Self::new()
    }
}

impl HumanController {
    pub fn new() -> Self {
        Self {
            handle: std::rc::Rc::new(std::cell::RefCell::new(HumanIntent {
                queued: None,
                target: None,
                auto_attack: true,
            })),
        }
    }

    /// A clone of the shared intent handle for the UI to mutate.
    pub fn handle(&self) -> HumanHandle {
        self.handle.clone()
    }
}

impl Controller for HumanController {
    fn decide(&mut self, view: &View, _rng: &mut Rng) -> Action {
        let mut intent = self.handle.borrow_mut();
        if let Some(action) = intent.queued.take() {
            return action;
        }
        if intent.auto_attack {
            // Prefer the explicitly chosen target if it's still a live enemy,
            // else fall back to the weakest enemy.
            let target = intent
                .target
                .and_then(|t| view.actors.get(t as usize))
                .filter(|a| a.team != view.me.team && a.is_active())
                .map(|a| a.id)
                .or_else(|| view.weakest_enemy().map(|a| a.id));
            if let Some(t) = target {
                if let Some(ab) = auto_attack(view.me) {
                    if view.usable(ab) {
                        return Action::Use { ability_id: ab.id.clone(), targets: vec![t] };
                    }
                }
            }
        }
        Action::Idle
    }

    fn label(&self) -> &str {
        "human"
    }
}
