use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::ExprSubscript;

use crate::{checkers::ast::Checker, settings::types::PythonVersion};

/// ## What it does
/// Checks for uses of `Unpack[]` on Python 3.11 and above, and suggests
/// using `*` instead.
///
/// ## Why is this bad?
/// [PEP 646] introduced a new syntax for unpacking sequences based on the `*`
/// operator. This syntax is more concise and readable than the previous
/// `Unpack[]` syntax.
///
/// ## Example
///
/// ```python
/// from typing import Unpack
///
///
/// def foo(*args: Unpack[tuple[int, ...]]) -> None:
///     pass
/// ```
///
/// Use instead:
///
/// ```python
/// def foo(*args: *tuple[int, ...]) -> None:
///     pass
/// ```
///
/// [PEP 646]: https://peps.python.org/pep-0646/
#[violation]
pub struct NonPEP646Unpack;

impl Violation for NonPEP646Unpack {
    const FIX_AVAILABILITY: FixAvailability = FixAvailability::Always;

    #[derive_message_formats]
    fn message(&self) -> String {
        "Use `*` for unpacking".to_string()
    }

    fn fix_title(&self) -> Option<String> {
        Some("Convert to `*` for unpacking".to_string())
    }
}

/// UP044
pub(crate) fn use_pep646_unpack(checker: &mut Checker, expr: &ExprSubscript) {
    if checker.settings.target_version < PythonVersion::Py311 {
        return;
    }

    if !checker.semantic().seen_typing() {
        return;
    }

    // Ignore `kwarg` unpacking, as in:
    // ```python
    // def f(**kwargs: Unpack[Array]):
    //     ...
    // ```
    if checker
        .semantic()
        .current_statement()
        .as_function_def_stmt()
        .and_then(|stmt| stmt.parameters.kwarg.as_ref())
        .and_then(|kwarg| kwarg.annotation.as_ref())
        .and_then(|annotation| annotation.as_subscript_expr())
        .is_some_and(|subscript| subscript == expr)
    {
        return;
    }

    let ExprSubscript {
        range,
        value,
        slice,
        ..
    } = expr;

    if !checker.semantic().match_typing_expr(value, "Unpack") {
        return;
    }

    // Skip semantically invalid subscript calls (e.g. `Unpack[str | num]`).
    if !(slice.is_name_expr() || slice.is_subscript_expr() || slice.is_attribute_expr()) {
        return;
    }

    let mut diagnostic = Diagnostic::new(NonPEP646Unpack, *range);

    let inner = checker.locator().slice(slice.as_ref());

    diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
        format!("*{inner}"),
        *range,
    )));

    checker.diagnostics.push(diagnostic);
}
