/// DreamStack Diagnostic — unified error/warning type shared across compiler crates.
///
/// Provides Elm-style error rendering with carets, multi-span labels, and suggestions.

use ds_parser::Span;

// ── Core Types ──────────────────────────────────────────

/// A compiler diagnostic — error, warning, or hint.
#[derive(Debug, Clone)]
pub struct Diagnostic {
    pub severity: Severity,
    pub code: Option<String>,
    pub message: String,
    pub span: Span,
    pub labels: Vec<Label>,
    pub suggestion: Option<Suggestion>,
}

/// Severity level of a diagnostic.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity {
    Hint,
    Warning,
    Error,
}

/// A secondary label pointing at a span with a message.
#[derive(Debug, Clone)]
pub struct Label {
    pub span: Span,
    pub message: String,
}

/// A suggested fix attached to a diagnostic.
#[derive(Debug, Clone)]
pub struct Suggestion {
    pub message: String,
    pub replacement: String,
    pub span: Span,
}

// ── Constructors ────────────────────────────────────────

impl Diagnostic {
    /// Create an error diagnostic.
    pub fn error(message: impl Into<String>, span: Span) -> Self {
        Diagnostic {
            severity: Severity::Error,
            code: None,
            message: message.into(),
            span,
            labels: Vec::new(),
            suggestion: None,
        }
    }

    /// Create a warning diagnostic.
    pub fn warning(message: impl Into<String>, span: Span) -> Self {
        Diagnostic {
            severity: Severity::Warning,
            code: None,
            message: message.into(),
            span,
            labels: Vec::new(),
            suggestion: None,
        }
    }

    /// Create a hint diagnostic.
    pub fn hint(message: impl Into<String>, span: Span) -> Self {
        Diagnostic {
            severity: Severity::Hint,
            code: None,
            message: message.into(),
            span,
            labels: Vec::new(),
            suggestion: None,
        }
    }

    /// Attach a diagnostic code (e.g. "E0001").
    pub fn with_code(mut self, code: impl Into<String>) -> Self {
        self.code = Some(code.into());
        self
    }

    /// Add a secondary label.
    pub fn with_label(mut self, span: Span, message: impl Into<String>) -> Self {
        self.labels.push(Label { span, message: message.into() });
        self
    }

    /// Add a suggestion.
    pub fn with_suggestion(mut self, span: Span, message: impl Into<String>, replacement: impl Into<String>) -> Self {
        self.suggestion = Some(Suggestion {
            message: message.into(),
            replacement: replacement.into(),
            span,
        });
        self
    }
}

// ── Rendering ───────────────────────────────────────────

impl Severity {
    pub fn label(&self) -> &'static str {
        match self {
            Severity::Error => "ERROR",
            Severity::Warning => "WARNING",
            Severity::Hint => "HINT",
        }
    }

    pub fn prefix(&self) -> &'static str {
        match self {
            Severity::Error => "error",
            Severity::Warning => "warning",
            Severity::Hint => "hint",
        }
    }
}

impl std::fmt::Display for Severity {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.label())
    }
}

