use serde::{Deserialize, Serialize};

/// Severity of a diagnostic or log entry.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum Severity {
    Error,
    Warning,
    Info,
    Debug,
}

impl std::fmt::Display for Severity {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Severity::Error => write!(f, "error"),
            Severity::Warning => write!(f, "warning"),
            Severity::Info => write!(f, "info"),
            Severity::Debug => write!(f, "debug"),
        }
    }
}

/// Category of a diagnostic, used by callers to route or aggregate failures.
///
/// Each variant maps to exactly one [`ErrorKind`](crate::error::ErrorKind) variant,
/// so diagnostics never lose their failure domain when attached to an error.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum Category {
    /// A request-validation failure (bad arguments, limits exceeded).
    InvalidRequest,
    /// A compilation failure produced by the Roto compiler.
    Compile,
    /// A runtime contract violation (wrong entrypoint signature, output too large).
    RuntimeContract,
    /// A runtime execution failure (unexpected error during script execution).
    RuntimeExecution,
    /// A transport failure (gRPC server error, network issue).
    Transport,
    /// A resource-exhaustion failure (limits exceeded for bytes, temp space, etc.).
    ResourceExhausted,
    /// An operation cancelled by the caller or system.
    Cancelled,
    /// An internal SDK failure (bug, unexpected state).
    Internal,
    /// A panic from a script or module.
    Panic,
}

impl std::fmt::Display for Category {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Category::InvalidRequest => write!(f, "invalid-request"),
            Category::Compile => write!(f, "compile"),
            Category::RuntimeContract => write!(f, "runtime-contract"),
            Category::RuntimeExecution => write!(f, "runtime-execution"),
            Category::Transport => write!(f, "transport"),
            Category::ResourceExhausted => write!(f, "resource-exhausted"),
            Category::Cancelled => write!(f, "cancelled"),
            Category::Internal => write!(f, "internal"),
            Category::Panic => write!(f, "panic"),
        }
    }
}

/// A source location inside a script or module.
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct SourceLocation {
    /// File name or synthetic identifier (e.g. `"<inline>"`).
    pub file: String,
    /// 1-based line number, if known.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub line: Option<u32>,
    /// 1-based column number, if known.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub column: Option<u32>,
    /// Byte offsets into the source, if known.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub start_byte: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub end_byte: Option<usize>,
}

/// A single structured diagnostic emitted during a run.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Diagnostic {
    pub category: Category,
    pub severity: Severity,
    pub message: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub location: Option<SourceLocation>,
    /// Optional machine-readable code (e.g. "entrypoint-not-found").
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub code: Option<String>,
}

impl Diagnostic {
    pub fn new(category: Category, severity: Severity, message: impl Into<String>) -> Self {
        Self {
            category,
            severity,
            message: message.into(),
            location: None,
            code: None,
        }
    }

    pub fn with_location(mut self, location: SourceLocation) -> Self {
        self.location = Some(location);
        self
    }

    pub fn with_code(mut self, code: impl Into<String>) -> Self {
        self.code = Some(code.into());
        self
    }
}

/// A single structured log entry emitted during a run.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct LogEntry {
    pub severity: Severity,
    pub message: String,
}

/// Collects diagnostics and logs produced during a single run.
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct RunDiagnostics {
    pub diagnostics: Vec<Diagnostic>,
    pub logs: Vec<LogEntry>,
}

impl RunDiagnostics {
    pub fn is_empty(&self) -> bool {
        self.diagnostics.is_empty() && self.logs.is_empty()
    }

    pub fn has_errors(&self) -> bool {
        self.diagnostics
            .iter()
            .any(|d| d.severity == Severity::Error)
    }

    pub fn diagnostic(&mut self, d: Diagnostic) {
        self.diagnostics.push(d);
    }

    pub fn log(&mut self, l: LogEntry) {
        self.logs.push(l);
    }
}

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

    #[test]
    fn run_diagnostics_tracks_items() {
        let mut d = RunDiagnostics::default();
        assert!(d.is_empty());
        assert!(!d.has_errors());

        d.diagnostic(Diagnostic::new(Category::Compile, Severity::Error, "oops"));
        assert!(!d.is_empty());
        assert!(d.has_errors());

        d.log(LogEntry {
            severity: Severity::Info,
            message: "started".into(),
        });
        assert!(!d.is_empty());
        assert_eq!(d.diagnostics.len(), 1);
        assert_eq!(d.logs.len(), 1);
    }

    #[test]
    fn diagnostic_builder_methods() {
        let d = Diagnostic::new(Category::RuntimeContract, Severity::Warning, "check")
            .with_location(SourceLocation {
                file: "test.roto".into(),
                line: Some(1),
                column: Some(4),
                start_byte: Some(3),
                end_byte: Some(7),
            })
            .with_code("W001");

        assert_eq!(d.code.as_deref(), Some("W001"));
        assert_eq!(d.location.as_ref().unwrap().line, Some(1));
    }

    #[test]
    fn severity_and_category_display() {
        assert_eq!(format!("{}", Severity::Error), "error");
        assert_eq!(format!("{}", Category::Compile), "compile");
        assert_eq!(format!("{}", Category::InvalidRequest), "invalid-request");
        assert_eq!(format!("{}", Category::RuntimeContract), "runtime-contract");
        assert_eq!(
            format!("{}", Category::RuntimeExecution),
            "runtime-execution"
        );
        assert_eq!(format!("{}", Category::Transport), "transport");
        assert_eq!(
            format!("{}", Category::ResourceExhausted),
            "resource-exhausted"
        );
        assert_eq!(format!("{}", Category::Cancelled), "cancelled");
        assert_eq!(format!("{}", Category::Internal), "internal");
        assert_eq!(format!("{}", Category::Panic), "panic");
    }

    #[test]
    fn source_location_defaults() {
        let loc = SourceLocation::default();
        assert_eq!(loc.file, "");
        assert!(loc.line.is_none());
    }
}
