use std::error::Error as StdError;
use std::fmt::{Display, Formatter};

use crate::diagnostic::{Category, Diagnostic, Severity};

/// The kind of error that occurred.
///
/// This enum distinguishes between request, compile, runtime, transport,
/// and internal failures. Each variant represents a stable failure domain
/// that callers can rely on for error handling and routing.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ErrorKind {
    /// Invalid request: bad arguments, missing required fields, or quota exceeded.
    InvalidRequest,
    /// Compile failure: Roto compilation error.
    Compile,
    /// Runtime contract violation: wrong entrypoint signature, output too large, etc.
    RuntimeContract,
    /// Runtime execution error: unexpected failure during script execution.
    RuntimeExecution,
    /// Transport error: gRPC server failure, network issue, etc.
    Transport,
    /// Resource exhausted: limits exceeded (bytes, temp space, etc.).
    ResourceExhausted,
    /// Operation cancelled by caller or system.
    Cancelled,
    /// Internal SDK error: bug in the SDK or unexpected state.
    Internal,
    /// Panic: unrecoverable error from a panicked script or module.
    Panic,
}

impl ErrorKind {
    /// Map `ErrorKind` to a structured diagnostic category.
    pub fn category(self) -> Category {
        match self {
            ErrorKind::InvalidRequest => Category::InvalidRequest,
            ErrorKind::Compile => Category::Compile,
            ErrorKind::RuntimeContract => Category::RuntimeContract,
            ErrorKind::RuntimeExecution => Category::RuntimeExecution,
            ErrorKind::Transport => Category::Transport,
            ErrorKind::ResourceExhausted => Category::ResourceExhausted,
            ErrorKind::Cancelled => Category::Cancelled,
            ErrorKind::Internal => Category::Internal,
            ErrorKind::Panic => Category::Panic,
        }
    }

    /// Returns a stable machine-readable code string for this error kind.
    pub fn code_str(self) -> &'static str {
        match self {
            ErrorKind::InvalidRequest => "invalid-request",
            ErrorKind::Compile => "compile",
            ErrorKind::RuntimeContract => "runtime-contract",
            ErrorKind::RuntimeExecution => "runtime-execution",
            ErrorKind::Transport => "transport",
            ErrorKind::ResourceExhausted => "resource-exhausted",
            ErrorKind::Cancelled => "cancelled",
            ErrorKind::Internal => "internal",
            ErrorKind::Panic => "panic",
        }
    }
}

#[derive(Clone, Debug)]
pub struct Error {
    kind: ErrorKind,
    message: String,
    diagnostics: Vec<Diagnostic>,
}

impl Error {
    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
        Self {
            kind,
            message: message.into(),
            diagnostics: Vec::new(),
        }
    }

    /// Create an error for invalid requests (bad arguments, missing fields).
    pub fn invalid_request(message: impl Into<String>) -> Self {
        Self::new(ErrorKind::InvalidRequest, message)
    }

    /// Create an error for compile failures.
    pub fn compile(message: impl Into<String>) -> Self {
        Self::new(ErrorKind::Compile, message)
    }

    /// Create an error for runtime contract violations.
    pub fn runtime_contract(message: impl Into<String>) -> Self {
        Self::new(ErrorKind::RuntimeContract, message)
    }

    /// Create an error for runtime execution failures.
    pub fn runtime_execution(message: impl Into<String>) -> Self {
        Self::new(ErrorKind::RuntimeExecution, message)
    }

    /// Create an error for transport failures.
    pub fn transport(message: impl Into<String>) -> Self {
        Self::new(ErrorKind::Transport, message)
    }

    /// Create an error for resource exhaustion (limits exceeded).
    pub fn resource_exhausted(message: impl Into<String>) -> Self {
        Self::new(ErrorKind::ResourceExhausted, message)
    }

    /// Create an error for cancelled operations.
    pub fn cancelled(message: impl Into<String>) -> Self {
        Self::new(ErrorKind::Cancelled, message)
    }

    /// Create an error for internal SDK failures.
    pub fn internal(message: impl Into<String>) -> Self {
        Self::new(ErrorKind::Internal, message)
    }

    /// Create an error for panics from scripts or modules.
    pub fn panic(message: impl Into<String>) -> Self {
        Self::new(ErrorKind::Panic, message)
    }

    /// Returns the kind of this error.
    pub fn kind(&self) -> ErrorKind {
        self.kind
    }

    pub fn message_str(&self) -> &str {
        &self.message
    }

    pub fn diagnostics(&self) -> &[Diagnostic] {
        &self.diagnostics
    }

    pub fn into_diagnostics(self) -> Vec<Diagnostic> {
        self.diagnostics
    }

    /// Add a structured diagnostic to this error.
    pub fn with_diagnostic(mut self, d: Diagnostic) -> Self {
        self.diagnostics.push(d);
        self
    }

    /// Append multiple structured diagnostics to this error.
    pub fn with_diagnostics(mut self, ds: impl IntoIterator<Item = Diagnostic>) -> Self {
        self.diagnostics.extend(ds);
        self
    }

    /// Add a diagnostic derived from the error's own code and message.
    pub fn with_primary_diagnostic(self, code: impl Into<String>) -> Self {
        let d =
            Diagnostic::new(self.kind.category(), Severity::Error, &self.message).with_code(code);
        self.with_diagnostic(d)
    }
}