/// Render a diagnostic with source context, carets, and labels.
///
/// Produces Elm/Rust-style error output:
/// ```text
/// ── ERROR ──────────────────────────────────────────────
/// 5:12
///
///   5 │ let count: Int = "hello"
///     │                  ^^^^^^^ expected Int, found String
///
///   Hint: ...
/// ```
pub fn render(diag: &Diagnostic, source: &str) -> String {
    let mut out = String::new();
    let lines: Vec<&str> = source.lines().collect();

    // Header
    let title = match &diag.code {
        Some(code) => format!("{} [{}]", diag.severity.label(), code),
        None => diag.severity.label().to_string(),
    };
    let rule_width = 60usize.saturating_sub(title.len() + 4);
    out.push_str(&format!("── {} {}\n", title, "─".repeat(rule_width)));

    // Primary span
    let line = diag.span.line as usize;
    let col = diag.span.col as usize;

    out.push_str(&format!("{}:{}\n", line, col));

    // Source context
    if line > 0 && line <= lines.len() {
        let src_line = lines[line - 1];
        let line_num = format!("{}", line);
        let pad = " ".repeat(line_num.len());

        out.push('\n');
        // Line before (context)
        if line >= 2 && line - 1 <= lines.len() {
            let prev = lines[line - 2];
            if !prev.trim().is_empty() {
                out.push_str(&format!("  {} │ {}\n", format!("{:>width$}", line - 1, width = line_num.len()), prev));
            }
        }

        out.push_str(&format!("  {} │ {}\n", line_num, src_line));

        // Caret line
        let caret_start = if col > 0 { col - 1 } else { 0 };
        let caret_len = if diag.span.end > diag.span.start {
            (diag.span.end - diag.span.start).max(1)
        } else {
            1
        };
        out.push_str(&format!("  {} │ {}{}",
            pad,
            " ".repeat(caret_start),
            "^".repeat(caret_len),
        ));

        // Primary message on caret line
        out.push_str(&format!(" {}\n", diag.message));
    }

    // Secondary labels
    for label in &diag.labels {
        let l = label.span.line as usize;
        if l > 0 && l <= lines.len() {
            let src = lines[l - 1];
            let lnum = format!("{}", l);
            let lpad = " ".repeat(lnum.len());
            out.push('\n');
            out.push_str(&format!("  {} │ {}\n", lnum, src));
            let lc = if label.span.col > 0 { label.span.col as usize - 1 } else { 0 };
            let ll = if label.span.end > label.span.start {
                (label.span.end - label.span.start).max(1)
            } else {
                1
            };
            out.push_str(&format!("  {} │ {}{} {}\n", lpad, " ".repeat(lc), "-".repeat(ll), label.message));
        }
    }

    // Suggestion
    if let Some(ref sugg) = diag.suggestion {
        out.push_str(&format!("\n  Hint: {}\n", sugg.message));
        if !sugg.replacement.is_empty() {
            out.push_str(&format!("  Try:  {}\n", sugg.replacement));
        }
    }

    out
}

// ── Sorting ─────────────────────────────────────────────

/// Sort diagnostics by severity (errors first) then by span position.
pub fn sort_diagnostics(diags: &mut Vec<Diagnostic>) {
    diags.sort_by(|a, b| {
        b.severity.cmp(&a.severity)
            .then_with(|| a.span.line.cmp(&b.span.line))
            .then_with(|| a.span.col.cmp(&b.span.col))
    });
}

// ── Conversions ─────────────────────────────────────────

use ds_parser::parser::ParseError;

/// Convert a `ParseError` into a `Diagnostic`.
impl From<ParseError> for Diagnostic {
    fn from(err: ParseError) -> Self {
        Diagnostic::error(
            err.message.clone(),
            Span {
                start: 0,
                end: 0,
                line: err.line,
                col: err.col,
            },
        )
        .with_code("E0001")
    }
}

/// Convert a slice of `ParseError`s into a `Vec<Diagnostic>`.
pub fn parse_errors_to_diagnostics(errors: &[ParseError]) -> Vec<Diagnostic> {
    errors.iter().map(|e| Diagnostic::from(e.clone())).collect()
}


