use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::{
    fs,
    path::{Path, PathBuf},
};

pub const APP_DIRECTORY: &str = "mudmouth";

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
    pub kokoro_url: String,
    pub log_path: Option<PathBuf>,
    pub game_directory: Option<PathBuf>,
    pub volume: f32,
    pub max_queue_length: usize,
    pub repeat_window_seconds: u64,
    pub repeat_second_speed: f32,
    pub repeat_third_plus_speed: f32,
    pub events: EventSettings,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct EventSettings {
    pub npc_dialogue: bool,
    pub zone_transitions: bool,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            kokoro_url: "http://127.0.0.1:8880".into(),
            log_path: None,
            game_directory: None,
            volume: 0.85,
            max_queue_length: 10,
            repeat_window_seconds: 15,
            repeat_second_speed: 1.5,
            repeat_third_plus_speed: 2.0,
            events: EventSettings::default(),
        }
    }
}

impl Default for EventSettings {
    fn default() -> Self {
        Self {
            npc_dialogue: true,
            zone_transitions: false,
        }
    }
}

impl Config {
    pub fn app_dir() -> Result<PathBuf> {
        dirs::data_local_dir()
            .or_else(dirs::data_dir)
            .map(|path| path.join(APP_DIRECTORY))
            .context("could not determine a platform data directory")
    }

    pub fn path() -> Result<PathBuf> {
        Ok(Self::app_dir()?.join("config.toml"))
    }

    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()))?;
        toml::from_str(&contents).context("could not parse Mudmouth config")
    }

    pub fn save(&self) -> Result<()> {
        let path = Self::path()?;
        atomic_write(&path, toml::to_string_pretty(self)?.as_bytes())
    }
}

pub fn atomic_write(path: &Path, contents: &[u8]) -> Result<()> {
    let parent = path
        .parent()
        .context("cannot save a file without a parent directory")?;
    fs::create_dir_all(parent)?;
    let temporary = path.with_extension("tmp");
    fs::write(&temporary, contents)
        .with_context(|| format!("could not write {}", temporary.display()))?;
    fs::rename(&temporary, path)
        .with_context(|| format!("could not replace {}", path.display()))?;
    Ok(())
}

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

    #[test]
    fn atomically_replaces_config_contents() {
        let directory = tempdir().unwrap();
        let path = directory.path().join("config.toml");
        atomic_write(&path, b"volume = 0.5\n").unwrap();
        atomic_write(&path, b"volume = 0.8\n").unwrap();
        assert_eq!(std::fs::read_to_string(path).unwrap(), "volume = 0.8\n");
    }

    #[test]
    fn config_defaults_enable_only_npc_dialogue() {
        let config = Config::default();
        assert!(config.events.npc_dialogue);
        assert!(!config.events.zone_transitions);
    }
}
