use crate::config::{Config, atomic_write};
use anyhow::{Context, Result};
use chrono::{DateTime, Local};
use rand::prelude::IndexedRandom;
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, fs, path::PathBuf};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "mode", rename_all = "snake_case")]
pub enum VoiceMode {
    PersistentRandom { voice: String },
    Pinned { voice: String },
    RandomEachUtterance,
}

impl VoiceMode {
    pub fn display(&self) -> String {
        match self {
            Self::PersistentRandom { voice } => format!("random → {voice}"),
            Self::Pinned { voice } => format!("pinned → {voice}"),
            Self::RandomEachUtterance => "random every line".into(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Speaker {
    pub name: String,
    pub mode: VoiceMode,
    pub last_seen: DateTime<Local>,
    pub last_zone: Option<String>,
}

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct SpeakerStore {
    speakers: BTreeMap<String, Speaker>,
}

impl SpeakerStore {
    fn path() -> Result<PathBuf> {
        Ok(Config::app_dir()?.join("speakers.json"))
    }

    pub fn load() -> Result<Self> {
        let path = Self::path()?;
        if !path.exists() {
            return Ok(Self::default());
        }
        let contents = fs::read_to_string(&path)
            .with_context(|| format!("could not read {}", path.display()))?;
        serde_json::from_str(&contents).context("could not parse speaker database")
    }

    pub fn save(&self) -> Result<()> {
        atomic_write(&Self::path()?, serde_json::to_vec_pretty(self)?.as_slice())
    }

    pub fn resolve_voice(
        &mut self,
        key: String,
        name: String,
        voices: &[String],
        zone: Option<String>,
    ) -> Option<String> {
        let now = Local::now();
        let speaker = self.speakers.entry(key).or_insert_with(|| {
            let voice = choose_voice(voices).unwrap_or_else(|| "af_heart".into());
            Speaker {
                name,
                mode: VoiceMode::PersistentRandom { voice },
                last_seen: now,
                last_zone: zone.clone(),
            }
        });

        speaker.last_seen = now;
        speaker.last_zone = zone;
        match &speaker.mode {
            VoiceMode::PersistentRandom { voice } | VoiceMode::Pinned { voice } => {
                Some(voice.clone())
            }
            VoiceMode::RandomEachUtterance => choose_voice(voices),
        }
    }

    pub fn entries(&self) -> impl Iterator<Item = (&String, &Speaker)> {
        self.speakers.iter()
    }

    pub fn get(&self, key: &str) -> Option<&Speaker> {
        self.speakers.get(key)
    }

    pub fn pin(&mut self, key: &str, voice: String) {
        if let Some(speaker) = self.speakers.get_mut(key) {
            speaker.mode = VoiceMode::Pinned { voice };
        }
    }

    pub fn set_random_each_utterance(&mut self, key: &str) {
        if let Some(speaker) = self.speakers.get_mut(key) {
            speaker.mode = VoiceMode::RandomEachUtterance;
        }
    }

    pub fn reset_persistent_random(&mut self, key: &str, voices: &[String]) {
        if let (Some(speaker), Some(voice)) = (self.speakers.get_mut(key), choose_voice(voices)) {
            speaker.mode = VoiceMode::PersistentRandom { voice };
        }
    }
}

fn choose_voice(voices: &[String]) -> Option<String> {
    voices.choose(&mut rand::rng()).cloned()
}

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

    #[test]
    fn random_assignment_is_persisted() {
        let mut store = SpeakerStore::default();
        let voices = vec!["af_heart".into(), "am_adam".into()];
        let first = store.resolve_voice("npc:turga".into(), "Turga".into(), &voices, None);
        let next = store.resolve_voice("npc:turga".into(), "Turga".into(), &voices, None);
        assert_eq!(first, next);
    }

    #[test]
    fn random_each_utterance_remains_configurable() {
        let mut store = SpeakerStore::default();
        let voices = vec!["af_heart".into()];
        store.resolve_voice("npc:turga".into(), "Turga".into(), &voices, None);
        store.set_random_each_utterance("npc:turga");
        assert_eq!(
            store.get("npc:turga").unwrap().mode,
            VoiceMode::RandomEachUtterance
        );
    }
}
