//! Diagnostics primitives for all stages of compiler and frontend code.
//!
//! These types describe what to show the user when something goes wrong while
//! parsing or doing lightweight frontend checks. They are intentionally small,
//! depend only on [`AstSpan`], and are easy to construct and store.

mod parse_error_diagnostics;
mod render;

use crate::lexer::TokenSpan;
pub(crate) use parse_error_diagnostics::diagnostic_from_parse_error;

/// Classification of a diagnostic by its impact.
///
/// Choose the most restrictive level that reflects the state of the source and
/// the compiler's ability to continue.
///
/// - `Error`: use when the source is invalid according to the language rules or
///     the parser cannot make a sound interpretation. Errors typically prevent
///     code generation. Examples: mismatched delimiters,
///     missing required tokens, invalid escapes, unrecoverable ambiguity.
/// - `Warning`: use when the source is valid but likely unintended, obsolete,
///     or suboptimal. Warnings should not change program semantics if ignored
///     and must not block compilation. Examples: deprecated syntax, shadowing
///     that is allowed but suspicious, unreachable code after a return.
///     Do not use warnings to paper over true syntax errors. If the construct
///     is invalid, prefer [`Severity::Error`] even if recovery is possible.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
#[non_exhaustive]
pub enum Severity {
    /// Fatal to the current compilation unit or requires recovery.
    Error,
    /// Non-fatal advisory about suspicious but valid code.
    Warning,
}

/// A labeled source span with a short inline message.
///
/// Message should be one sentence, start lowercase, and omit the final period.
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct Label {
    /// Span to highlight in source coordinates.
    pub span: TokenSpan,
    /// Short inline text shown next to the caret line.
    pub message: String,
}

/// A single pure data diagnostic message with optional structured context.
#[derive(Clone, Debug, PartialEq, Eq)]
#[must_use]
pub struct Diagnostic {
    /// Headline, e.g. "Mismatched closing delimiter: `}`".
    headline: String,
    /// Impact of the diagnostic. See [`Severity`] for guidance.
    severity: Severity,
    /// Optional stable identifier, e.g. "P0007" or "L0001".
    ///
    /// Codes must match `^[LPTSXD][0-9]{4}$` where the prefix is the domain:
    /// `L` lexer, `P` parser, `T` type check, `S` semantics, `X` lints,
    /// `D` deprecations.
    code: Option<String>,
    /// Marks the main location the user should look at first.
    ///
    /// Typically the exact token or span that triggered the diagnostic.
    primary_label: Option<Label>,
    /// Supplemental locations that add context or cross-reference the primary
    /// site.
    ///
    /// Examples: "matching delimiter was opened here", "declared here",
    /// "previous use here". Secondary labels should not compete with
    /// the primary.
    secondary_labels: Vec<Label>,
    /// A single actionable suggestion aimed at a quick fix. Keep it concise.
    help: Option<String>,
    /// Additional free-form lines not intended as a fix suggestion.
    ///
    /// Use for technical details, references, or rationale. Keep each string to
    /// a single paragraph.
    notes: Vec<String>,
}

impl Diagnostic {
    /// Construct a new error diagnostic with the given headline.
    ///
    /// Use for invalid constructs that prevent a sound interpretation.
    pub fn error(headline: impl Into<String>) -> Self {
        Self {
            headline: headline.into(),
            severity: Severity::Error,
            code: None,
            primary_label: None,
            secondary_labels: Vec::new(),
            notes: Vec::new(),
            help: None,
        }
    }

    /// Construct a new warning diagnostic with the given headline.
    ///
    /// Use for valid but suspicious or suboptimal constructs.
    pub fn warning(headline: impl Into<String>) -> Self {
        Self {
            headline: headline.into(),
            severity: Severity::Warning,
            code: None,
            primary_label: None,
            secondary_labels: Vec::new(),
            notes: Vec::new(),
            help: None,
        }
    }

    /// Returns `true` iff severity is [`Severity::Error`].
    #[must_use]
    pub fn stops_compilation(&self) -> bool {
        self.severity == Severity::Error
    }

