use super::{
    directive::{self, Directive},
    EnvFilter, FromEnvError,
};
use crate::sync::RwLock;
use std::env;
use thread_local::ThreadLocal;
use tracing::level_filters::STATIC_MAX_LEVEL;

/// A [builder] for constructing new [`EnvFilter`]s.
///
/// [builder]: https://rust-unofficial.github.io/patterns/patterns/creational/builder.html
#[derive(Debug, Clone)]
#[must_use]
pub struct Builder {
    regex: bool,
    env: Option<String>,
    default_directive: Option<Directive>,
}

impl Builder {
    /// Sets whether span field values can be matched with regular expressions.
    ///
    /// If this is `true`, field filter directives will be interpreted as
    /// regular expressions if they are not able to be interpreted as a `bool`,
    /// `i64`, `u64`, or `f64` literal. If this is `false,` those field values
    /// will be interpreted as literal [`std::fmt::Debug`] output instead.
    ///
    /// By default, regular expressions are enabled.
    ///
    /// **Note**: when [`EnvFilter`]s are constructed from untrusted inputs,
    /// disabling regular expressions is strongly encouraged.
    pub fn with_regex(self, regex: bool) -> Self {
        Self { regex, ..self }
    }

    /// Sets a default [filtering directive] that will be added to the filter if
    /// the parsed string or environment variable contains no filter directives.
    ///
    /// By default, there is no default directive.
    ///
    /// # Examples
    ///
    /// If [`parse`], [`parse_lossy`], [`from_env`], or [`from_env_lossy`] are
    /// called with an empty string or environment variable, the default
    /// directive is used instead:
    ///
    /// ```rust
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// use tracing_subscriber::filter::{EnvFilter, LevelFilter};
    ///
    /// let filter = EnvFilter::builder()
    ///     .with_default_directive(LevelFilter::INFO.into())
    ///     .parse("")?;
    ///
    /// assert_eq!(format!("{}", filter), "info");
    /// # Ok(()) }
    /// ```
    ///
    /// Note that the `lossy` variants ([`parse_lossy`] and [`from_env_lossy`])
    /// will ignore any invalid directives. If all directives in a filter
    /// string or environment variable are invalid, those methods will also use
    /// the default directive:
    ///
    /// ```rust
    /// use tracing_subscriber::filter::{EnvFilter, LevelFilter};
    ///
    /// let filter = EnvFilter::builder()
    ///     .with_default_directive(LevelFilter::INFO.into())
    ///     .parse_lossy("some_target=fake level,foo::bar=lolwut");
    ///
    /// assert_eq!(format!("{}", filter), "info");
    /// ```
    ///
    ///
    /// If the string or environment variable contains valid filtering
    /// directives, the default directive is not used:
    ///
    /// ```rust
    /// use tracing_subscriber::filter::{EnvFilter, LevelFilter};
    ///
    /// let filter = EnvFilter::builder()
    ///     .with_default_directive(LevelFilter::INFO.into())
    ///     .parse_lossy("foo=trace");
    ///
    /// // The default directive is *not* used:
    /// assert_eq!(format!("{}", filter), "foo=trace");
    /// ```
    ///
    /// Parsing a more complex default directive from a string:
    ///
    /// ```rust
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// use tracing_subscriber::filter::{EnvFilter, LevelFilter};
    ///
    /// let default = "myapp=debug".parse()
    ///     .expect("hard-coded default directive should be valid");
    ///
    /// let filter = EnvFilter::builder()
    ///     .with_default_directive(default)
    ///     .parse("")?;
    ///
    /// assert_eq!(format!("{}", filter), "myapp=debug");
    /// # Ok(()) }
    /// ```
    ///
    /// [`parse_lossy`]: Self::parse_lossy
    /// [`from_env_lossy`]: Self::from_env_lossy
    /// [`parse`]: Self::parse
    /// [`from_env`]: Self::from_env
    pub fn with_default_directive(self, default_directive: Directive) -> Self {
        Self {
            default_directive: Some(default_directive),
            ..self
        }
    }

