//! Instrumentation (spec §12): a structured event stream plus a recorder that
//! rolls it into per-fight metrics. Outcomes alone can't tell you *why* a config
//! feels bad, so the schema records **causes** — pre-mitigation damage and each
//! mitigation stage's contribution (you can't tune the con curve from `landed`
//! alone), `blocked` reasons (a "wanted to act but couldn't" signal that makes
//! soft-lock/turtle modes visible), and a fizzle-vs-interrupt split.

use crate::actor::{Actor, ActorId, HitOutcome, StaggerState};
use crate::fixed::Milli;
use serde::{Deserialize, Serialize};

/// Why an attempted action didn't happen (§12 `action_blocked_reason`).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BlockedReason {
    CantAfford,
    OnCooldown,
    Dazed,
    NoTarget,
}

/// Why a committed ability produced nothing (§12 `fail_reason`). Kept separate so
/// "competency too low" and "wind-ups too long" never collapse into one signal.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FailReason {
    CompetencyFizzle,
    Interrupted,
}

/// How a fight ended.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Outcome {
    /// A single team had the last conscious actor(s) standing.
    Win { team: u8 },
    /// No conscious actors on any team (mutual collapse).
    Draw,
    /// Tick budget exhausted with multiple teams still up.
    Timeout,
}

/// The raw event stream (opt-in; not stored for million-fight sweeps). A tagged
/// enum so it serializes cleanly to JSON for inspecting a single fight.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum Event {
    Action {
        tick: u64,
        actor: ActorId,
        ability: String,
        fill_before: Milli,
        fill_after: Milli,
        fatigue: Milli,
        cap: Milli,
    },
    CastStarted {
        tick: u64,
        actor: ActorId,
        ability: String,
        completes_at: u64,
    },
    Blocked {
        tick: u64,
        actor: ActorId,
        ability: String,
        reason: BlockedReason,
    },
    Fail {
        tick: u64,
        actor: ActorId,
        ability: String,
        reason: FailReason,
    },
    Hit {
        tick: u64,
        source: ActorId,
        target: ActorId,
        ability: String,
        pre_mitigation: Milli,
        armor: Milli,
        con: Milli,
        defense: Milli,
        stance: Milli,
        buff: Milli,
        landed: Milli,
        outcome: HitKind,
        target_fill_after: Milli,
        target_cap_after: Milli,
    },
    Ceiling {
        tick: u64,
        source: ActorId,
        target: ActorId,
        ability: String,
        fatigue_added: Milli,
    },
    Recovery {
        tick: u64,
        source: ActorId,
        target: ActorId,
        ability: String,
        fatigue_healed: Milli,
        revived: bool,
    },
    Death {
        tick: u64,
        actor: ActorId,
    },
    End {
        tick: u64,
        outcome: Outcome,
    },
}

/// Serializable mirror of [`HitOutcome`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HitKind {
    Absorbed,
    Dazed,
    Unconscious,
    Killed,
}

impl From<HitOutcome> for HitKind {
    fn from(o: HitOutcome) -> Self {
        match o {
            HitOutcome::Absorbed => HitKind::Absorbed,
            HitOutcome::Dazed => HitKind::Dazed,
            HitOutcome::Unconscious => HitKind::Unconscious,
            HitOutcome::Killed => HitKind::Killed,
        }
    }
}

/// Serializable final state of an actor.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FinalState {
    Alive,
    Dazed,
    Unconscious,
    Dead,
}

impl From<StaggerState> for FinalState {
    fn from(s: StaggerState) -> Self {
        match s {
            StaggerState::Normal => FinalState::Alive,
            StaggerState::Dazed { .. } => FinalState::Dazed,
            StaggerState::Unconscious => FinalState::Unconscious,
            StaggerState::Dead => FinalState::Dead,
        }
    }
}

/// Per-actor rolled-up metrics for one fight (§12).
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ActorMetrics {
    pub id: ActorId,
    pub name: String,
    pub team: u8,
    pub level: i32,

    // Throughput.
    pub fill_damage_dealt: Milli,
    pub fill_damage_taken: Milli,
    pub ceiling_damage_dealt: Milli,
    pub ceiling_damage_taken: Milli,

    // Fill-flow accounting (§12 make-or-break co-tuning): where every point went.
    pub fill_gained_regen: Milli,
    pub fill_spent_actions: Milli,
    pub fill_lost_damage: Milli,
    pub fill_restored: Milli,

    // Stagger ledger.
    pub dazes_inflicted: u32,
    pub unconscious_inflicted: u32,
    pub dazes_taken: u32,
    pub unconscious_taken: u32,

    // Reliability.
    pub fizzles: u32,
    pub interruptions_suffered: u32,

    // Blocked-action breakdown.
    pub blocked_cant_afford: u32,
    pub blocked_on_cooldown: u32,
    pub blocked_dazed: u32,
    pub blocked_no_target: u32,

    // Action mix — feeds the auto-attack-only guardrail (§12).
    pub auto_attacks: u32,
    pub abilities_used: u32,
    pub recoveries_used: u32,
    pub idle_ticks: u32,

    pub near_death_recoveries: u32,

    // Lows over the fight.
    pub min_fill: Milli,
    pub min_cap: Milli,

    pub final_state: FinalState,

    /// Sampled `cap` over time (the ceiling-decay curve). Empty unless sampling
    /// is enabled.
    pub cap_series: Vec<Milli>,
    /// Sampled `vigor` over time. Empty unless sampling is enabled.
    pub fill_series: Vec<Milli>,
}