    /// Returns the diagnostic code if present.
    ///
    /// See [`DiagnosticBuilder::code`] for code scheme.
    #[must_use]
    pub fn code(&self) -> Option<&str> {
        self.code.as_deref()
    }

    /// Returns the primary label, if any.
    #[must_use]
    pub const fn primary_label(&self) -> Option<&Label> {
        self.primary_label.as_ref()
    }

    /// Returns the secondary labels in insertion order.
    #[must_use]
    pub fn secondary_labels(&self) -> &[Label] {
        &self.secondary_labels
    }

    /// Returns the headline.
    #[must_use]
    pub fn headline(&self) -> &str {
        &self.headline
    }

    /// Returns the severity.
    #[must_use]
    pub const fn severity(&self) -> Severity {
        self.severity
    }

    /// Returns the notes.
    #[must_use]
    pub fn notes(&self) -> &[String] {
        &self.notes
    }

    /// Returns the help message, if any.
    #[must_use]
    pub fn help(&self) -> Option<&str> {
        self.help.as_deref()
    }
}

/// A convenient diagnostic builder.
#[derive(Debug)]
#[must_use]
pub struct DiagnosticBuilder {
    diagnostic: Diagnostic,
}

impl DiagnosticBuilder {
    /// Creates a new builder for an error diagnostic with a given headline.
    pub fn error(headline: impl Into<String>) -> Self {
        Self {
            diagnostic: Diagnostic::error(headline),
        }
    }

    /// Creates a new builder for a warning diagnostic with a given headline.
    pub fn warning(headline: impl Into<String>) -> Self {
        Self {
            diagnostic: Diagnostic::warning(headline),
        }
    }

    /// Attach or replace the stable diagnostic code.
    ///
    /// Codes identify classes of diagnostics across versions.
    /// Keep them short, ASCII-only, and stable. Prefer the scheme:
    ///
    /// 1. Prefix = domain:
    ///    - `L`: lexer (invalid char, unterminated string);
    ///    - `P`: parser (mismatched delimiters, expected X found Y);
    ///    - `T`: type check;
    ///    - `S`: semantic analysis (name resolution, visibility);
    ///    - `X`: style/lints (shadowing, dead code);
    ///    - `D`: deprecations.
    ///
    /// 2. Suffix = 4 digits, zero-padded: `0001`..`9999`.
    ///    Example codes: `L0001`, `P0007`, `T0123`.
    ///
    /// Codes are optional, but once published should not change.
    pub fn code(mut self, code: impl Into<String>) -> Self {
        self.diagnostic.code = Some(code.into());
        self
    }

    /// Attach or replace a primary label.
    ///
    /// One sentence, starting with lowercase letter, no period at the end.
    /// Since only one primary label can be specified, the previous primary is
    /// replaced.
    pub fn primary_label(mut self, span: TokenSpan, message: impl Into<String>) -> Self {
        self.diagnostic.primary_label = Some(Label {
            span,
            message: message.into(),
        });
        self
    }

    /// Add a secondary label.
    ///
    /// One sentence, starting with lowercase letter, no period at the end.
    pub fn secondary_label(mut self, span: TokenSpan, message: impl Into<String>) -> Self {
        self.diagnostic.secondary_labels.push(Label {
            span,
            message: message.into(),
        });
        self
    }

    /// Add a free-form note line.
    ///
    /// Can be several sentences, starting with uppercase letter and with period
    /// at the end.
    pub fn note(mut self, message: impl Into<String>) -> Self {
        self.diagnostic.notes.push(message.into());
        self
    }

    /// Set the help message.
    ///
    /// Can be several sentences, starting with uppercase letter and with period
    /// at the end.
    pub fn help(mut self, message: impl Into<String>) -> Self {
        self.diagnostic.help = Some(message.into());
        self
    }

    /// Finishes building and returns the diagnostic.
    pub fn build(self) -> Diagnostic {
        self.diagnostic
    }
}

impl From<DiagnosticBuilder> for Diagnostic {
    fn from(diagnostic_builder: DiagnosticBuilder) -> Self {
        diagnostic_builder.build()
    }
}