    /// Sets the name of the environment variable used by the [`from_env`],
    /// [`from_env_lossy`], and [`try_from_env`] methods.
    ///
    /// By default, this is the value of [`EnvFilter::DEFAULT_ENV`]
    /// (`RUST_LOG`).
    ///
    /// [`from_env`]: Self::from_env
    /// [`from_env_lossy`]: Self::from_env_lossy
    /// [`try_from_env`]: Self::try_from_env
    pub fn with_env_var(self, var: impl ToString) -> Self {
        Self {
            env: Some(var.to_string()),
            ..self
        }
    }

    /// Returns a new [`EnvFilter`] from the directives in the given string,
    /// *ignoring* any that are invalid.
    pub fn parse_lossy<S: AsRef<str>>(&self, dirs: S) -> EnvFilter {
        let directives = dirs
            .as_ref()
            .split(',')
            .filter(|s| !s.is_empty())
            .filter_map(|s| match Directive::parse(s, self.regex) {
                Ok(d) => Some(d),
                Err(err) => {
                    eprintln!("ignoring `{}`: {}", s, err);
                    None
                }
            });
        self.from_directives(directives)
    }

    /// Returns a new [`EnvFilter`] from the directives in the given string,
    /// or an error if any are invalid.
    pub fn parse<S: AsRef<str>>(&self, dirs: S) -> Result<EnvFilter, directive::ParseError> {
        let dirs = dirs.as_ref();
        if dirs.is_empty() {
            return Ok(self.from_directives(std::iter::empty()));
        }
        let directives = dirs
            .split(',')
            .filter(|s| !s.is_empty())
            .map(|s| Directive::parse(s, self.regex))
            .collect::<Result<Vec<_>, _>>()?;
        Ok(self.from_directives(directives))
    }

    /// Returns a new [`EnvFilter`] from the directives in the configured
    /// environment variable, ignoring any directives that are invalid.
    pub fn from_env_lossy(&self) -> EnvFilter {
        let var = env::var(self.env_var_name()).unwrap_or_default();
        self.parse_lossy(var)
    }

    /// Returns a new [`EnvFilter`] from the directives in the in the configured
    /// environment variable, or an error if the environment variable is not set
    /// or contains invalid directives.
    pub fn from_env(&self) -> Result<EnvFilter, FromEnvError> {
        let var = env::var(self.env_var_name()).unwrap_or_default();
        self.parse(var).map_err(Into::into)
    }

    /// Returns a new [`EnvFilter`] from the directives in the in the configured
    /// environment variable, or an error if the environment variable is not set
    /// or contains invalid directives.
    pub fn try_from_env(&self) -> Result<EnvFilter, FromEnvError> {
        let var = env::var(self.env_var_name())?;
        self.parse(var).map_err(Into::into)
    }