impl ActorMetrics {
    fn new(a: &Actor) -> Self {
        Self {
            id: a.id,
            name: a.name.clone(),
            team: a.team,
            level: a.level,
            fill_damage_dealt: 0,
            fill_damage_taken: 0,
            ceiling_damage_dealt: 0,
            ceiling_damage_taken: 0,
            fill_gained_regen: 0,
            fill_spent_actions: 0,
            fill_lost_damage: 0,
            fill_restored: 0,
            dazes_inflicted: 0,
            unconscious_inflicted: 0,
            dazes_taken: 0,
            unconscious_taken: 0,
            fizzles: 0,
            interruptions_suffered: 0,
            blocked_cant_afford: 0,
            blocked_on_cooldown: 0,
            blocked_dazed: 0,
            blocked_no_target: 0,
            auto_attacks: 0,
            abilities_used: 0,
            recoveries_used: 0,
            idle_ticks: 0,
            near_death_recoveries: 0,
            min_fill: a.vigor,
            min_cap: a.cap(),
            final_state: FinalState::Alive,
            cap_series: Vec::new(),
            fill_series: Vec::new(),
        }
    }
}

/// Whole-fight metrics (§12). The sim exports one row per fight from this.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FightMetrics {
    pub seed: u64,
    pub ticks: u64,
    pub tick_rate: u32,
    pub outcome: Outcome,
    pub actors: Vec<ActorMetrics>,
}

impl FightMetrics {
    /// Fight duration in seconds (derived from ticks and rate).
    pub fn seconds(&self) -> f64 {
        self.ticks as f64 / self.tick_rate.max(1) as f64
    }

    /// Look up an actor's metrics by id.
    pub fn actor(&self, id: ActorId) -> Option<&ActorMetrics> {
        self.actors.iter().find(|m| m.id == id)
    }
}

/// Accumulates events into [`FightMetrics`]. Always tracks the rollups; only
/// retains the raw [`Event`] log when `log_events` is set (cheap for a single
/// inspected fight, skipped for million-fight sweeps). Sampling `cap`/`fill`
/// series is gated by `sample_every` (0 = off).
#[derive(Clone, Debug)]
pub struct FightRecorder {
    metrics: Vec<ActorMetrics>,
    events: Vec<Event>,
    log_events: bool,
    sample_every: u32,
    tick_rate: u32,
    seed: u64,
}

impl FightRecorder {
    pub fn new(actors: &[Actor], seed: u64, tick_rate: u32, log_events: bool, sample_every: u32) -> Self {
        Self {
            metrics: actors.iter().map(ActorMetrics::new).collect(),
            events: Vec::new(),
            log_events,
            sample_every,
            tick_rate,
            seed,
        }
    }

    fn m(&mut self, id: ActorId) -> &mut ActorMetrics {
        // Actor ids are dense 0..n in encounters; fall back to a search if not.
        let idx = self
            .metrics
            .iter()
            .position(|x| x.id == id)
            .expect("metrics exist for every actor");
        &mut self.metrics[idx]
    }

    fn push(&mut self, e: Event) {
        if self.log_events {
            self.events.push(e);
        }
    }

    // --- engine-facing recording API --------------------------------------

    pub fn action(&mut self, tick: u64, a: &Actor, ability: &str, fill_before: Milli) {
        self.push(Event::Action {
            tick,
            actor: a.id,
            ability: ability.to_string(),
            fill_before,
            fill_after: a.vigor,
            fatigue: a.fatigue,
            cap: a.cap(),
        });
    }

    pub fn cast_started(&mut self, tick: u64, actor: ActorId, ability: &str, completes_at: u64) {
        self.push(Event::CastStarted {
            tick,
            actor,
            ability: ability.to_string(),
            completes_at,
        });
    }

    pub fn idle(&mut self, actor: ActorId) {
        self.m(actor).idle_ticks += 1;
    }

    pub fn auto_attack(&mut self, actor: ActorId) {
        self.m(actor).auto_attacks += 1;
    }

    pub fn ability_used(&mut self, actor: ActorId) {
        self.m(actor).abilities_used += 1;
    }

    pub fn recovery_used(&mut self, actor: ActorId) {
        self.m(actor).recoveries_used += 1;
    }

    pub fn spent(&mut self, actor: ActorId, fill: Milli) {
        self.m(actor).fill_spent_actions += fill;
    }

