use crate::Result;
use anyhow::Context;
use serde::{Deserialize, Serialize};
use std::{
    collections::HashMap,
    fs::{self, OpenOptions},
    io::Write,
    path::{Path, PathBuf},
};

const DEFAULT_PROFILE_NAME: &str = "default";

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigProfile {
    #[serde(default)]
    pub username: Option<String>,
    #[serde(default = "ConfigProfile::default_base_url")]
    pub base_url: String,
    #[serde(default = "ConfigProfile::default_jsonapi_path")]
    pub jsonapi_path: String,
    #[serde(default)]
    pub basic_auth_b64: Option<String>,
    #[serde(default)]
    pub download_root: Option<PathBuf>,
    #[serde(default = "ConfigProfile::default_max_concurrent_downloads")]
    pub max_concurrent_downloads: usize,
}

impl Default for ConfigProfile {
    fn default() -> Self {
        Self {
            username: None,
            base_url: Self::default_base_url(),
            jsonapi_path: Self::default_jsonapi_path(),
            basic_auth_b64: None,
            download_root: None,
            max_concurrent_downloads: Self::default_max_concurrent_downloads(),
        }
    }
}

impl ConfigProfile {
    fn default_base_url() -> String {
        "https://studip.uni-trier.de".to_string()
    }

    fn default_jsonapi_path() -> String {
        "/jsonapi.php/v1".to_string()
    }

    fn default_max_concurrent_downloads() -> usize {
        3
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigFile {
    #[serde(default = "default_profile_name")]
    pub default_profile: String,
    #[serde(default)]
    pub profiles: HashMap<String, ConfigProfile>,
}

impl Default for ConfigFile {
    fn default() -> Self {
        let mut profiles = HashMap::new();
        profiles.insert(DEFAULT_PROFILE_NAME.to_string(), ConfigProfile::default());
        Self {
            default_profile: DEFAULT_PROFILE_NAME.to_string(),
            profiles,
        }
    }
}

fn default_profile_name() -> String {
    DEFAULT_PROFILE_NAME.to_string()
}

impl ConfigFile {
    pub fn load_or_default(path: &Path) -> Result<Self> {
        match fs::read_to_string(path) {
            Ok(contents) => {
                let file: Self =
                    toml::from_str(&contents).context("Failed to parse config.toml")?;
                Ok(file)
            }
            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
            Err(err) => Err(err.into()),
        }
    }

    pub fn save(&self, path: &Path) -> Result<()> {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }

        let contents = toml::to_string_pretty(self)?;

        let mut file = OpenOptions::new()
            .create(true)
            .write(true)
            .truncate(true)
            .open(path)?;
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mut perms = file.metadata()?.permissions();
            perms.set_mode(0o600);
            file.set_permissions(perms)?;
        }
        file.write_all(contents.as_bytes())?;
        Ok(())
    }
}
