use crate::diagnostic::did_you_mean;
use core::fmt;
use itertools::Itertools;
use ruff_db::diagnostic::{DiagnosticId, LintName, Severity};
use rustc_hash::FxHashMap;
use std::error::Error;
use std::fmt::Formatter;
use std::hash::Hasher;

#[derive(Debug, Clone)]
pub struct LintMetadata {
    /// The unique identifier for the lint.
    pub name: LintName,

    /// A one-sentence summary of what the lint catches.
    pub summary: &'static str,

    /// An in depth explanation of the lint in markdown. Covers what the lint does, why it's bad and possible fixes.
    pub raw_documentation: &'static str,

    /// The default level of the lint if the user doesn't specify one.
    pub default_level: Level,

    pub status: LintStatus,

    /// The source file in which the lint is declared.
    pub file: &'static str,

    /// The 1-based line number in the source `file` where the lint is declared.
    pub line: u32,
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, get_size2::GetSize)]
#[cfg_attr(
    feature = "serde",
    derive(serde::Serialize, serde::Deserialize),
    serde(rename_all = "kebab-case")
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum Level {
    /// # Ignore
    ///
    /// The lint is disabled and should not run.
    Ignore,

    /// # Warn
    ///
    /// The lint is enabled and diagnostic should have a warning severity.
    Warn,

    /// # Error
    ///
    /// The lint is enabled and diagnostics have an error severity.
    Error,
}

impl Level {
    pub const fn is_error(self) -> bool {
        matches!(self, Level::Error)
    }

    pub const fn is_warn(self) -> bool {
        matches!(self, Level::Warn)
    }

    pub const fn is_ignore(self) -> bool {
        matches!(self, Level::Ignore)
    }
}

impl fmt::Display for Level {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Level::Ignore => f.write_str("ignore"),
            Level::Warn => f.write_str("warn"),
            Level::Error => f.write_str("error"),
        }
    }
}

impl TryFrom<Level> for Severity {
    type Error = ();

    fn try_from(level: Level) -> Result<Self, ()> {
        match level {
            Level::Ignore => Err(()),
            Level::Warn => Ok(Severity::Warning),
            Level::Error => Ok(Severity::Error),
        }
    }
}

impl LintMetadata {
    pub fn name(&self) -> LintName {
        self.name
    }

    pub fn summary(&self) -> &str {
        self.summary
    }

    /// Returns the documentation line by line with Rust doc-comment prefixes and trailing
    /// whitespace removed.
    pub fn documentation_lines(&self) -> impl Iterator<Item = &str> {
        let has_doc_comment_prefix = self.raw_documentation.starts_with(' ');

        self.raw_documentation
            .strip_suffix('\n')
            .unwrap_or(self.raw_documentation)
            .lines()
            .map(move |line| {
                let line = if has_doc_comment_prefix {
                    line.strip_prefix(' ').unwrap_or(line)
                } else {
                    line
                };
                line.trim_end()
            })
    }

    /// Returns the documentation as a single string.
    pub fn documentation(&self) -> String {
        self.documentation_lines().join("\n")
    }

    pub(crate) fn documentation_url(&self) -> String {
        lint_documentation_url(self.name())
    }

    pub fn default_level(&self) -> Level {
        self.default_level
    }

    pub fn status(&self) -> &LintStatus {
        &self.status
    }

    pub fn file(&self) -> &str {
        self.file
    }

    pub fn line(&self) -> u32 {
        self.line
    }
}

pub(crate) fn lint_documentation_url(lint_name: LintName) -> String {
    format!("https://ty.dev/rules#{lint_name}")
}

#[doc(hidden)]
pub const fn lint_metadata_defaults(status: LintStatus) -> LintMetadata {
    LintMetadata {
        name: LintName::of(""),
        summary: "",
        raw_documentation: "",
        default_level: Level::Error,
        status,
        file: "",
        line: 1,
    }
}

#[derive(Copy, Clone, Debug)]
#[cfg_attr(
    feature = "serde",
    derive(serde::Serialize),
    serde(tag = "type", rename_all = "lowercase")
)]
pub enum LintStatus {
    /// The lint is stable.
    Stable {
        /// The version in which the lint was added.
        since: &'static str,
    },

    /// The lint is deprecated and no longer recommended for use.
    Deprecated {
        /// The version in which the lint was deprecated.
        since: &'static str,

        /// The reason why the lint has been deprecated.
        ///
        /// This should explain why the lint has been deprecated and if there's a replacement lint that users
        /// can use instead.
        reason: &'static str,
    },

