//! Wire error envelope. Mirrors `MizanError` in `frontends/mizan-base/src/index.ts`.
//!
//! Two envelope shapes are tolerated:
//!
//! - FastAPI: `{"error": {"code": "...", "message": "...", "details": ...}}`
//! - Django:  `{"error": true, "code": "...", "message": "...", "details": ...}`
//!
//! When neither shape parses, `code` falls back to `HTTP_<status>` and the
//! raw response body is the message.

use serde::Deserialize;
use serde_json::Value;


#[derive(Debug, Clone)]
pub struct MizanError {
    pub status: u16,
    pub code: String,
    pub message: String,
    pub details: Option<Value>,
    pub raw_body: String,
}


impl MizanError {
    pub fn from_response(status: u16, body: String) -> Self {
        let parsed = serde_json::from_str::<Envelope>(&body).ok();
        let (code, message, details) = match parsed {
            Some(Envelope::Fastapi { error }) => (
                error.code.unwrap_or_else(|| format!("HTTP_{status}")),
                error.message.unwrap_or_else(|| format!("Mizan call failed ({status})")),
                error.details,
            ),
            Some(Envelope::Django { code, message, details, .. }) => (
                code.unwrap_or_else(|| format!("HTTP_{status}")),
                message.unwrap_or_else(|| format!("Mizan call failed ({status})")),
                details,
            ),
            None => (
                format!("HTTP_{status}"),
                format!("Mizan call failed ({status})"),
                None,
            ),
        };
        Self { status, code, message, details, raw_body: body }
    }

    pub fn transport(message: impl Into<String>) -> Self {
        Self {
            status: 0,
            code: "TRANSPORT".to_string(),
            message: message.into(),
            details: None,
            raw_body: String::new(),
        }
    }
}


impl std::fmt::Display for MizanError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Mizan {} ({}): {}", self.status, self.code, self.message)
    }
}


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


#[derive(Deserialize)]
#[serde(untagged)]
enum Envelope {
    Fastapi { error: NestedError },
    Django {
        // Django form is `{"error": true, "code": ..., "message": ..., "details": ...}`.
        // `error` is a bool sentinel; the actual fields are siblings.
        error: bool,
        code: Option<String>,
        message: Option<String>,
        details: Option<Value>,
    },
}


#[derive(Deserialize)]
struct NestedError {
    code: Option<String>,
    message: Option<String>,
    details: Option<Value>,
}


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

    #[test]
    fn parses_fastapi_envelope() {
        let body = r#"{"error":{"code":"BAD_REQUEST","message":"oops","details":{"k":1}}}"#;
        let e = MizanError::from_response(400, body.to_string());
        assert_eq!(e.code, "BAD_REQUEST");
        assert_eq!(e.message, "oops");
        assert_eq!(e.details, Some(serde_json::json!({"k": 1})));
    }

    #[test]
    fn parses_django_envelope() {
        let body = r#"{"error":true,"code":"NOT_FOUND","message":"missing","details":null}"#;
        let e = MizanError::from_response(404, body.to_string());
        assert_eq!(e.code, "NOT_FOUND");
        assert_eq!(e.message, "missing");
    }

    #[test]
    fn falls_back_on_unparseable_body() {
        let e = MizanError::from_response(500, "Internal Server Error".to_string());
        assert_eq!(e.code, "HTTP_500");
        assert!(e.message.contains("500"));
    }
}