    pub fn blocked(&mut self, tick: u64, actor: ActorId, ability: &str, reason: BlockedReason) {
        {
            let m = self.m(actor);
            match reason {
                BlockedReason::CantAfford => m.blocked_cant_afford += 1,
                BlockedReason::OnCooldown => m.blocked_on_cooldown += 1,
                BlockedReason::Dazed => m.blocked_dazed += 1,
                BlockedReason::NoTarget => m.blocked_no_target += 1,
            }
        }
        self.push(Event::Blocked {
            tick,
            actor,
            ability: ability.to_string(),
            reason,
        });
    }

    pub fn fizzle(&mut self, tick: u64, actor: ActorId, ability: &str) {
        self.m(actor).fizzles += 1;
        self.push(Event::Fail {
            tick,
            actor,
            ability: ability.to_string(),
            reason: FailReason::CompetencyFizzle,
        });
    }

    pub fn interrupted(&mut self, tick: u64, actor: ActorId, ability: &str) {
        self.m(actor).interruptions_suffered += 1;
        self.push(Event::Fail {
            tick,
            actor,
            ability: ability.to_string(),
            reason: FailReason::Interrupted,
        });
    }

    /// Record a resolved hit with its full mitigation breakdown.
    #[allow(clippy::too_many_arguments)]
    pub fn hit(
        &mut self,
        tick: u64,
        source: ActorId,
        ability: &str,
        target: &Actor,
        mit: crate::mitigation::MitigationResult,
        outcome: HitOutcome,
    ) {
        {
            let m = self.m(source);
            m.fill_damage_dealt += mit.landed;
            match outcome {
                HitOutcome::Dazed => m.dazes_inflicted += 1,
                HitOutcome::Unconscious | HitOutcome::Killed => m.unconscious_inflicted += 1,
                HitOutcome::Absorbed => {}
            }
        }
        {
            let m = self.m(target.id);
            m.fill_damage_taken += mit.landed;
            m.fill_lost_damage += mit.landed;
            match outcome {
                HitOutcome::Dazed => m.dazes_taken += 1,
                HitOutcome::Unconscious => m.unconscious_taken += 1,
                _ => {}
            }
        }
        self.push(Event::Hit {
            tick,
            source,
            target: target.id,
            ability: ability.to_string(),
            pre_mitigation: mit.pre_mitigation,
            armor: mit.armor_delta,
            con: mit.con_delta,
            defense: mit.defense_delta,
            stance: mit.stance_delta,
            buff: mit.buff_delta,
            landed: mit.landed,
            outcome: outcome.into(),
            target_fill_after: target.vigor,
            target_cap_after: target.cap(),
        });
    }

    pub fn ceiling_damage(&mut self, tick: u64, source: ActorId, target: ActorId, ability: &str, fatigue: Milli) {
        self.m(source).ceiling_damage_dealt += fatigue;
        self.m(target).ceiling_damage_taken += fatigue;
        self.push(Event::Ceiling {
            tick,
            source,
            target,
            ability: ability.to_string(),
            fatigue_added: fatigue,
        });
    }

    pub fn recovery(
        &mut self,
        tick: u64,
        source: ActorId,
        target: ActorId,
        ability: &str,
        fatigue_healed: Milli,
        revived: bool,
    ) {
        if revived {
            self.m(target).near_death_recoveries += 1;
        }
        self.push(Event::Recovery {
            tick,
            source,
            target,
            ability: ability.to_string(),
            fatigue_healed,
            revived,
        });
    }

    pub fn fill_restored(&mut self, target: ActorId, amount: Milli) {
        self.m(target).fill_restored += amount;
    }

    pub fn regen(&mut self, actor: ActorId, amount: Milli) {
        self.m(actor).fill_gained_regen += amount;
    }

    pub fn death(&mut self, tick: u64, actor: ActorId) {
        self.push(Event::Death { tick, actor });
    }

    /// Per-tick sampling of lows + (optional) time series.
    pub fn sample(&mut self, tick: u64, a: &Actor) {
        let store_series = self.sample_every > 0 && tick.is_multiple_of(self.sample_every as u64);
        let m = self.m(a.id);
        m.min_fill = m.min_fill.min(a.vigor);
        m.min_cap = m.min_cap.min(a.cap());
        if store_series {
            m.cap_series.push(a.cap());
            m.fill_series.push(a.vigor);
        }
    }

    /// Finalize into [`FightMetrics`], stamping each actor's terminal state.
    pub fn finish(mut self, tick: u64, outcome: Outcome, actors: &[Actor]) -> FightMetrics {
        for a in actors {
            self.m(a.id).final_state = a.stagger.into();
        }
        self.push(Event::End { tick, outcome });
        FightMetrics {
            seed: self.seed,
            ticks: tick,
            tick_rate: self.tick_rate,
            outcome,
            actors: self.metrics,
        }
    }

    /// The raw event log (empty unless `log_events` was set).
    pub fn into_events(self) -> Vec<Event> {
        self.events
    }

    /// Take the events buffered since the last drain, clearing it. For a
    /// real-time front-end pulling per-tick events without retaining the fight.
    pub fn take_events(&mut self) -> Vec<Event> {
        std::mem::take(&mut self.events)
    }
}
