use std::cmp::Reverse;
use std::fmt::Display;

use cargo_util_schemas::manifest;
use cargo_util_terminal::report::Level;

use crate::core::{Feature, Features};
use crate::util::GlobalContext;
use crate::util::context::WarningHandling;

#[derive(Clone, Debug)]
pub struct Lint {
    pub name: &'static str,
    pub desc: &'static str,
    pub primary_group: &'static LintGroup,
    /// The minimum supported Rust version for applying this lint
    ///
    /// Note: If the lint is on by default and did not qualify as a hard-warning before the
    /// linting system, then at earliest an MSRV of 1.78 is required as `[lints.cargo]` was a hard
    /// error before then.
    pub msrv: Option<manifest::RustVersion>,
    pub feature_gate: Option<&'static Feature>,
    /// This is a markdown formatted string that will be used when generating
    /// the lint documentation. If docs is `None`, the lint will not be
    /// documented.
    pub docs: Option<&'static str>,
}

impl Lint {
    pub fn level(
        &self,
        pkg_lints: &manifest::TomlToolLints,
        pkg_rust_version: Option<&manifest::RustVersion>,
        unstable_features: &Features,
        gctx: &GlobalContext,
    ) -> LintLevelProduct {
        // We should return `Allow` if a lint is behind a feature, but it is
        // not enabled, that way the lint does not run.
        if self
            .feature_gate
            .is_some_and(|f| !unstable_features.is_enabled(f))
        {
            let level = LintLevel::Allow;
            let source = LintLevelSource::Default;
            return LintLevelProduct { level, source };
        }

        if let (Some(msrv), Some(pkg_rust_version)) = (&self.msrv, pkg_rust_version) {
            let pkg_rust_version = pkg_rust_version.to_partial();
            if !msrv.is_compatible_with(&pkg_rust_version) {
                let level = LintLevel::Allow;
                let source = LintLevelSource::Default;
                return LintLevelProduct { level, source };
            }
        }

        let lint_level_priority =
            level_priority(self.name, self.primary_group.default_level, pkg_lints);

        let group_level_priority = level_priority(
            self.primary_group.name,
            self.primary_group.default_level,
            pkg_lints,
        );

        let default_group = if LintLevel::Warn <= self.primary_group.default_level {
            let lint_level_priority =
                level_priority("default", self.primary_group.default_level, pkg_lints);
            Some(("default", lint_level_priority))
        } else {
            None
        };

        let (_, (level, source, _)) = [
            (self.name, lint_level_priority),
            (self.primary_group.name, group_level_priority),
        ]
        .into_iter()
        .chain(default_group)
        .max_by_key(|(n, (l, s, p))| {
            (
                l == &LintLevel::Forbid,
                *s != LintLevelSource::Default,
                *p,
                Reverse(*n),
            )
        })
        .unwrap();

        let (level, source) = match (level, gctx.warning_handling().ok()) {
            // `Deny` needs to be handled later, at the end of the operation
            (LintLevel::Warn, Some(WarningHandling::Allow)) => {
                (LintLevel::Allow, LintLevelSource::Default)
            }
            _ => (level, source),
        };

        LintLevelProduct { level, source }
    }

    pub fn emitted_source(&self, lint_level: LintLevel, source: LintLevelSource) -> String {
        format!("`cargo::{}` is set to `{lint_level}` {source}", self.name,)
    }
}

pub struct LintLevelProduct {
    pub level: LintLevel,
    pub source: LintLevelSource,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum LintLevel {
    Allow,
    Warn,
    Deny,
    Forbid,
}

impl Display for LintLevel {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            LintLevel::Allow => write!(f, "allow"),
            LintLevel::Warn => write!(f, "warn"),
            LintLevel::Deny => write!(f, "deny"),
            LintLevel::Forbid => write!(f, "forbid"),
        }
    }
}

impl LintLevel {
    pub fn is_warn(&self) -> bool {
        self == &LintLevel::Warn
    }

    pub fn is_error(&self) -> bool {
        self == &LintLevel::Forbid || self == &LintLevel::Deny
    }

    pub fn to_diagnostic_level(self) -> Level<'static> {
        match self {
            LintLevel::Allow => unreachable!("allow does not map to a diagnostic level"),
            LintLevel::Warn => Level::WARNING,
            LintLevel::Deny => Level::ERROR,
            LintLevel::Forbid => Level::ERROR,
        }
    }

    pub fn force(self) -> bool {
        match self {
            Self::Allow => false,
            Self::Warn => true,
            Self::Deny => true,
            Self::Forbid => true,
        }
    }
}

impl From<manifest::TomlLintLevel> for LintLevel {
    fn from(toml_lint_level: manifest::TomlLintLevel) -> LintLevel {
        match toml_lint_level {
            manifest::TomlLintLevel::Allow => LintLevel::Allow,
            manifest::TomlLintLevel::Warn => LintLevel::Warn,
            manifest::TomlLintLevel::Deny => LintLevel::Deny,
            manifest::TomlLintLevel::Forbid => LintLevel::Forbid,
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum LintLevelSource {
    Default,
    Package,
}

impl Display for LintLevelSource {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            LintLevelSource::Default => write!(f, "by default"),
            LintLevelSource::Package => write!(f, "in `[lints]`"),
        }
    }
}

impl LintLevelSource {
    pub(crate) fn is_user_specified(&self) -> bool {
        match self {
            LintLevelSource::Default => false,
            LintLevelSource::Package => true,
        }
    }
}

pub(crate) fn level_priority(
    name: &str,
    default_level: LintLevel,
    pkg_lints: &manifest::TomlToolLints,
) -> (LintLevel, LintLevelSource, i8) {
    if let Some(defined_level) = pkg_lints.get(name) {
        (
            defined_level.level().into(),
            LintLevelSource::Package,
            defined_level.priority(),
        )
    } else {
        (default_level, LintLevelSource::Default, 0)
    }
}

#[derive(Clone, Debug)]
pub struct LintGroup {
    pub name: &'static str,
    pub default_level: LintLevel,
    pub desc: &'static str,
    pub feature_gate: Option<&'static Feature>,
    pub hidden: bool,
}

#[cfg(test)]
mod tests {
    use super::*;