    /// The lint has been removed and can no longer be used.
    Removed {
        /// The version in which the lint was removed.
        since: &'static str,

        /// The reason why the lint has been removed.
        reason: &'static str,
    },
}

impl LintStatus {
    pub const fn stable(since: &'static str) -> Self {
        LintStatus::Stable { since }
    }

    pub const fn deprecated(since: &'static str, reason: &'static str) -> Self {
        LintStatus::Deprecated { since, reason }
    }

    pub(crate) const fn removed(since: &'static str, reason: &'static str) -> Self {
        LintStatus::Removed { since, reason }
    }

    const fn is_removed(&self) -> bool {
        matches!(self, LintStatus::Removed { .. })
    }

    pub const fn is_deprecated(&self) -> bool {
        matches!(self, LintStatus::Deprecated { .. })
    }
}

/// Declares a lint rule with the given metadata.
///
/// ```rust
/// use ty_python_semantic::declare_lint;
/// use ty_python_semantic::lint::{LintStatus, Level};
///
/// declare_lint! {
///     /// ## What it does
///     /// Checks for references to names that are not defined.
///     ///
///     /// ## Why is this bad?
///     /// Using an undefined variable will raise a `NameError` at runtime.
///     ///
///     /// ## Example
///     ///
///     /// ```python
///     /// print(x)  # NameError: name 'x' is not defined
///     /// ```
///     pub(crate) static UNRESOLVED_REFERENCE = {
///         summary: "detects references to names that are not defined",
///         status: LintStatus::stable("1.0.0"),
///         default_level: Level::Warn,
///     }
/// }
/// ```
#[macro_export]
macro_rules! declare_lint {
    (
        $(#[expect($($expect:tt)*)])?
        $(#[allow($($allow:tt)*)])?
        $(#[doc = $doc:expr])+
        $vis: vis static $name: ident = {
            summary: $summary: literal,
            status: $status: expr,
            // Optional properties
            $( $key:ident: $value:expr, )*
        }
    ) => {
        $(#[expect($($expect)*)])?
        $(#[allow($($allow)*)])?
        $( #[doc = $doc] )+
        $vis static $name: $crate::lint::LintMetadata = $crate::lint::LintMetadata {
            name: ruff_db::diagnostic::LintName::of(ruff_macros::kebab_case!($name)),
            summary: $summary,
            raw_documentation: concat!($($doc, '\n',)+),
            file: file!(),
            line: line!(),
            $( $key: $value, )*
            ..$crate::lint::lint_metadata_defaults($status)
        };
    };
}

#[cfg(test)]
mod tests {
    use super::{Level, LintStatus};

    crate::declare_lint! {
        /// First line.
        ///
        ///     indented
        static INLINE_DOCUMENTATION = {
            summary: "inline documentation",
            status: LintStatus::stable("0.0.0"),
            default_level: Level::Error,
        }
    }

    crate::declare_lint! {
        #[doc = include_str!("../resources/lint_docs/invalid-attribute-access.md")]
        static INCLUDED_DOCUMENTATION = {
            summary: "included documentation",
            status: LintStatus::stable("0.0.0"),
            default_level: Level::Error,
        }
    }

    #[test]
    fn inline_documentation_strips_doc_comment_prefixes() {
        assert_eq!(
            INLINE_DOCUMENTATION.documentation(),
            "First line.\n\n    indented"
        );
    }

    #[test]
    fn included_documentation_preserves_indentation() {
        let documentation = INCLUDED_DOCUMENTATION.documentation();
        assert!(documentation.starts_with("## What it does"));
        assert!(documentation.contains("\n    class_var: ClassVar[int] = 1\n"));
    }
}

/// A unique identifier for a lint rule.
///
/// Implements `PartialEq`, `Eq`, and `Hash` based on the `LintMetadata` pointer
/// for fast comparison and lookup.
#[derive(Debug, Clone, Copy, get_size2::GetSize)]
pub struct LintId {
    definition: &'static LintMetadata,
}

impl LintId {
    pub const fn of(definition: &'static LintMetadata) -> Self {
        LintId { definition }
    }
}

impl PartialEq for LintId {
    fn eq(&self, other: &Self) -> bool {
        std::ptr::eq(self.definition, other.definition)
    }
}

impl Eq for LintId {}

impl std::hash::Hash for LintId {
    fn hash<H: Hasher>(&self, state: &mut H) {
        std::ptr::hash(self.definition, state);
    }
}

impl std::ops::Deref for LintId {
    type Target = LintMetadata;

    fn deref(&self) -> &Self::Target {
        self.definition
    }
}

#[derive(Default, Debug)]
pub struct LintRegistryBuilder {
    /// Registered lints that haven't been removed.
    lints: Vec<LintId>,

    /// Lints indexed by name, including aliases and removed rules.
    by_name: FxHashMap<&'static str, LintEntry>,
}

impl LintRegistryBuilder {
    #[track_caller]
    pub(crate) fn register_lint(&mut self, lint: &'static LintMetadata) {
        assert_eq!(
            self.by_name.insert(&*lint.name, lint.into()),
            None,
            "duplicate lint registration for '{name}'",
            name = lint.name
        );

        if !lint.status.is_removed() {
            self.lints.push(LintId::of(lint));
        }
    }

    #[track_caller]
    pub fn register_alias(&mut self, from: LintName, to: &'static LintMetadata) {
        let target = match self.by_name.get(to.name.as_str()) {
            Some(LintEntry::Lint(target) | LintEntry::Removed(target)) => target,
            Some(LintEntry::Alias(target)) => {
                panic!(
                    "lint alias {from} -> {to:?} points to another alias {target:?}",
                    target = target.name()
                )
            }
            None => panic!(
                "lint alias {from} -> {to} points to non-registered lint",
                to = to.name
            ),
        };

        assert_eq!(
            self.by_name
                .insert(from.as_str(), LintEntry::Alias(*target)),
            None,
            "duplicate lint registration for '{from}'",
        );
    }

    pub(crate) fn build(self) -> LintRegistry {
        LintRegistry {
            lints: self.lints,
            by_name: self.by_name,
        }
    }
}

#[derive(Default, Debug, Clone)]
pub struct LintRegistry {
    lints: Vec<LintId>,
    by_name: FxHashMap<&'static str, LintEntry>,
}

impl LintRegistry {
    /// Looks up a lint by its name.
    pub fn get(&self, code: &str) -> Result<LintId, GetLintError> {
        match self.by_name.get(code) {
            Some(LintEntry::Lint(metadata)) => Ok(*metadata),
            Some(LintEntry::Alias(lint)) => {
                if lint.status.is_removed() {
                    Err(GetLintError::Removed(lint.name()))
                } else {
                    Ok(*lint)
                }
            }
            Some(LintEntry::Removed(lint)) => Err(GetLintError::Removed(lint.name())),
            None => {
                if let Some(without_prefix) = DiagnosticId::strip_category(code) {
                    if let Some(entry) = self.by_name.get(without_prefix) {
                        return Err(GetLintError::PrefixedWithCategory {
                            prefixed: code.to_string(),
                            suggestion: entry.id().name.to_string(),
                        });
                    }
                }

                let suggestion = did_you_mean(self.by_name.keys().copied(), code);

                Err(GetLintError::Unknown {
                    code: code.to_string(),
                    suggestion: suggestion.map(str::to_string),
                })
            }
        }
    }

    /// Returns all registered, non-removed lints.
    pub fn lints(&self) -> &[LintId] {
        &self.lints
    }

    /// Returns an iterator over all known aliases and to their target lints.
    ///
    /// This iterator includes aliases that point to removed lints.
    pub fn aliases(&self) -> impl Iterator<Item = (LintName, LintId)> + '_ {
        self.by_name.iter().filter_map(|(key, value)| {
            if let LintEntry::Alias(alias) = value {
                Some((LintName::of(key), *alias))
            } else {
                None
            }
        })
    }

    /// Iterates over all removed lints.
    pub fn removed(&self) -> impl Iterator<Item = LintId> + '_ {
        self.by_name.values().filter_map(|value| {
            if let LintEntry::Removed(metadata) = value {
                Some(*metadata)
            } else {
                None
            }
        })
    }
}

#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)]
pub enum GetLintError {
    /// The name maps to this removed lint.
    Removed(LintName),

    /// No lint with the given name is known.
    Unknown {
        code: String,
        suggestion: Option<String>,
    },

    /// The name uses the full qualified diagnostic id `lint:<rule>` instead of just `rule`.
    /// The String is the name without the `lint:` category prefix.
    PrefixedWithCategory {
        prefixed: String,
        suggestion: String,
    },
}

impl Error for GetLintError {}

impl std::fmt::Display for GetLintError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            GetLintError::Removed(code) => write!(f, "Removed rule `{code}`"),
            GetLintError::Unknown { code, suggestion } => match suggestion {
                None => write!(f, "Unknown rule `{code}`"),
                Some(suggestion) => {
                    write!(f, "Unknown rule `{code}`. Did you mean `{suggestion}`?")
                }
            },
            GetLintError::PrefixedWithCategory {
                prefixed,
                suggestion,
            } => write!(f, "Unknown rule `{prefixed}`. Did you mean `{suggestion}`?"),
        }
    }
}

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum LintEntry {
    /// An existing lint rule. Can be stable or deprecated.
    Lint(LintId),
    /// A lint rule that has been removed.
    Removed(LintId),
    Alias(LintId),
}

impl LintEntry {
    fn id(self) -> LintId {
        match self {
            LintEntry::Lint(id) => id,
            LintEntry::Removed(id) => id,
            LintEntry::Alias(id) => id,
        }
    }
}

impl From<&'static LintMetadata> for LintEntry {
    fn from(metadata: &'static LintMetadata) -> Self {
        if metadata.status.is_removed() {
            LintEntry::Removed(LintId::of(metadata))
        } else {
            LintEntry::Lint(LintId::of(metadata))
        }
    }
}

