//! Typed application error enum.
//!
//! Replaces `Result<T, String>` across Tauri commands and backend modules.
//! Tauri 2.x requires `serde::Serialize` on command error types.
//! The custom `Serialize` impl produces `{ kind: "Database", detail: "..." }`
//! so the frontend receives structured, classifiable errors.

use serde::Serialize;

/// Structured error type for all Tauri commands and backend operations.
#[derive(Debug)]
pub enum AppError {
    Database { detail: String },
    Auth { detail: String },
    Filesystem { detail: String },
    Ipc { detail: String },
    NotFound { detail: String },
    Validation { detail: String },
    Sidecar { detail: String },
    Config { detail: String },
    Network { detail: String },
    Internal { detail: String },
}

impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            AppError::Database { detail } => write!(f, "Database: {detail}"),
            AppError::Auth { detail } => write!(f, "Auth: {detail}"),
            AppError::Filesystem { detail } => write!(f, "Filesystem: {detail}"),
            AppError::Ipc { detail } => write!(f, "IPC: {detail}"),
            AppError::NotFound { detail } => write!(f, "NotFound: {detail}"),
            AppError::Validation { detail } => write!(f, "Validation: {detail}"),
            AppError::Sidecar { detail } => write!(f, "Sidecar: {detail}"),
            AppError::Config { detail } => write!(f, "Config: {detail}"),
            AppError::Network { detail } => write!(f, "Network: {detail}"),
            AppError::Internal { detail } => write!(f, "Internal: {detail}"),
        }
    }
}

impl std::error::Error for AppError {}

/// Helper struct for structured JSON serialization.
/// Produces `{ "kind": "Database", "detail": "message" }`.
#[derive(Serialize)]
struct ErrorPayload<'a> {
    kind: &'a str,
    detail: &'a str,
}

impl Serialize for AppError {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::ser::Serializer,
    {
        let (kind, detail) = match self {
            AppError::Database { detail } => ("Database", detail.as_str()),
            AppError::Auth { detail } => ("Auth", detail.as_str()),
            AppError::Filesystem { detail } => ("Filesystem", detail.as_str()),
            AppError::Ipc { detail } => ("IPC", detail.as_str()),
            AppError::NotFound { detail } => ("NotFound", detail.as_str()),
            AppError::Validation { detail } => ("Validation", detail.as_str()),
            AppError::Sidecar { detail } => ("Sidecar", detail.as_str()),
            AppError::Config { detail } => ("Config", detail.as_str()),
            AppError::Network { detail } => ("Network", detail.as_str()),
            AppError::Internal { detail } => ("Internal", detail.as_str()),
        };
        ErrorPayload { kind, detail }.serialize(serializer)
    }
}

// --- Convenience conversions ---

impl From<rusqlite::Error> for AppError {
    fn from(e: rusqlite::Error) -> Self {
        AppError::Database {
            detail: e.to_string(),
        }
    }
}

impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self {
        AppError::Filesystem {
            detail: e.to_string(),
        }
    }
}

impl From<serde_json::Error> for AppError {
    fn from(e: serde_json::Error) -> Self {
        AppError::Validation {
            detail: e.to_string(),
        }
    }
}

/// Convert from legacy `String` errors (used at the boundary between
/// agor-core functions that still return `Result<T, String>` and
/// Tauri commands that now return `Result<T, AppError>`).
impl From<String> for AppError {
    fn from(s: String) -> Self {
        AppError::Internal { detail: s }
    }
}

impl From<&str> for AppError {
    fn from(s: &str) -> Self {
        AppError::Internal {
            detail: s.to_string(),
        }
    }
}

/// Convenience constructors for common error patterns.
impl AppError {
    pub fn database(detail: impl Into<String>) -> Self {
        AppError::Database {
            detail: detail.into(),
        }
    }

    pub fn auth(detail: impl Into<String>) -> Self {
        AppError::Auth {
            detail: detail.into(),
        }
    }

    pub fn filesystem(detail: impl Into<String>) -> Self {
        AppError::Filesystem {
            detail: detail.into(),
        }
    }

    pub fn not_found(detail: impl Into<String>) -> Self {
        AppError::NotFound {
            detail: detail.into(),
        }
    }

    pub fn validation(detail: impl Into<String>) -> Self {
        AppError::Validation {
            detail: detail.into(),
        }
    }

    pub fn sidecar(detail: impl Into<String>) -> Self {
        AppError::Sidecar {
            detail: detail.into(),
        }
    }

    pub fn config(detail: impl Into<String>) -> Self {
        AppError::Config {
            detail: detail.into(),
        }
    }

    pub fn network(detail: impl Into<String>) -> Self {
        AppError::Network {
            detail: detail.into(),
        }
    }

    pub fn internal(detail: impl Into<String>) -> Self {
        AppError::Internal {
            detail: detail.into(),
        }
    }
}

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

    #[test]
    fn test_display() {
        let e = AppError::database("connection refused");
        assert_eq!(e.to_string(), "Database: connection refused");
    }

    #[test]
    fn test_serialize_produces_structured_json() {
        let e = AppError::Database {
            detail: "table not found".to_string(),
        };
        let json = serde_json::to_value(&e).unwrap();
        assert_eq!(json["kind"], "Database");
        assert_eq!(json["detail"], "table not found");
    }

    #[test]
    fn test_from_rusqlite_error() {
        let e: AppError = rusqlite::Error::SqliteFailure(
            rusqlite::ffi::Error::new(1),
            Some("test".to_string()),
        )
        .into();
        assert!(matches!(e, AppError::Database { .. }));
    }

    #[test]
    fn test_from_io_error() {
        let e: AppError = std::io::Error::new(std::io::ErrorKind::NotFound, "gone").into();
        assert!(matches!(e, AppError::Filesystem { .. }));
    }

    #[test]
    fn test_from_string() {
        let e: AppError = "something went wrong".into();
        assert!(matches!(e, AppError::Internal { .. }));
    }

    #[test]
    fn test_from_serde_json_error() {
        let e: AppError = serde_json::from_str::<serde_json::Value>("not json")
            .unwrap_err()
            .into();
        assert!(matches!(e, AppError::Validation { .. }));
    }
}