    const STYLE: LintGroup = LintGroup {
        name: "style",
        desc: "code that should be written in a more idiomatic way",
        default_level: LintLevel::Warn,
        feature_gate: None,
        hidden: false,
    };

    fn gctx() -> GlobalContext {
        let cwd = std::env::current_dir().unwrap();
        GlobalContext::new(
            cargo_util_terminal::Shell::new(),
            cwd.clone(),
            home::cargo_home_with_cwd(&cwd).unwrap(),
        )
    }

    fn test_lint(name: &'static str, group: &'static LintGroup) -> Lint {
        Lint {
            name,
            desc: "test lint",
            primary_group: group,
            msrv: None,
            feature_gate: None,
            docs: None,
        }
    }

    #[test]
    fn lint_level_prefers_user_specified_over_default() {
        let lint = test_lint("unused_dependencies", &STYLE);

        let mut pkg_lints = manifest::TomlToolLints::new();
        pkg_lints.insert(
            "unused_dependencies".to_string(),
            manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
        );
        let features = Features::default();

        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
        assert_eq!(level, LintLevel::Deny);
        assert_eq!(source, LintLevelSource::Package);
    }

    #[test]
    fn lint_level_group_overrides_default() {
        let lint = test_lint("non_kebab_case_bins", &STYLE);

        let mut pkg_lints = manifest::TomlToolLints::new();
        pkg_lints.insert(
            "style".to_string(),
            manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
        );
        let features = Features::default();

        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
        assert_eq!(level, LintLevel::Deny);
        assert_eq!(source, LintLevelSource::Package);
    }

    #[test]
    fn default_group_overrides_default() {
        let lint = test_lint("non_kebab_case_bins", &STYLE);

        let mut pkg_lints = manifest::TomlToolLints::new();
        pkg_lints.insert(
            "default".to_string(),
            manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
        );
        let features = Features::default();

        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
        assert_eq!(level, LintLevel::Deny);
        assert_eq!(source, LintLevelSource::Package);
    }

    #[test]
    fn default_before_primary() {
        let lint = test_lint("non_kebab_case_bins", &STYLE);

        let mut pkg_lints = manifest::TomlToolLints::new();
        pkg_lints.insert(
            "default".to_string(),
            manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
        );
        pkg_lints.insert(
            "style".to_string(),
            manifest::TomlLint::Level(manifest::TomlLintLevel::Allow),
        );
        let features = Features::default();

        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
        assert_eq!(level, LintLevel::Deny);
        assert_eq!(source, LintLevelSource::Package);
    }

    #[test]
    fn default_after_primary() {
        let lint = test_lint("non_kebab_case_bins", &STYLE);

        let mut pkg_lints = manifest::TomlToolLints::new();
        pkg_lints.insert(
            "style".to_string(),
            manifest::TomlLint::Level(manifest::TomlLintLevel::Allow),
        );
        pkg_lints.insert(
            "default".to_string(),
            manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
        );
        let features = Features::default();

        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
        assert_eq!(level, LintLevel::Deny);
        assert_eq!(source, LintLevelSource::Package);
    }

    #[test]
    fn default_higher_than_primary() {
        let lint = test_lint("non_kebab_case_bins", &STYLE);

        let mut pkg_lints = manifest::TomlToolLints::new();
        pkg_lints.insert(
            "default".to_string(),
            manifest::TomlLint::Config(manifest::TomlLintConfig {
                level: manifest::TomlLintLevel::Deny,
                priority: 1,
                config: Default::default(),
            }),
        );
        pkg_lints.insert(
            "style".to_string(),
            manifest::TomlLint::Config(manifest::TomlLintConfig {
                level: manifest::TomlLintLevel::Allow,
                priority: -1,
                config: Default::default(),
            }),
        );
        let features = Features::default();

        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
        assert_eq!(level, LintLevel::Deny);
        assert_eq!(source, LintLevelSource::Package);
    }

    #[test]
    fn default_lower_than_primary() {
        let lint = test_lint("non_kebab_case_bins", &STYLE);

        let mut pkg_lints = manifest::TomlToolLints::new();
        pkg_lints.insert(
            "default".to_string(),
            manifest::TomlLint::Config(manifest::TomlLintConfig {
                level: manifest::TomlLintLevel::Deny,
                priority: -1,
                config: Default::default(),
            }),
        );
        pkg_lints.insert(
            "style".to_string(),
            manifest::TomlLint::Config(manifest::TomlLintConfig {
                level: manifest::TomlLintLevel::Allow,
                priority: 1,
                config: Default::default(),
            }),
        );
        let features = Features::default();

        let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
        assert_eq!(level, LintLevel::Allow);
        assert_eq!(source, LintLevelSource::Package);
    }
}
