use std::borrow::Cow;
use std::error::Error;
use std::fmt::{Display, Formatter, Result as FmtResult};

use super::source::{ConfigFilePath, EnvName};

/// Dotted config field path
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ConfigFieldPath(Cow<'static, str>);

impl ConfigFieldPath {
    #[doc(hidden)]
    pub const fn from_static(value: &'static str) -> Self {
        Self(Cow::Borrowed(value))
    }

    #[doc(hidden)]
    pub fn from_string(value: String) -> Self {
        Self(Cow::Owned(value))
    }

    /// Text value
    pub fn as_str(&self) -> &str {
        self.0.as_ref()
    }
}

impl Display for ConfigFieldPath {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        f.write_str(self.as_str())
    }
}

/// Config failure reason
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ConfigReason(Cow<'static, str>);

impl ConfigReason {
    #[doc(hidden)]
    pub const fn from_static(value: &'static str) -> Self {
        Self(Cow::Borrowed(value))
    }

    #[doc(hidden)]
    pub fn from_string(value: String) -> Self {
        Self(Cow::Owned(value))
    }

    /// Text value
    pub fn as_str(&self) -> &str {
        self.0.as_ref()
    }
}

impl Display for ConfigReason {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        f.write_str(self.as_str())
    }
}

/// Config fix suggestion
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ConfigSuggestion(Cow<'static, str>);

impl ConfigSuggestion {
    #[doc(hidden)]
    pub const fn from_static(value: &'static str) -> Self {
        Self(Cow::Borrowed(value))
    }

    #[doc(hidden)]
    pub fn from_string(value: String) -> Self {
        Self(Cow::Owned(value))
    }

    /// Text value
    pub fn as_str(&self) -> &str {
        self.0.as_ref()
    }
}

impl Display for ConfigSuggestion {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        f.write_str(self.as_str())
    }
}

/// Config loading failure
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ConfigError {
    MissingEnv {
        name: EnvName,
        field: ConfigFieldPath,
        suggestion: ConfigSuggestion,
    },
    InvalidEnv {
        name: EnvName,
        field: ConfigFieldPath,
        reason: ConfigReason,
        suggestion: ConfigSuggestion,
    },
    MissingFileValue {
        field: ConfigFieldPath,
        suggestion: ConfigSuggestion,
    },
    InvalidFileValue {
        field: ConfigFieldPath,
        reason: ConfigReason,
        suggestion: ConfigSuggestion,
    },
    InvalidFile {
        path: ConfigFilePath,
        reason: ConfigReason,
        suggestion: ConfigSuggestion,
    },
    UnknownFileValue {
        field: ConfigFieldPath,
        suggestion: ConfigSuggestion,
    },
    Dotenv {
        path: Option<ConfigFilePath>,
        reason: ConfigReason,
        suggestion: ConfigSuggestion,
    },
    NoSource {
        field: ConfigFieldPath,
        suggestion: ConfigSuggestion,
    },
}

impl ConfigError {
    #[doc(hidden)]
    pub fn missing_env(name: EnvName, field: ConfigFieldPath) -> Self {
        Self::MissingEnv {
            suggestion: ConfigSuggestion::from_string(format!("set {name}=... before startup")),
            name,
            field,
        }
    }

    #[doc(hidden)]
    pub fn invalid_env(name: EnvName, field: ConfigFieldPath, reason: String) -> Self {
        Self::InvalidEnv {
            name,
            field,
            reason: ConfigReason::from_string(reason),
            suggestion: ConfigSuggestion::from_static("fix the env var value or the field type"),
        }
    }

    #[doc(hidden)]
    pub fn missing_file_value(field: ConfigFieldPath) -> Self {
        Self::MissingFileValue {
            field,
            suggestion: ConfigSuggestion::from_static(
                "add the field to config.toml or declare a default",
            ),
        }
    }

    #[doc(hidden)]
    pub fn invalid_file_value(field: ConfigFieldPath, reason: String) -> Self {
        Self::InvalidFileValue {
            field,
            reason: ConfigReason::from_string(reason),
            suggestion: ConfigSuggestion::from_static("fix the TOML value or the field type"),
        }
    }

    #[doc(hidden)]
    pub fn invalid_file_value_with_suggestion(
        field: ConfigFieldPath,
        reason: String,
        suggestion: String,
    ) -> Self {
        Self::InvalidFileValue {
            field,
            reason: ConfigReason::from_string(reason),
            suggestion: ConfigSuggestion::from_string(suggestion),
        }
    }

