use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::{SemanticModel, analyze};
use ruff_python_stdlib::builtins;
use ruff_text_size::Ranged;

use crate::checkers::ast::Checker;
use crate::preview::is_custom_exception_checking_enabled;
use crate::{Edit, Fix, FixAvailability, Violation};
use ruff_python_ast::PythonVersion;

/// ## What it does
/// Checks for an exception that is not raised.
///
/// ## Why is this bad?
/// It's unnecessary to create an exception without raising it. For example,
/// `ValueError("...")` on its own will have no effect (unlike
/// `raise ValueError("...")`) and is likely a mistake.
///
/// ## Known problems
/// This rule only detects built-in exceptions, like `ValueError`, and does
/// not catch user-defined exceptions.
///
/// In [preview], this rule will also detect user-defined exceptions, but only
/// the ones defined in the file being checked.
///
/// ## Example
/// ```python
/// ValueError("...")
/// ```
///
/// Use instead:
/// ```python
/// raise ValueError("...")
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as converting a useless exception
/// statement to a `raise` statement will change the program's behavior.
///
/// [preview]: https://docs.astral.sh/ruff/preview/
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "0.5.0")]
pub(crate) struct UselessExceptionStatement;

impl Violation for UselessExceptionStatement {
    const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;

    #[derive_message_formats]
    fn message(&self) -> String {
        "Missing `raise` statement on exception".to_string()
    }

    fn fix_title(&self) -> Option<String> {
        Some("Add `raise` keyword".to_string())
    }
}

/// PLW0133
pub(crate) fn useless_exception_statement(checker: &Checker, expr: &ast::StmtExpr) {
    let Expr::Call(ast::ExprCall { func, .. }) = expr.value.as_ref() else {
        return;
    };

    if is_builtin_exception(func, checker.semantic(), checker.target_version())
        || (is_custom_exception_checking_enabled(checker.settings())
            && is_custom_exception(func, checker.semantic(), checker.target_version()))
    {
        let mut diagnostic = checker.report_diagnostic(UselessExceptionStatement, expr.range());
        diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
            "raise ".to_string(),
            expr.start(),
        )));
    }
}

/// Returns `true` if the given expression is a builtin exception.
fn is_builtin_exception(
    expr: &Expr,
    semantic: &SemanticModel,
    target_version: PythonVersion,
) -> bool {
    semantic
        .resolve_qualified_name(expr)
        .is_some_and(|qualified_name| {
            matches!(qualified_name.segments(), ["" | "builtins", name]
            if builtins::is_exception(name, target_version.minor))
        })
}

/// Returns `true` if the given expression is a custom exception.
fn is_custom_exception(
    expr: &Expr,
    semantic: &SemanticModel,
    target_version: PythonVersion,
) -> bool {
    let Some(qualified_name) = semantic.resolve_qualified_name(expr) else {
        return false;
    };
    let Some(symbol) = qualified_name.segments().last() else {
        return false;
    };
    let Some(binding_id) = semantic.lookup_symbol(symbol) else {
        return false;
    };
    let binding = semantic.binding(binding_id);
    let Some(source) = binding.source else {
        return false;
    };
    let statement = semantic.statement(source);
    if let ast::Stmt::ClassDef(class_def) = statement {
        return analyze::class::any_qualified_base_class(class_def, semantic, |qualified_name| {
            if let ["" | "builtins", name] = qualified_name.segments() {
                return builtins::is_exception(name, target_version.minor);
            }
            false
        });
    }
    false
}
