use std::collections::BTreeSet;
use std::fmt::Display;
use std::str::FromStr;

use serde::de::DeserializeOwned;
use toml::Value;

use super::error::{ConfigError, ConfigFieldPath};
use super::schema::{ConfigSchema, ConfigSchemaField, ConfigValueSource};
use super::source::{ConfigFilePath, EnvName, EnvPrefix, Sources};

/// Root config loading options
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RootSpec {
    pub file_path: &'static str,
    pub file_env: Option<&'static str>,
    pub env_prefix: Option<&'static str>,
    pub dotenv: bool,
}

/// Inherited source for nested config sections
#[doc(hidden)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ConfigSourceSpec {
    None,
    File,
    Env { prefix: Option<EnvPrefix> },
}

impl ConfigSourceSpec {
    pub const fn env(prefix: Option<EnvPrefix>) -> Self {
        Self::Env { prefix }
    }
}

/// Loaded config inputs
#[doc(hidden)]
#[derive(Clone, Debug)]
pub struct LoadContext {
    sources: Sources,
    spec: RootSpec,
    document: Value,
}

impl LoadContext {
    pub fn load(sources: Sources, spec: RootSpec) -> Result<Self, ConfigError> {
        load_dotenv(&sources, spec)?;
        let file_path = resolve_file_path(&sources, spec);
        let document = load_document(&sources, &file_path)?;
        Ok(Self {
            sources,
            spec,
            document,
        })
    }

    pub fn reject_unknown_file_values(&self, schema: &ConfigSchema) -> Result<(), ConfigError> {
        let expected = expected_file_paths(schema);
        let mut actual = Vec::new();
        flatten_file_paths(&self.document, String::new(), &mut actual);
        match actual.into_iter().find(|path| !expected.contains(path)) {
            Some(path) => Err(ConfigError::unknown_file_value(
                ConfigFieldPath::from_string(path),
            )),
            None => Ok(()),
        }
    }

    pub fn file_required<T>(&self, path: &str, default: Option<&str>) -> Result<T, ConfigError>
    where
        T: DeserializeOwned + FromStr,
        T::Err: Display,
    {
        match lookup_value(&self.document, path) {
            Some(value) => deserialize_file(path, value),
            None => self.parse_default_or_missing(path, default),
        }
    }

    pub fn file_optional<T>(
        &self,
        path: &str,
        default: Option<&str>,
    ) -> Result<Option<T>, ConfigError>
    where
        T: DeserializeOwned + FromStr,
        T::Err: Display,
    {
        match lookup_value(&self.document, path) {
            Some(value) => deserialize_file(path, value).map(Some),
            None => self.parse_optional_default(path, default),
        }
    }

    #[doc(hidden)]
    pub fn file_has_value(&self, path: &str) -> bool {
        lookup_value(&self.document, path).is_some()
    }

    pub fn env_required<T>(
        &self,
        path: &str,
        exact: Option<&str>,
        prefix: Option<&str>,
        default: Option<&str>,
    ) -> Result<T, ConfigError>
    where
        T: FromStr,
        T::Err: Display,
    {
        let name = self.env_name(path, exact, prefix);
        match self.read_env(&name) {
            Some(value) => parse_env(path, name, &value),
            None => self.parse_env_default_or_missing(path, name, default),
        }
    }

    pub fn env_optional<T>(
        &self,
        path: &str,
        exact: Option<&str>,
        prefix: Option<&str>,
        default: Option<&str>,
    ) -> Result<Option<T>, ConfigError>
    where
        T: FromStr,
        T::Err: Display,
    {
        let name = self.env_name(path, exact, prefix);
        match self.read_env(&name) {
            Some(value) => parse_env(path, name, &value).map(Some),
            None => self.parse_optional_default(path, default),
        }
    }

    pub fn env_name(&self, path: &str, exact: Option<&str>, prefix: Option<&str>) -> EnvName {
        env_name_for(path, exact, prefix, self.spec.env_prefix)
    }

    fn read_env(&self, name: &EnvName) -> Option<String> {
        self.sources
            .env_value(name)
            .or_else(|| read_process_env(&self.sources, name.as_str()))
    }

    fn parse_default_or_missing<T>(
        &self,
        path: &str,
        default: Option<&str>,
    ) -> Result<T, ConfigError>
    where
        T: FromStr,
        T::Err: Display,
    {
        match default {
            Some(value) => parse_file_default(path, value),
            None => Err(ConfigError::missing_file_value(
                ConfigFieldPath::from_string(path.into()),
            )),
        }
    }

    fn parse_env_default_or_missing<T>(
        &self,
        path: &str,
        name: EnvName,
        default: Option<&str>,
    ) -> Result<T, ConfigError>
    where
        T: FromStr,
        T::Err: Display,
    {
        match default {
            Some(value) => parse_env(path, name, value),
            None => Err(ConfigError::missing_env(
                name,
                ConfigFieldPath::from_string(path.into()),
            )),
        }
    }

    fn parse_optional_default<T>(
        &self,
        path: &str,
        default: Option<&str>,
    ) -> Result<Option<T>, ConfigError>
    where
        T: FromStr,
        T::Err: Display,
    {
        match default {
            Some(value) => parse_file_default(path, value).map(Some),
            None => Ok(None),
        }
    }
}

#[doc(hidden)]
pub fn env_name_for(
    path: &str,
    exact: Option<&str>,
    prefix: Option<&str>,
    root_prefix: Option<&str>,
) -> EnvName {
    match exact {
        Some(name) => EnvName::from_string(name.into()),
        None => implicit_env_name(path, prefix, root_prefix),
    }
}

