use rustc_hash::FxHashSet;

use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Stmt};
use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr};
use ruff_text_size::Ranged;

use crate::Violation;
use crate::checkers::ast::Checker;
use crate::rules::ruff::helpers::{
    dataclass_kind, has_default_copy_semantics, is_class_var_annotation, is_final_annotation,
    is_special_attribute,
};

/// ## What it does
/// Checks for mutable default values in class attributes.
///
/// ## Why is this bad?
/// Mutable default values share state across all instances of the class,
/// while not being obvious. This can lead to bugs when the attributes are
/// changed in one instance, as those changes will unexpectedly affect all
/// other instances.
///
/// Generally speaking, you probably want to avoid having mutable default
/// values in the class body at all; instead, these variables should usually
/// be initialized in `__init__`. However, other possible fixes for the issue
/// can include:
/// - Explicitly annotating the variable with [`typing.ClassVar`][ClassVar] to
///   indicate that it is intended to be shared across all instances.
/// - Using an immutable data type (e.g. a tuple instead of a list)
///   for the default value.
///
/// ## Example
///
/// ```python
/// class A:
///     variable_1: list[int] = []
///     variable_2: set[int] = set()
///     variable_3: dict[str, int] = {}
/// ```
///
/// Use instead:
///
/// ```python
/// class A:
///     def __init__(self) -> None:
///         self.variable_1: list[int] = []
///         self.variable_2: set[int] = set()
///         self.variable_3: dict[str, int] = {}
/// ```
///
/// Or:
///
/// ```python
/// from typing import ClassVar
///
///
/// class A:
///     variable_1: ClassVar[list[int]] = []
///     variable_2: ClassVar[set[int]] = set()
///     variable_3: ClassVar[dict[str, int]] = {}
/// ```
///
/// Or:
///
/// ```python
/// class A:
///     variable_1: list[int] | None = None
///     variable_2: set[int] | None = None
///     variable_3: dict[str, int] | None = None
/// ```
///
/// Or:
///
/// ```python
/// from collections.abc import Sequence, Mapping, Set as AbstractSet
/// from types import MappingProxyType
///
///
/// class A:
///     variable_1: Sequence[int] = ()
///     variable_2: AbstractSet[int] = frozenset()
///     variable_3: Mapping[str, int] = MappingProxyType({})
/// ```
///
/// [ClassVar]: https://docs.python.org/3/library/typing.html#typing.ClassVar
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.273")]
pub(crate) struct MutableClassDefault;

impl Violation for MutableClassDefault {
    #[derive_message_formats]
    fn message(&self) -> String {
        "Mutable class attributes should be annotated with `typing.ClassVar`".to_string()
    }
}

/// RUF012
pub(crate) fn mutable_class_default(checker: &Checker, class_def: &ast::StmtClassDef) {
    // Collect any `ClassVar`s we find in case they get reassigned later.
    let mut class_var_targets = FxHashSet::default();

    for statement in &class_def.body {
        match statement {
            Stmt::AnnAssign(ast::StmtAnnAssign {
                annotation,
                target,
                value: Some(value),
                ..
            }) => {
                if let ast::Expr::Name(ast::ExprName { id, .. }) = target.as_ref() {
                    if is_class_var_annotation(annotation, checker.semantic()) {
                        class_var_targets.insert(id);
                    }
                }

                if !is_special_attribute(target)
                    && is_mutable_expr(value, checker.semantic())
                    && !is_class_var_annotation(annotation, checker.semantic())
                    && !is_final_annotation(annotation, checker.semantic())
                    && !is_immutable_annotation(annotation, checker.semantic(), &[])
                {
                    if dataclass_kind(class_def, checker.semantic()).is_some() {
                        continue;
                    }

                    // Avoid, e.g., Pydantic and msgspec models, which end up copying defaults on instance creation.
                    if has_default_copy_semantics(class_def, checker.semantic()) {
                        return;
                    }

                    checker.report_diagnostic(MutableClassDefault, value.range());
                }
            }
            Stmt::Assign(ast::StmtAssign { value, targets, .. }) => {
                if !targets.iter().all(|target| {
                    is_special_attribute(target)
                        || target
                            .as_name_expr()
                            .is_some_and(|name| class_var_targets.contains(&name.id))
                }) && is_mutable_expr(value, checker.semantic())
                {
                    // Avoid, e.g., Pydantic and msgspec models, which end up copying defaults on instance creation.
                    if has_default_copy_semantics(class_def, checker.semantic()) {
                        return;
                    }

                    checker.report_diagnostic(MutableClassDefault, value.range());
                }
            }
            _ => (),
        }
    }
}
