use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_trivia::is_python_whitespace;
use ruff_text_size::{TextRange, TextSize};

use crate::Locator;
use crate::Violation;
use crate::checkers::ast::LintContext;

/// ## What it does
/// Checks for a shebang directive that is not at the beginning of the file.
///
/// ## Why is this bad?
/// In Python, a shebang (also known as a hashbang) is the first line of a
/// script, which specifies the interpreter that should be used to run the
/// script.
///
/// The shebang's `#!` prefix must be the first two characters of a file. If
/// the shebang is not at the beginning of the file, it will be ignored, which
/// is likely a mistake.
///
/// ## Example
/// ```python
/// foo = 1
/// #!/usr/bin/env python3
/// ```
///
/// Use instead:
/// ```python
/// #!/usr/bin/env python3
/// foo = 1
/// ```
///
/// ## References
/// - [Python documentation: Executable Python Scripts](https://docs.python.org/3/tutorial/appendix.html#executable-python-scripts)
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.229")]
pub(crate) struct ShebangNotFirstLine;

impl Violation for ShebangNotFirstLine {
    #[derive_message_formats]
    fn message(&self) -> String {
        "Shebang should be at the beginning of the file".to_string()
    }
}

/// EXE005
pub(crate) fn shebang_not_first_line(range: TextRange, locator: &Locator, context: &LintContext) {
    // If the shebang is at the beginning of the file, abort.
    if range.start() == TextSize::from(0) {
        return;
    }

    // If the entire prefix is whitespace, abort (this is handled by EXE004).
    if locator
        .up_to(range.start())
        .chars()
        .all(|c| is_python_whitespace(c) || matches!(c, '\r' | '\n'))
    {
        return;
    }

    context.report_diagnostic_if_enabled(ShebangNotFirstLine, range);
}