fn load_dotenv(sources: &Sources, spec: RootSpec) -> Result<(), ConfigError> {
    if !spec.dotenv || !sources.reads_process_env() {
        return Ok(());
    }
    match dotenvy::dotenv() {
        Ok(_) => Ok(()),
        Err(dotenvy::Error::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
        Err(error) => Err(ConfigError::dotenv(None, error.to_string())),
    }
}

fn resolve_file_path(sources: &Sources, spec: RootSpec) -> ConfigFilePath {
    if let Some(path) = sources.file_path() {
        return path.clone();
    }
    if let Some(name) = spec.file_env.and_then(|name| bootstrap_env(sources, name)) {
        return ConfigFilePath::new(name);
    }
    ConfigFilePath::new(spec.file_path)
}

fn bootstrap_env(sources: &Sources, name: &'static str) -> Option<String> {
    let name = EnvName::from_static(name);
    sources
        .env_value(&name)
        .or_else(|| read_process_env(sources, name.as_str()))
}

fn load_document(sources: &Sources, path: &ConfigFilePath) -> Result<Value, ConfigError> {
    match sources.toml() {
        Some(text) => parse_document(path, text.as_str()),
        None if sources.reads_file_system() => read_document(path),
        None => Ok(Value::Table(Default::default())),
    }
}

fn read_document(path: &ConfigFilePath) -> Result<Value, ConfigError> {
    match std::fs::read_to_string(path.as_path()) {
        Ok(text) => parse_document(path, &text),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
            Ok(Value::Table(Default::default()))
        }
        Err(error) => Err(ConfigError::invalid_file(path.clone(), error.to_string())),
    }
}

fn parse_document(path: &ConfigFilePath, text: &str) -> Result<Value, ConfigError> {
    toml::from_str(text).map_err(|error| ConfigError::invalid_file(path.clone(), error.to_string()))
}

fn read_process_env(sources: &Sources, name: &str) -> Option<String> {
    sources
        .reads_process_env()
        .then(|| std::env::var(name).ok())
        .flatten()
}

fn lookup_value<'a>(document: &'a Value, path: &str) -> Option<&'a Value> {
    let mut value = document;
    for part in path.split('.') {
        value = value.get(part)?;
    }
    Some(value)
}

fn deserialize_file<T>(path: &str, value: &Value) -> Result<T, ConfigError>
where
    T: DeserializeOwned,
{
    value.clone().try_into().map_err(|error: toml::de::Error| {
        ConfigError::invalid_file_value(
            ConfigFieldPath::from_string(path.into()),
            error.to_string(),
        )
    })
}

fn parse_env<T>(path: &str, name: EnvName, value: &str) -> Result<T, ConfigError>
where
    T: FromStr,
    T::Err: Display,
{
    value.parse().map_err(|error: T::Err| {
        ConfigError::invalid_env(
            name,
            ConfigFieldPath::from_string(path.into()),
            error.to_string(),
        )
    })
}

fn parse_file_default<T>(path: &str, value: &str) -> Result<T, ConfigError>
where
    T: FromStr,
    T::Err: Display,
{
    value.parse().map_err(|error: T::Err| {
        ConfigError::invalid_file_value(
            ConfigFieldPath::from_string(path.into()),
            format!("invalid default: {error}"),
        )
    })
}

fn expected_file_paths(schema: &ConfigSchema) -> BTreeSet<String> {
    schema
        .fields()
        .iter()
        .filter_map(expected_file_path)
        .map(str::to_owned)
        .collect()
}

fn expected_file_path(field: &ConfigSchemaField) -> Option<&str> {
    match field.source() {
        ConfigValueSource::File => Some(field.path().as_str()),
        ConfigValueSource::Env(_) => None,
    }
}

fn flatten_file_paths(value: &Value, prefix: String, paths: &mut Vec<String>) {
    match value {
        Value::Table(table) => flatten_table(table, prefix, paths),
        _ if !prefix.is_empty() => paths.push(prefix),
        _ => {}
    }
}

fn flatten_table(table: &toml::map::Map<String, Value>, prefix: String, paths: &mut Vec<String>) {
    for (key, value) in table {
        let next = join_path(&prefix, key);
        flatten_file_paths(value, next, paths);
    }
}

fn join_path(prefix: &str, key: &str) -> String {
    match prefix.is_empty() {
        true => key.into(),
        false => format!("{prefix}.{key}"),
    }
}

fn implicit_env_name(path: &str, prefix: Option<&str>, root_prefix: Option<&str>) -> EnvName {
    let leaf = path.rsplit('.').next().unwrap_or(path);
    match (prefix, root_prefix) {
        (Some(prefix), _) => EnvName::from_string(format!("{}_{}", prefix, screaming(leaf))),
        (None, Some(prefix)) => EnvName::from_string(format!("{}_{}", prefix, screaming(path))),
        (None, None) => EnvName::from_string(screaming(path)),
    }
}

#[doc(hidden)]
pub fn field_path(prefix: &str, field: &str) -> String {
    join_path(prefix, field)
}

#[doc(hidden)]
pub fn env_prefix_from_path(root_prefix: Option<&str>, path: &str) -> EnvPrefix {
    match root_prefix {
        Some(prefix) => EnvPrefix::from_string(format!("{}_{}", prefix, screaming(path))),
        None => EnvPrefix::from_string(screaming(path)),
    }
}

fn screaming(value: &str) -> String {
    value
        .chars()
        .map(|ch| {
            if ch == '.' {
                '_'
            } else {
                ch.to_ascii_uppercase()
            }
        })
        .collect()
}