    // TODO(eliza): consider making this a public API?
    // Clippy doesn't love this naming, because it suggests that `from_` methods
    // should not take a `Self`...but in this case, it's the `EnvFilter` that is
    // being constructed "from" the directives, rather than the builder itself.
    #[allow(clippy::wrong_self_convention)]
    pub(super) fn from_directives(
        &self,
        directives: impl IntoIterator<Item = Directive>,
    ) -> EnvFilter {
        use tracing::Level;

        let mut directives: Vec<_> = directives.into_iter().collect();
        let mut disabled = Vec::new();
        for directive in &mut directives {
            if directive.level > STATIC_MAX_LEVEL {
                disabled.push(directive.clone());
            }
            if !self.regex {
                directive.deregexify();
            }
        }

        if !disabled.is_empty() {
            #[cfg(feature = "nu_ansi_term")]
            use nu_ansi_term::{Color, Style};
            // NOTE: We can't use a configured `MakeWriter` because the EnvFilter
            // has no knowledge of any underlying subscriber or collector, which
            // may or may not use a `MakeWriter`.
            let warn = |msg: &str| {
                #[cfg(not(feature = "nu_ansi_term"))]
                let msg = format!("warning: {}", msg);
                #[cfg(feature = "nu_ansi_term")]
                let msg = {
                    let bold = Style::new().bold();
                    let mut warning = Color::Yellow.paint("warning");
                    warning.style_ref_mut().is_bold = true;
                    format!("{}{} {}", warning, bold.paint(":"), bold.paint(msg))
                };
                eprintln!("{}", msg);
            };
            let ctx_prefixed = |prefix: &str, msg: &str| {
                #[cfg(not(feature = "nu_ansi_term"))]
                let msg = format!("{} {}", prefix, msg);
                #[cfg(feature = "nu_ansi_term")]
                let msg = {
                    let mut equal = Color::Fixed(21).paint("="); // dark blue
                    equal.style_ref_mut().is_bold = true;
                    format!(" {} {} {}", equal, Style::new().bold().paint(prefix), msg)
                };
                eprintln!("{}", msg);
            };
            let ctx_help = |msg| ctx_prefixed("help:", msg);
            let ctx_note = |msg| ctx_prefixed("note:", msg);
            let ctx = |msg: &str| {
                #[cfg(not(feature = "nu_ansi_term"))]
                let msg = format!("note: {}", msg);
                #[cfg(feature = "nu_ansi_term")]
                let msg = {
                    let mut pipe = Color::Fixed(21).paint("|");
                    pipe.style_ref_mut().is_bold = true;
                    format!(" {} {}", pipe, msg)
                };
                eprintln!("{}", msg);
            };
            warn("some trace filter directives would enable traces that are disabled statically");
            for directive in disabled {
                let target = if let Some(target) = &directive.target {
                    format!("the `{}` target", target)
                } else {
                    "all targets".into()
                };
                let level = directive
                    .level
                    .into_level()
                    .expect("=off would not have enabled any filters");
                ctx(&format!(
                    "`{}` would enable the {} level for {}",
                    directive, level, target
                ));
            }
            ctx_note(&format!("the static max level is `{}`", STATIC_MAX_LEVEL));
            let help_msg = || {
                let (feature, filter) = match STATIC_MAX_LEVEL.into_level() {
                    Some(Level::TRACE) => unreachable!(
                        "if the max level is trace, no static filtering features are enabled"
                    ),
                    Some(Level::DEBUG) => ("max_level_debug", Level::TRACE),
                    Some(Level::INFO) => ("max_level_info", Level::DEBUG),
                    Some(Level::WARN) => ("max_level_warn", Level::INFO),
                    Some(Level::ERROR) => ("max_level_error", Level::WARN),
                    None => return ("max_level_off", String::new()),
                };
                (feature, format!("{} ", filter))
            };
            let (feature, earlier_level) = help_msg();
            ctx_help(&format!(
                "to enable {}logging, remove the `{}` feature from the `tracing` crate",
                earlier_level, feature
            ));
        }

        let (dynamics, statics) = Directive::make_tables(directives);
        let has_dynamics = !dynamics.is_empty();

        let mut filter = EnvFilter {
            statics,
            dynamics,
            has_dynamics,
            by_id: RwLock::new(Default::default()),
            by_cs: RwLock::new(Default::default()),
            scope: ThreadLocal::new(),
            regex: self.regex,
        };

        if !has_dynamics && filter.statics.is_empty() {
            if let Some(ref default) = self.default_directive {
                filter = filter.add_directive(default.clone());
            }
        }

        filter
    }

    fn env_var_name(&self) -> &str {
        self.env.as_deref().unwrap_or(EnvFilter::DEFAULT_ENV)
    }
}

impl Default for Builder {
    fn default() -> Self {
        Self {
            regex: true,
            env: None,
            default_directive: None,
        }
    }
}