impl Display for Error {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}: {}", self.kind.code_str(), self.message)?;
        if !self.diagnostics.is_empty() {
            write!(f, " [{} diagnostic(s)]", self.diagnostics.len())?;
            for d in &self.diagnostics {
                write!(f, "\n  {}: {}", d.severity, d.message)?;
                if let Some(ref code) = d.code {
                    write!(f, " ({})", code)?;
                }
            }
        }
        Ok(())
    }
}

impl StdError for Error {}

impl From<std::io::Error> for Error {
    fn from(value: std::io::Error) -> Self {
        Self::internal(value.to_string())
    }
}

impl From<serde_json::Error> for Error {
    fn from(value: serde_json::Error) -> Self {
        Self::internal(value.to_string())
    }
}

pub type Result<T> = std::result::Result<T, Error>;

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

    #[test]
    fn error_kind_category_roundtrip() {
        assert_eq!(
            ErrorKind::InvalidRequest.category(),
            Category::InvalidRequest
        );
        assert_eq!(ErrorKind::Compile.category(), Category::Compile);
        assert_eq!(
            ErrorKind::RuntimeContract.category(),
            Category::RuntimeContract
        );
        assert_eq!(
            ErrorKind::RuntimeExecution.category(),
            Category::RuntimeExecution
        );
        assert_eq!(ErrorKind::Transport.category(), Category::Transport);
        assert_eq!(
            ErrorKind::ResourceExhausted.category(),
            Category::ResourceExhausted
        );
        assert_eq!(ErrorKind::Cancelled.category(), Category::Cancelled);
        assert_eq!(ErrorKind::Internal.category(), Category::Internal);
        assert_eq!(ErrorKind::Panic.category(), Category::Panic);
    }

    #[test]
    fn error_kind_code_str_matches_expectations() {
        assert_eq!(ErrorKind::InvalidRequest.code_str(), "invalid-request");
        assert_eq!(ErrorKind::Compile.code_str(), "compile");
        assert_eq!(ErrorKind::RuntimeContract.code_str(), "runtime-contract");
        assert_eq!(ErrorKind::RuntimeExecution.code_str(), "runtime-execution");
        assert_eq!(ErrorKind::Transport.code_str(), "transport");
        assert_eq!(
            ErrorKind::ResourceExhausted.code_str(),
            "resource-exhausted"
        );
        assert_eq!(ErrorKind::Cancelled.code_str(), "cancelled");
        assert_eq!(ErrorKind::Internal.code_str(), "internal");
        assert_eq!(ErrorKind::Panic.code_str(), "panic");
    }

    #[test]
    fn error_constructors_set_correct_kind() {
        assert_eq!(
            Error::invalid_request("msg").kind(),
            ErrorKind::InvalidRequest
        );
        assert_eq!(Error::compile("msg").kind(), ErrorKind::Compile);
        assert_eq!(
            Error::runtime_contract("msg").kind(),
            ErrorKind::RuntimeContract
        );
        assert_eq!(
            Error::runtime_execution("msg").kind(),
            ErrorKind::RuntimeExecution
        );
        assert_eq!(Error::transport("msg").kind(), ErrorKind::Transport);
        assert_eq!(
            Error::resource_exhausted("msg").kind(),
            ErrorKind::ResourceExhausted
        );
        assert_eq!(Error::cancelled("msg").kind(), ErrorKind::Cancelled);
        assert_eq!(Error::internal("msg").kind(), ErrorKind::Internal);
        assert_eq!(Error::panic("msg").kind(), ErrorKind::Panic);
    }

    #[test]
    fn with_primary_diagnostic_attaches_correct_category() {
        let err =
            Error::compile("compilation failed").with_primary_diagnostic("roto-compile-error");
        assert_eq!(err.kind(), ErrorKind::Compile);
        assert_eq!(err.diagnostics().len(), 1);
        let d = &err.diagnostics()[0];
        assert_eq!(d.category, Category::Compile);
        assert_eq!(d.severity, Severity::Error);
        assert_eq!(d.code.as_deref(), Some("roto-compile-error"));
        assert_eq!(d.message, "compilation failed");
    }

    #[test]
    fn display_format_includes_kind_prefix() {
        let err = Error::invalid_request("bad input");
        let displayed = format!("{}", err);
        assert!(displayed.starts_with("invalid-request: bad input"));
    }

    #[test]
    fn display_format_includes_diagnostic_count() {
        let err = Error::runtime_contract("sig mismatch")
            .with_diagnostic(Diagnostic::new(
                Category::RuntimeContract,
                Severity::Error,
                "detail",
            ))
            .with_diagnostic(Diagnostic::new(
                Category::RuntimeContract,
                Severity::Warning,
                "hint",
            ));
        let displayed = format!("{}", err);
        assert!(displayed.contains("[2 diagnostic(s)]"));
    }

    #[test]
    fn multiple_diagnostics_attached() {
        let err = Error::resource_exhausted("too large").with_diagnostics(vec![
            Diagnostic::new(Category::ResourceExhausted, Severity::Error, "a"),
            Diagnostic::new(Category::Internal, Severity::Info, "b"),
        ]);
        assert_eq!(err.diagnostics().len(), 2);
        assert_eq!(err.diagnostics()[0].category, Category::ResourceExhausted);
        assert_eq!(err.diagnostics()[1].category, Category::Internal);
    }
}