// ── Tests ───────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;

    fn span(line: usize, col: usize, start: usize, end: usize) -> Span {
        Span { start, end, line, col }
    }

    #[test]
    fn test_error_rendering() {
        let source = "let count = 0\nlet name: Int = \"hello\"\nview main = text \"hi\"";
        let diag = Diagnostic::error("expected Int, found String", span(2, 17, 31, 38));

        let output = render(&diag, source);
        assert!(output.contains("ERROR"));
        assert!(output.contains("2:17"));
        assert!(output.contains("^^^^^^^"));
        assert!(output.contains("expected Int, found String"));
    }

    #[test]
    fn test_warning_rendering() {
        let source = "let unused = 42\nview main = text \"hi\"";
        let diag = Diagnostic::warning("signal `unused` is never read", span(1, 5, 4, 10));

        let output = render(&diag, source);
        assert!(output.contains("WARNING"));
        assert!(output.contains("unused"));
    }

    #[test]
    fn test_with_suggestion() {
        let source = "let count = 0\nmatch status\n  Loading -> text \"...\"\n";
        let diag = Diagnostic::error("non-exhaustive match", span(2, 1, 14, 26))
            .with_code("E0004")
            .with_suggestion(span(2, 1, 14, 26), "Add missing variants", "  _ -> text \"fallback\"");

        let output = render(&diag, source);
        assert!(output.contains("[E0004]"));
        assert!(output.contains("Hint:"));
        assert!(output.contains("Add missing variants"));
    }

    #[test]
    fn test_sort_diagnostics() {
        let mut diags = vec![
            Diagnostic::warning("w1", span(3, 1, 30, 35)),
            Diagnostic::error("e1", span(1, 1, 0, 5)),
            Diagnostic::error("e2", span(5, 1, 50, 55)),
        ];
        sort_diagnostics(&mut diags);
        // Errors first, then by line
        assert_eq!(diags[0].message, "e1");
        assert_eq!(diags[1].message, "e2");
        assert_eq!(diags[2].message, "w1");
    }

    #[test]
    fn test_parse_error_to_diagnostic() {
        let err = ParseError {
            message: "unexpected token: Colon".to_string(),
            line: 5,
            col: 12,
            source_line: Some("let x = foo(bar:".to_string()),
        };
        let diag = Diagnostic::from(err);
        assert_eq!(diag.severity, Severity::Error);
        assert!(diag.message.contains("unexpected token: Colon"));
        assert_eq!(diag.span.line, 5);
        assert_eq!(diag.span.col, 12);
        assert_eq!(diag.code, Some("E0001".to_string()));
    }

    #[test]
    fn test_parse_errors_to_diagnostics_batch() {
        let errors = vec![
            ParseError {
                message: "error 1".to_string(),
                line: 1,
                col: 1,
                source_line: None,
            },
            ParseError {
                message: "error 2".to_string(),
                line: 3,
                col: 5,
                source_line: None,
            },
        ];
        let diags = parse_errors_to_diagnostics(&errors);
        assert_eq!(diags.len(), 2);
        assert_eq!(diags[0].message, "error 1");
        assert_eq!(diags[1].message, "error 2");
        assert_eq!(diags[1].span.line, 3);
    }

    // ── v0.10 Diagnostic Pipeline Tests ─────────────────────

    #[test]
    fn test_render_with_code() {
        let source = "let x = true + 1";
        let diag = Diagnostic::error("type mismatch", span(1, 9, 8, 16))
            .with_code("E0003");
        let output = render(&diag, source);
        assert!(output.contains("[E0003]"), "should include error code");
        assert!(output.contains("type mismatch"), "should include message");
    }

    #[test]
    fn test_render_with_label() {
        let source = "let x: Int = \"hello\"";
        let diag = Diagnostic::error("type mismatch", span(1, 14, 13, 20))
            .with_label(span(1, 8, 7, 10), "expected type declared here");
        let output = render(&diag, source);
        assert!(output.contains("ERROR"), "should be error severity");
        assert!(output.contains("type mismatch"), "should have message");
    }

    #[test]
    fn test_render_hint() {
        let source = "let x = 0";
        let diag = Diagnostic::hint("consider using a more descriptive name", span(1, 5, 4, 5));
        let output = render(&diag, source);
        assert!(output.contains("HINT") || output.contains("hint"), "should render as hint");
    }

    #[test]
    fn test_render_multiline_source() {
        let source = "let a = 0\nlet b = a + 1\nlet c = b * 2";
        let diag = Diagnostic::warning("unused signal", span(3, 5, 28, 29));
        let output = render(&diag, source);
        assert!(output.contains("3:"), "should reference line 3");
    }

    #[test]
    fn test_render_col_zero_edge() {
        let source = "let x = 0";
        let diag = Diagnostic::error("test", span(1, 0, 0, 3));
        // Should not panic with col=0
        let output = render(&diag, source);
        assert!(output.contains("ERROR"));
    }

    #[test]
    fn test_render_minimal_source() {
        // Minimal single-character source
        let source = " ";
        let diag = Diagnostic::error("unexpected end of input", span(1, 1, 0, 1));
        let output = render(&diag, source);
        assert!(output.contains("unexpected end of input"));
    }
}


