//! Local debate simulator — drives the cinema standalone so we can polish
//! the visual before wiring the live SDK. Shaped like the live SSE stream
//! we'll build into next:
//!
//!   - `Simulator::new(idea)` opens a fresh "debate"
//!   - `tick(dt)` advances the state machine
//!   - public accessors expose the same shapes the live stream will produce:
//!     persona activation order, *streaming reactions per persona*, sentiment
//!     deltas, verdict
//!
//! When the SDK lands, we swap this for a `LiveDebate` that drains an
//! mpsc::Receiver from a tokio task reading SSE; the cinema doesn't care.
//!
//! Reactions stream tweet-by-tweet, not character-by-character — the
//! cinema renders them as a social-media-style feed of cards in the right
//! pane, one per archetype, in activation order.

use std::time::{Duration, Instant};

use crate::personas::ARCHETYPES;

const NUM_PERSONAS: usize = 10;

/// Wall-clock budget for each phase.
const WAKE_DURATION_MS: u128 = 5_000;
const REACTION_GAP_MS: u128 = 900; // one reaction posted per ~900ms after wake
const VERDICT_HOLD_MS: u128 = 3_500;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Phase {
    Waking,
    Streaming,
    VerdictReveal,
    Done,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Sentiment {
    Hostile,
    Neutral,
    Aligned,
}

impl Sentiment {
    pub fn label(self) -> &'static str {
        match self {
            Sentiment::Aligned => "aligned",
            Sentiment::Neutral => "neutral",
            Sentiment::Hostile => "hostile",
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub struct PersonaState {
    pub activated_at_ms: Option<u128>,
    pub sentiment: Sentiment,
    pub reacted_at_ms: Option<u128>,
}

#[derive(Debug, Clone, Copy)]
pub struct Reaction {
    pub persona_idx: usize,
    pub sentiment: Sentiment,
    /// Wall-clock ms when the reaction was emitted. Reserved for v0.2 when
    /// per-reaction tachyonfx fade-in is wired against this timestamp.
    #[allow(dead_code)]
    pub posted_at_ms: u128,
}

#[derive(Debug, Clone, Copy)]
pub struct Verdict {
    pub aligned_pct: u16,
    pub hostile_pct: u16,
    pub neutral_pct: u16,
    pub headline: &'static str,
}

pub struct Simulator {
    started: Instant,
    elapsed_ms: u128,
    personas: [PersonaState; NUM_PERSONAS],
    reactions: Vec<Reaction>,
    next_reaction_idx: usize, // index of next persona to emit a reaction
    next_reaction_ms: u128,
    /// Rolling history of sentiment counts (one snapshot per ~80ms).
    /// Three channels for the three sparklines. Capped at 240 samples.
    hostile_history: Vec<u64>,
    neutral_history: Vec<u64>,
    aligned_history: Vec<u64>,
    last_history_tick_ms: u128,
    rng_state: u64,
    verdict: Option<Verdict>,
    idea: String,
}

impl Simulator {
    pub fn new(idea: String) -> Self {
        let seed = fnv1a64(idea.as_bytes());
        Self {
            started: Instant::now(),
            elapsed_ms: 0,
            personas: [PersonaState {
                activated_at_ms: None,
                sentiment: Sentiment::Neutral,
                reacted_at_ms: None,
            }; NUM_PERSONAS],
            reactions: Vec::with_capacity(NUM_PERSONAS),
            next_reaction_idx: 0,
            next_reaction_ms: WAKE_DURATION_MS, // reactions begin after wake phase
            hostile_history: Vec::with_capacity(240),
            neutral_history: Vec::with_capacity(240),
            aligned_history: Vec::with_capacity(240),
            last_history_tick_ms: 0,
            rng_state: seed.max(1),
            verdict: None,
            idea,
        }
    }

    pub fn idea(&self) -> &str {
        &self.idea
    }

    pub fn elapsed(&self) -> Duration {
        self.started.elapsed()
    }

    pub fn phase(&self) -> Phase {
        let stream_end_ms = WAKE_DURATION_MS + (NUM_PERSONAS as u128) * REACTION_GAP_MS;
        match self.elapsed_ms {
            n if n < WAKE_DURATION_MS => Phase::Waking,
            n if n < stream_end_ms => Phase::Streaming,
            n if n < stream_end_ms + VERDICT_HOLD_MS => Phase::VerdictReveal,
            _ => Phase::Done,
        }
    }

    pub fn personas(&self) -> &[PersonaState; NUM_PERSONAS] {
        &self.personas
    }

    pub fn reactions(&self) -> &[Reaction] {
        &self.reactions
    }

    pub fn hostile_history(&self) -> &[u64] {
        &self.hostile_history
    }

    pub fn neutral_history(&self) -> &[u64] {
        &self.neutral_history
    }

    pub fn aligned_history(&self) -> &[u64] {
        &self.aligned_history
    }

    pub fn verdict(&self) -> Option<&Verdict> {
        self.verdict.as_ref()
    }

    /// Advance the simulation. Call every draw tick with the wall-clock delta.
    pub fn tick(&mut self, dt: Duration) {
        self.elapsed_ms = self.elapsed_ms.saturating_add(dt.as_millis());

        // Persona activation cascade — one persona wakes every 500ms during
        // the wake phase. Deterministic order is the archetype list itself.
        for (i, persona) in self.personas.iter_mut().enumerate() {
            let activation_ms = (i as u128) * 500;
            if persona.activated_at_ms.is_none() && self.elapsed_ms >= activation_ms {
                persona.activated_at_ms = Some(self.elapsed_ms);
                persona.sentiment = pick_sentiment(&mut self.rng_state, i);
            }
        }

        // Reaction emission — after wake phase, each persona posts their
        // reaction in turn, one every REACTION_GAP_MS.
        while self.next_reaction_idx < NUM_PERSONAS
            && self.elapsed_ms >= self.next_reaction_ms
        {
            let idx = self.next_reaction_idx;
            let sentiment = self.personas[idx].sentiment;
            self.reactions.push(Reaction {
                persona_idx: idx,
                sentiment,
                posted_at_ms: self.elapsed_ms,
            });
            self.personas[idx].reacted_at_ms = Some(self.elapsed_ms);
            self.next_reaction_idx += 1;
            self.next_reaction_ms += REACTION_GAP_MS;
        }

        // Sparkline history — sample every ~80ms.
        if self.elapsed_ms.saturating_sub(self.last_history_tick_ms) >= 80 {
            self.last_history_tick_ms = self.elapsed_ms;
            let mut hostile = 0u64;
            let mut neutral = 0u64;
            let mut aligned = 0u64;
            for p in &self.personas {
                if p.activated_at_ms.is_none() {
                    continue;
                }
                match p.sentiment {
                    Sentiment::Hostile => hostile += 1,
                    Sentiment::Neutral => neutral += 1,
                    Sentiment::Aligned => aligned += 1,
                }
            }
            push_capped(&mut self.hostile_history, hostile, 240);
            push_capped(&mut self.neutral_history, neutral, 240);
            push_capped(&mut self.aligned_history, aligned, 240);
        }

        // Lock in the verdict once the stream phase ends.
        let stream_end_ms = WAKE_DURATION_MS + (NUM_PERSONAS as u128) * REACTION_GAP_MS;
        if self.verdict.is_none() && self.elapsed_ms >= stream_end_ms {
            self.verdict = Some(self.compute_verdict());
        }
    }

    fn compute_verdict(&self) -> Verdict {
        let mut hostile = 0u16;
        let mut neutral = 0u16;
        let mut aligned = 0u16;
        for p in &self.personas {
            match p.sentiment {
                Sentiment::Hostile => hostile += 1,
                Sentiment::Neutral => neutral += 1,
                Sentiment::Aligned => aligned += 1,
            }
        }
        let total = (hostile + neutral + aligned).max(1);
        let aligned_pct = (aligned as u32 * 100 / total as u32) as u16;
        let hostile_pct = (hostile as u32 * 100 / total as u32) as u16;
        let neutral_pct = 100u16.saturating_sub(aligned_pct + hostile_pct);
        // Single-word headlines so the BigText overlay never clips, regardless
        // of card width.
        let headline = match aligned_pct {
            p if p >= 70 => "STRONG",
            p if p >= 50 => "ALIGNED",
            p if p >= 30 => "MIXED",
            _ => "HOSTILE",
        };
        Verdict {
            aligned_pct,
            hostile_pct,
            neutral_pct,
            headline,
        }
    }
}

fn push_capped(v: &mut Vec<u64>, value: u64, cap: usize) {
    if v.len() >= cap {
        v.remove(0);
    }
    v.push(value);
}

fn pick_sentiment(rng: &mut u64, persona_idx: usize) -> Sentiment {
    let roll = (xorshift(rng).wrapping_add(persona_idx as u64) % 100) as u8;
    match roll {
        0..=24 => Sentiment::Hostile,
        25..=39 => Sentiment::Neutral,
        _ => Sentiment::Aligned,
    }
}

fn xorshift(state: &mut u64) -> u64 {
    let mut s = *state;
    s ^= s << 13;
    s ^= s >> 7;
    s ^= s << 17;
    *state = s;
    s
}

fn fnv1a64(bytes: &[u8]) -> u64 {
    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
    for b in bytes {
        h ^= *b as u64;
        h = h.wrapping_mul(0x100_0000_01b3);
    }
    h
}

// Confirm the persona count matches the schema before we ship.
const _ASSERT_TEN_PERSONAS: () = {
    assert!(ARCHETYPES.len() == NUM_PERSONAS);
};

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn personas_wake_in_order() {
        let mut sim = Simulator::new("test idea".to_string());
        sim.tick(Duration::from_millis(0));
        for p in sim.personas() {
            assert!(p.activated_at_ms.is_none() || p.activated_at_ms == Some(0));
        }
        sim.tick(Duration::from_millis(5500));
        for p in sim.personas() {
            assert!(p.activated_at_ms.is_some(), "all 10 should be awake by 5.5s");
        }
    }

    #[test]
    fn reactions_post_in_order() {
        let mut sim = Simulator::new("post order".to_string());
        // Run past wake + a few reaction slots.
        sim.tick(Duration::from_millis(5000 + 3 * 900));
        let reactions = sim.reactions();
        assert!(reactions.len() >= 3);
        for (i, r) in reactions.iter().enumerate() {
            assert_eq!(r.persona_idx, i);
        }
    }

    #[test]
    fn phase_advances_through_full_run() {
        let mut sim = Simulator::new("phase test".to_string());
        assert_eq!(sim.phase(), Phase::Waking);
        sim.tick(Duration::from_millis(6_000));
        assert_eq!(sim.phase(), Phase::Streaming);
        // Wake (5s) + 10 reactions * 900ms = 14s; reveal phase starts then.
        sim.tick(Duration::from_millis(9_000));
        assert_eq!(sim.phase(), Phase::VerdictReveal);
        sim.tick(Duration::from_millis(4_000));
        assert_eq!(sim.phase(), Phase::Done);
        assert!(sim.verdict().is_some());
    }

    #[test]
    fn verdict_percentages_sum_to_100() {
        let mut sim = Simulator::new("sums".to_string());
        sim.tick(Duration::from_millis(20_000));
        let v = sim.verdict().expect("verdict computed by Done phase");
        let total = v.aligned_pct + v.hostile_pct + v.neutral_pct;
        assert!((99..=101).contains(&total), "got total={total}");
    }

    #[test]
    fn same_idea_is_reproducible() {
        let mut a = Simulator::new("reproducible idea".to_string());
        let mut b = Simulator::new("reproducible idea".to_string());
        a.tick(Duration::from_millis(20_000));
        b.tick(Duration::from_millis(20_000));
        let va = a.verdict().unwrap();
        let vb = b.verdict().unwrap();
        assert_eq!(va.aligned_pct, vb.aligned_pct);
        assert_eq!(va.hostile_pct, vb.hostile_pct);
        // Reactions list should match exactly too.
        assert_eq!(a.reactions().len(), b.reactions().len());
        for (ra, rb) in a.reactions().iter().zip(b.reactions().iter()) {
            assert_eq!(ra.persona_idx, rb.persona_idx);
            assert_eq!(ra.sentiment, rb.sentiment);
        }
    }
}