    #[doc(hidden)]
    pub fn invalid_file(path: ConfigFilePath, reason: String) -> Self {
        Self::InvalidFile {
            path,
            reason: ConfigReason::from_string(reason),
            suggestion: ConfigSuggestion::from_static("fix the config file or choose another path"),
        }
    }

    #[doc(hidden)]
    pub fn unknown_file_value(field: ConfigFieldPath) -> Self {
        Self::UnknownFileValue {
            field,
            suggestion: ConfigSuggestion::from_static(
                "remove the field or declare it in the config struct",
            ),
        }
    }

    #[doc(hidden)]
    pub fn dotenv(path: Option<ConfigFilePath>, reason: String) -> Self {
        Self::Dotenv {
            path,
            reason: ConfigReason::from_string(reason),
            suggestion: ConfigSuggestion::from_static(
                "fix the .env syntax or disable dotenv loading",
            ),
        }
    }

    #[doc(hidden)]
    pub fn no_source(field: ConfigFieldPath) -> Self {
        Self::NoSource {
            field,
            suggestion: ConfigSuggestion::from_static("mark the field with #[file] or #[env]"),
        }
    }

    #[cfg(feature = "errors")]
    fn code_value(&self) -> &'static str {
        match self {
            Self::MissingEnv { .. } => "config::missing_env",
            Self::InvalidEnv { .. } => "config::invalid_env",
            Self::MissingFileValue { .. } => "config::missing_file_value",
            Self::InvalidFileValue { .. } => "config::invalid_file_value",
            Self::InvalidFile { .. } => "config::invalid_file",
            Self::UnknownFileValue { .. } => "config::unknown_file_value",
            Self::Dotenv { .. } => "config::dotenv",
            Self::NoSource { .. } => "config::no_source",
        }
    }

    #[cfg(feature = "errors")]
    fn suggestion(&self) -> &str {
        match self {
            Self::MissingEnv { suggestion, .. }
            | Self::InvalidEnv { suggestion, .. }
            | Self::MissingFileValue { suggestion, .. }
            | Self::InvalidFileValue { suggestion, .. }
            | Self::InvalidFile { suggestion, .. }
            | Self::UnknownFileValue { suggestion, .. }
            | Self::Dotenv { suggestion, .. }
            | Self::NoSource { suggestion, .. } => suggestion.as_str(),
        }
    }
}

impl Display for ConfigError {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        match self {
            Self::MissingEnv {
                name,
                field,
                suggestion,
            } => {
                write!(
                    f,
                    "missing config env var {name}\nfield: {field}\nplease try: {suggestion}"
                )
            }
            Self::InvalidEnv {
                name,
                field,
                reason,
                suggestion,
            } => write!(
                f,
                "invalid config env var {name}\nfield: {field}\nreason: {reason}\nplease try: {suggestion}"
            ),
            Self::MissingFileValue { field, suggestion } => {
                write!(
                    f,
                    "missing config file value\nfield: {field}\nplease try: {suggestion}"
                )
            }
            Self::InvalidFileValue {
                field,
                reason,
                suggestion,
            } => write!(
                f,
                "invalid config file value\nfield: {field}\nreason: {reason}\nplease try: {suggestion}"
            ),
            Self::InvalidFile {
                path,
                reason,
                suggestion,
            } => write!(
                f,
                "invalid config file {}\nreason: {reason}\nplease try: {suggestion}",
                path.display()
            ),
            Self::UnknownFileValue { field, suggestion } => {
                write!(
                    f,
                    "unknown config file value\nfield: {field}\nplease try: {suggestion}"
                )
            }
            Self::Dotenv {
                path,
                reason,
                suggestion,
            } => write_dotenv(f, path, reason, suggestion),
            Self::NoSource { field, suggestion } => {
                write!(
                    f,
                    "config field has no source\nfield: {field}\nplease try: {suggestion}"
                )
            }
        }
    }
}

impl Error for ConfigError {}

#[cfg(feature = "errors")]
impl miette::Diagnostic for ConfigError {
    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
        Some(Box::new(self.code_value()))
    }

    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
        Some(Box::new(self.suggestion()))
    }
}

fn write_dotenv(
    f: &mut Formatter<'_>,
    path: &Option<ConfigFilePath>,
    reason: &ConfigReason,
    suggestion: &ConfigSuggestion,
) -> FmtResult {
    match path {
        Some(path) => write!(
            f,
            "invalid dotenv file {}\nreason: {reason}\nplease try: {suggestion}",
            path.display()
        ),
        None => write!(
            f,
            "invalid dotenv file\nreason: {reason}\nplease try: {suggestion}"
        ),
    }
}
