use serde::Deserialize;
use std::fmt::{Display, Formatter};
use std::net::IpAddr;
use thiserror::Error;

#[derive(Deserialize, Debug)]
#[non_exhaustive]
pub struct MagnetarNetworking {
    pub host: String,
    pub port: u16,
    pub bind_addr: IpAddr,
    pub protocol: MagnetarNetworkingProtocol,
    pub media_proxy: Option<String>,
    pub proxy_remote_files: bool,
}

#[derive(Deserialize, Debug, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum MagnetarNetworkingProtocol {
    Http,
    Https,
}

impl AsRef<str> for MagnetarNetworkingProtocol {
    fn as_ref(&self) -> &str {
        match *self {
            MagnetarNetworkingProtocol::Http => "http",
            MagnetarNetworkingProtocol::Https => "https",
        }
    }
}

impl Display for MagnetarNetworkingProtocol {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_ref())
    }
}

fn env_host() -> String {
    std::env::var("MAG_C_HOST")
        .expect("MAG_C_HOST or \"networking.host\" in the default configuration must be set")
}

fn env_bind_addr() -> IpAddr {
    std::env::var("MAG_C_BIND_ADDR")
        .unwrap_or_else(|_| "::".to_owned())
        .parse()
        .map_err(|e| format!("Failed to parse \"MAG_C_BIND_ADDR\": {e}"))
        .unwrap()
}

fn env_port() -> u16 {
    std::env::var("MAG_C_PORT")
        .unwrap_or_else(|_| "4939".to_owned())
        .parse()
        .expect("MAG_C_PORT must be a valid port number")
}

fn env_protocol() -> MagnetarNetworkingProtocol {
    match std::env::var("MAG_C_PROTOCOL")
        .unwrap_or_else(|_| "https".to_owned())
        .to_lowercase()
        .as_str()
    {
        "http" => MagnetarNetworkingProtocol::Http,
        "https" => MagnetarNetworkingProtocol::Https,
        _ => panic!("MAG_C_PROTOCOL must be a valid protocol"),
    }
}

fn env_media_proxy() -> Option<String> {
    std::env::var("MAG_C_MEDIA_PROXY")
        .ok()
        .filter(String::is_empty)
}

fn env_proxy_remote_files() -> bool {
    std::env::var("MAG_C_PROXY_REMOTE_FILES")
        .unwrap_or_else(|_| "false".to_string())
        .parse()
        .expect("MAG_C_PROXY_REMOTE_FILES must be a boolean")
}

impl Default for MagnetarNetworking {
    fn default() -> Self {
        MagnetarNetworking {
            host: env_host(),
            bind_addr: env_bind_addr(),
            port: env_port(),
            protocol: env_protocol(),
            media_proxy: env_media_proxy(),
            proxy_remote_files: env_proxy_remote_files(),
        }
    }
}

#[derive(Deserialize, Debug)]
#[non_exhaustive]
pub struct MagnetarCalckeyFrontendConfig {
    pub bind_addr: IpAddr,
    pub port: u16,
}

fn env_fe_bind_addr() -> IpAddr {
    std::env::var("MAG_C_CK_FE_BIND_ADDR")
        .unwrap_or_else(|_| "::".to_owned())
        .parse()
        .map_err(|e| format!("Failed to parse \"MAG_C_CK_FE_BIND_ADDR\": {e}"))
        .unwrap()
}

fn env_fe_port() -> u16 {
    std::env::var("MAG_C_CK_FE_PORT")
        .unwrap_or_else(|_| "4938".to_owned())
        .parse()
        .expect("MAG_C_CK_FE_PORT must be a valid port number")
}

impl Default for MagnetarCalckeyFrontendConfig {
    fn default() -> Self {
        MagnetarCalckeyFrontendConfig {
            bind_addr: env_fe_bind_addr(),
            port: env_fe_port(),
        }
    }
}

#[derive(Deserialize, Debug)]
#[non_exhaustive]
pub struct MagnetarBranding {
    pub name: String,
    pub version: String,
    pub homepage: String,
    pub repository: String,
}

fn env_brand_name() -> String {
    std::env::var("MAG_C_BR_NAME").unwrap_or_else(|_| "magnetar".to_owned())
}

fn env_brand_version() -> String {
    std::env::var("MAG_C_BR_VERSION").unwrap_or_else(|_| env!("CARGO_PKG_VERSION").to_owned())
}

fn env_brand_homepage() -> String {
    std::env::var("MAG_C_BR_HOMEPAGE")
        .unwrap_or_else(|_| "https://git.astolfo.cool/natty/magnetar".to_owned())
}

fn env_brand_repository() -> String {
    std::env::var("MAG_C_BR_REPOSITORY")
        .unwrap_or_else(|_| "https://git.astolfo.cool/natty/magnetar".to_owned())
}

impl Default for MagnetarBranding {
    fn default() -> Self {
        MagnetarBranding {
            name: env_brand_name(),
            version: env_brand_version(),
            homepage: env_brand_homepage(),
            repository: env_brand_repository(),
        }
    }
}

#[derive(Deserialize, Debug)]
#[non_exhaustive]
pub struct MagnetarData {
    pub database_url: String,
    pub redis_url: String,
}

fn env_database_url() -> String {
    std::env::var("MAG_C_DATABASE_URL")
        .or_else(|_| std::env::var("DATABASE_URL"))
        .expect("MAG_C_DATABASE_URL, DATABASE_URL or \"data.database_url\" in the default configuration must be set")
}

fn env_redis_url() -> String {
    std::env::var("MAG_C_REDIS_URL")
        .expect("MAG_C_REDIS_URL or \"data.redis_url\" in the default configuration must be set")
}

impl Default for MagnetarData {
    fn default() -> Self {
        MagnetarData {
            database_url: env_database_url(),
            redis_url: env_redis_url(),
        }
    }
}

#[derive(Deserialize, Debug, Default)]
#[non_exhaustive]
pub struct MagnetarConfig {
    #[serde(default)]
    pub networking: MagnetarNetworking,
    #[serde(default)]
    pub branding: MagnetarBranding,
    #[serde(default)]
    pub calckey_frontend: MagnetarCalckeyFrontendConfig,
    #[serde(default)]
    pub data: MagnetarData,
}

#[derive(Debug, Error)]
pub enum MagnetarConfigError {
    #[error("Failed to load configuration: {0}")]
    IoError(#[from] std::io::Error),
    #[error("Failed to parse configuration: {0}")]
    DeserializeError(#[from] toml::de::Error),
    #[error("Configuration error: Not a valid hostname")]
    ConfigHostnameError(#[from] idna::Errors),
}

pub fn load_config() -> Result<MagnetarConfig, MagnetarConfigError> {
    let path =
        std::env::var("MAG_CONFIG_PATH").unwrap_or_else(|_| "config/default.toml".to_owned());

    let str_cfg = std::fs::read_to_string(path)?;

    let mut config: MagnetarConfig = toml::from_str(&str_cfg)?;

    // Validate the host
    idna::domain_to_unicode(&config.networking.host).1?;

    if config
        .networking
        .media_proxy
        .as_deref()
        .is_some_and(str::is_empty)
    {
        config.networking.media_proxy = None;
    }

    Ok(config)
}
