//! Linting for Django/Jinja templates.
//!
//! This crate provides lint rules for HTML templates with Django/Jinja syntax.
//! It works on top of the AST generated by `markup_fmt`.
//!
//! # Example
//!
//! ```
//! use djangofmt_lint::{check_ast, Settings};
//! use markup_fmt::{Language, parser::Parser};
//!
//! let source = r#"<form method="put"></form>"#;
//! let mut parser = Parser::new(source, Language::Jinja, vec![]);
//! let ast = parser.parse_root().unwrap();
//!
//! let diagnostics = check_ast(&ast, &Settings::default());
//! assert_eq!(diagnostics.len(), 1);
//! ```

mod checker;
pub mod registry;
mod rules;
pub mod settings;
mod violation;

pub use checker::Checker;
pub use registry::{Rule, RuleCategory};
pub use settings::Settings;

use markup_fmt::ast::Root;
use miette::{Diagnostic, NamedSource, SourceSpan};

/// A single lint diagnostic without source code.
///
/// Source code is added later via [`FileDiagnostics`] to avoid cloning
/// the entire file content for each diagnostic.
#[derive(Debug, Diagnostic, thiserror::Error)]
#[error("{message}")]
pub struct LintDiagnostic {
    /// Rule code (e.g., "invalid-attr-value").
    #[diagnostic(code)]
    pub code: String,
    /// Human-readable error message.
    pub message: String,
    /// Source span where the error occurred.
    #[label("here")]
    pub span: SourceSpan,
    /// Optional help text with suggestions.
    #[help]
    pub help: Option<String>,
}

/// A collection of lint diagnostics for a single file.
///
/// This struct holds the source code once and references it for all diagnostics,
/// avoiding the need to clone the source for each individual diagnostic.
#[derive(Debug, Diagnostic, thiserror::Error)]
#[error("Found {} lint error(s)", self.related.len())]
pub struct FileDiagnostics {
    /// The source code of the file.
    #[source_code]
    pub source_code: NamedSource<String>,
    /// All diagnostics found in this file.
    #[related]
    pub related: Vec<LintDiagnostic>,
}

impl FileDiagnostics {
    /// Create a new `FileDiagnostics` from source code and diagnostics.
    #[must_use]
    pub const fn new(source_code: NamedSource<String>, diagnostics: Vec<LintDiagnostic>) -> Self {
        Self {
            source_code,
            related: diagnostics,
        }
    }

    /// Create a new empty `FileDiagnostics`.
    #[must_use]
    pub fn empty() -> Self {
        Self {
            source_code: NamedSource::new("", String::with_capacity(0)),
            related: Vec::new(),
        }
    }

    /// Returns true if there are no diagnostics.
    #[must_use]
    pub const fn is_empty(&self) -> bool {
        self.related.is_empty()
    }

    /// Returns the number of diagnostics.
    #[must_use]
    pub const fn len(&self) -> usize {
        self.related.len()
    }
}

/// Check the AST for lint errors.
///
/// Traverses the AST and runs all enabled lint rules, returning any diagnostics found.
#[must_use]
pub fn check_ast(ast: &Root<'_>, settings: &Settings) -> Vec<LintDiagnostic> {
    let mut checker = Checker::new(settings);
    checker.visit_root(ast);
    checker.into_diagnostics()
}