#[derive(Clone, Default, PartialEq, Eq, get_size2::GetSize)]
pub struct RuleSelection {
    /// Map with the severity for each enabled lint rule.
    ///
    /// If a rule isn't present in this map, then it should be considered disabled.
    lints: FxHashMap<LintId, (Severity, LintSource)>,
}

impl RuleSelection {
    /// Creates a new rule selection from all known lints in the registry that are enabled
    /// according to their default severity.
    pub fn from_registry(registry: &LintRegistry) -> Self {
        Self::from_registry_with_default(registry, None)
    }

    /// Creates a new rule selection from all known lints in the registry, including lints that are default by default.
    /// Lints that are disabled by default use the `default_severity`.
    pub fn all(registry: &LintRegistry, default_severity: Severity) -> Self {
        Self::from_registry_with_default(registry, Some(default_severity))
    }

    fn from_registry_with_default(
        registry: &LintRegistry,
        default_severity: Option<Severity>,
    ) -> Self {
        let lints = registry
            .lints()
            .iter()
            .filter_map(|lint| {
                Severity::try_from(lint.default_level())
                    .ok()
                    .or(default_severity)
                    .map(|severity| (*lint, (severity, LintSource::Default)))
            })
            .collect();

        RuleSelection { lints }
    }

    /// Returns an iterator over all enabled lints.
    pub fn enabled(&self) -> impl Iterator<Item = LintId> + '_ {
        self.lints.keys().copied()
    }

    /// Returns an iterator over all enabled lints and their severity.
    pub fn iter(&self) -> impl ExactSizeIterator<Item = (LintId, Severity)> + '_ {
        self.lints
            .iter()
            .map(|(&lint, &(severity, _))| (lint, severity))
    }

    /// Returns the configured severity for the lint with the given id or `None` if the lint is disabled.
    pub(crate) fn severity(&self, lint: LintId) -> Option<Severity> {
        self.lints.get(&lint).map(|(severity, _)| *severity)
    }

    pub(crate) fn get(&self, lint: LintId) -> Option<(Severity, LintSource)> {
        self.lints.get(&lint).copied()
    }

    /// Returns `true` if the `lint` is enabled.
    pub(crate) fn is_enabled(&self, lint: LintId) -> bool {
        self.severity(lint).is_some()
    }

    /// Enables `lint` and configures with the given `severity`.
    ///
    /// Overrides any previous configuration for the lint.
    pub fn enable(&mut self, lint: LintId, severity: Severity, source: LintSource) {
        self.lints.insert(lint, (severity, source));
    }

    /// Disables `lint` if it was previously enabled.
    pub fn disable(&mut self, lint: LintId) {
        self.lints.remove(&lint);
    }
}

// The default `LintId` debug implementation prints the entire lint metadata.
// This is way too verbose.
impl fmt::Debug for RuleSelection {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let lints = self.lints.iter().sorted_by_key(|(lint, _)| lint.name);

        if f.alternate() {
            let mut f = f.debug_map();

            for (lint, (severity, source)) in lints {
                f.entry(
                    &lint.name().as_str(),
                    &format_args!("{severity:?} ({source:?})"),
                );
            }

            f.finish()
        } else {
            let mut f = f.debug_set();

            for (lint, _) in lints {
                f.entry(&lint.name());
            }

            f.finish()
        }
    }
}

#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, get_size2::GetSize)]
pub enum LintSource {
    /// The user didn't enable the rule explicitly, instead it's enabled by default.
    #[default]
    Default,

    /// The rule was enabled by using a CLI argument
    Cli,

    /// The rule was enabled in a configuration file.
    File,

    /// The rule was enabled from the configuration in the editor.
    Editor,

    /// The rule was enabled by uv workspace metadata.
    UvWorkspace,
}
