use serde::Serialize;
use thiserror::Error;

/// RFC 7807 Problem Details response body.
#[derive(Debug, Serialize)]
pub struct ProblemDetail {
    #[serde(rename = "type")]
    pub problem_type: String,
    pub title: String,
    pub status: u16,
    pub detail: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub instance: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub errors: Option<Vec<FieldError>>,
}

#[derive(Debug, Serialize)]
pub struct FieldError {
    pub field: String,
    pub message: String,
}

const ERROR_BASE_URL: &str = "https://api.kanoniv.com/errors";

#[derive(Error, Debug)]
pub enum CannonError {
    #[error("Database error: {0}")]
    Database(#[from] sqlx::Error),

    #[error("Configuration error: {0}")]
    Config(#[from] config::ConfigError),

    #[error("Redis error: {0}")]
    Redis(#[from] redis::RedisError),

    #[error("Not found: {0}")]
    NotFound(String),

    #[error("Invalid request: {0}")]
    BadRequest(String),

    #[error("Validation error: {0}")]
    ValidationError(String),

    #[error("Unauthorized")]
    Unauthorized,

    #[error("Forbidden: {0}")]
    Forbidden(String),

    #[error("Payment required: trial expired")]
    PaymentRequired,

    #[error("Usage limit exceeded: {0}")]
    QuotaExceeded(String),

    #[error("Conflict: {0}")]
    Conflict(String),

    #[error("Payload too large")]
    PayloadTooLarge,

    #[error("Internal error: {0}")]
    Internal(String),

    #[error(transparent)]
    Other(#[from] anyhow::Error),
}

impl CannonError {
    pub fn code(&self) -> &'static str {
        match self {
            Self::Database(_) => "DATABASE_ERROR",
            Self::Config(_) => "CONFIG_ERROR",
            Self::Redis(_) => "REDIS_ERROR",
            Self::NotFound(_) => "NOT_FOUND",
            Self::BadRequest(_) => "BAD_REQUEST",
            Self::ValidationError(_) => "VALIDATION_ERROR",
            Self::Unauthorized => "UNAUTHORIZED",
            Self::Forbidden(_) => "FORBIDDEN",
            Self::PaymentRequired => "PAYMENT_REQUIRED",
            Self::Conflict(_) => "CONFLICT",
            Self::QuotaExceeded(_) => "QUOTA_EXCEEDED",
            Self::PayloadTooLarge => "PAYLOAD_TOO_LARGE",
            Self::Internal(_) => "INTERNAL_ERROR",
            Self::Other(_) => "UNKNOWN_ERROR",
        }
    }

    pub fn problem_type_suffix(&self) -> &'static str {
        match self {
            Self::Database(_) | Self::Internal(_) | Self::Config(_)
            | Self::Redis(_) | Self::Other(_) => "internal-error",
            Self::NotFound(_) => "not-found",
            Self::BadRequest(_) => "bad-request",
            Self::ValidationError(_) => "validation-error",
            Self::Unauthorized => "unauthorized",
            Self::Forbidden(_) => "forbidden",
            Self::PaymentRequired => "payment-required",
            Self::Conflict(_) => "conflict",
            Self::QuotaExceeded(_) => "quota-exceeded",
            Self::PayloadTooLarge => "payload-too-large",
        }
    }

    pub fn problem_title(&self) -> &'static str {
        match self {
            Self::Database(_) | Self::Internal(_) | Self::Config(_)
            | Self::Redis(_) | Self::Other(_) => "Internal server error",
            Self::NotFound(_) => "Not found",
            Self::BadRequest(_) => "Bad request",
            Self::ValidationError(_) => "Validation failed",
            Self::Unauthorized => "Unauthorized",
            Self::Forbidden(_) => "Forbidden",
            Self::PaymentRequired => "Payment required",
            Self::Conflict(_) => "Conflict",
            Self::QuotaExceeded(_) => "Quota exceeded",
            Self::PayloadTooLarge => "Payload too large",
        }
    }

    pub fn to_problem_detail(&self, instance: Option<String>) -> ProblemDetail {
        let (status, detail) = match self {
            Self::NotFound(m) => (404u16, m.clone()),
            Self::Unauthorized => (401, "Missing or invalid credentials".to_string()),
            Self::Forbidden(m) => (403, m.clone()),
            Self::PaymentRequired => (402, "Trial expired".to_string()),
            Self::Conflict(m) => (409, m.clone()),
            Self::QuotaExceeded(m) => (429, m.clone()),
            Self::PayloadTooLarge => (413, "Request payload exceeds size limit".to_string()),
            Self::BadRequest(m) | Self::ValidationError(m) => (400, m.clone()),
            Self::Database(e) => {
                tracing::error!("Database error: {}", e);
                (500, "A database error occurred".to_string())
            },
            Self::Internal(m) => {
                tracing::error!("Internal error: {}", m);
                (500, "An internal server error occurred".to_string())
            },
            Self::Config(e) => {
                tracing::error!("Config error: {}", e);
                (500, "An internal server error occurred".to_string())
            },
            Self::Redis(e) => {
                tracing::error!("Redis error: {}", e);
                (500, "An internal server error occurred".to_string())
            },
            Self::Other(e) => {
                tracing::error!("Unknown error: {:?}", e);
                (500, "An unknown error occurred".to_string())
            },
        };

        ProblemDetail {
            problem_type: format!("{}/{}", ERROR_BASE_URL, self.problem_type_suffix()),
            title: self.problem_title().to_string(),
            status,
            detail,
            instance,
            errors: None,
        }
    }
}

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

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

    #[test]
    fn test_problem_detail_not_found() {
        let err = CannonError::NotFound("User abc not found".into());
        let pd = err.to_problem_detail(None);

        assert_eq!(pd.status, 404);
        assert_eq!(pd.title, "Not found");
        assert_eq!(pd.detail, "User abc not found");
        assert_eq!(pd.problem_type, "https://api.kanoniv.com/errors/not-found");
        assert!(pd.instance.is_none());
        assert!(pd.errors.is_none());
    }

    #[test]
    fn test_problem_detail_bad_request() {
        let err = CannonError::BadRequest("Missing field 'name'".into());
        let pd = err.to_problem_detail(Some("/v1/specs".into()));

        assert_eq!(pd.status, 400);
        assert_eq!(pd.title, "Bad request");
        assert_eq!(pd.detail, "Missing field 'name'");
        assert_eq!(pd.instance.as_deref(), Some("/v1/specs"));
    }

    #[test]
    fn test_problem_detail_validation_error() {
        let err = CannonError::ValidationError("Field 'email' is required".into());
        let pd = err.to_problem_detail(None);

        assert_eq!(pd.status, 400);
        assert_eq!(pd.title, "Validation failed");
        assert_eq!(pd.problem_type, "https://api.kanoniv.com/errors/validation-error");
    }

    #[test]
    fn test_problem_detail_unauthorized() {
        let err = CannonError::Unauthorized;
        let pd = err.to_problem_detail(None);

        assert_eq!(pd.status, 401);
        assert_eq!(pd.title, "Unauthorized");
        assert_eq!(pd.detail, "Missing or invalid credentials");
    }

    #[test]
    fn test_problem_detail_forbidden() {
        let err = CannonError::Forbidden("Admin role required".into());
        let pd = err.to_problem_detail(None);

        assert_eq!(pd.status, 403);
        assert_eq!(pd.title, "Forbidden");
        assert_eq!(pd.detail, "Admin role required");
    }

    #[test]
    fn test_problem_detail_payment_required() {
        let err = CannonError::PaymentRequired;
        let pd = err.to_problem_detail(None);

        assert_eq!(pd.status, 402);
        assert_eq!(pd.title, "Payment required");
        assert_eq!(pd.detail, "Trial expired");
    }

    #[test]
    fn test_problem_detail_conflict() {
        let err = CannonError::Conflict("Entity already exists".into());
        let pd = err.to_problem_detail(None);

        assert_eq!(pd.status, 409);
        assert_eq!(pd.title, "Conflict");
        assert_eq!(pd.detail, "Entity already exists");
    }

    #[test]
    fn test_problem_detail_quota_exceeded() {
        let err = CannonError::QuotaExceeded("Identity limit reached".into());
        let pd = err.to_problem_detail(None);

        assert_eq!(pd.status, 429);
        assert_eq!(pd.title, "Quota exceeded");
        assert_eq!(pd.detail, "Identity limit reached");
    }

    #[test]
    fn test_problem_detail_payload_too_large() {
        let err = CannonError::PayloadTooLarge;
        let pd = err.to_problem_detail(None);

        assert_eq!(pd.status, 413);
        assert_eq!(pd.title, "Payload too large");
        assert_eq!(pd.detail, "Request payload exceeds size limit");
    }

    #[test]
    fn test_problem_detail_internal_error() {
        let err = CannonError::Internal("something broke".into());
        let pd = err.to_problem_detail(None);

        assert_eq!(pd.status, 500);
        assert_eq!(pd.title, "Internal server error");
        assert_eq!(pd.detail, "An internal server error occurred");
        assert_eq!(pd.problem_type, "https://api.kanoniv.com/errors/internal-error");
    }

    #[test]
    fn test_problem_detail_other_anyhow() {
        let err = CannonError::Other(anyhow::anyhow!("unknown"));
        let pd = err.to_problem_detail(None);

        assert_eq!(pd.status, 500);
        assert_eq!(pd.detail, "An unknown error occurred");
    }

    #[test]
    fn test_problem_type_suffix_all_variants() {
        assert_eq!(CannonError::NotFound("".into()).problem_type_suffix(), "not-found");
        assert_eq!(CannonError::BadRequest("".into()).problem_type_suffix(), "bad-request");
        assert_eq!(CannonError::ValidationError("".into()).problem_type_suffix(), "validation-error");
        assert_eq!(CannonError::Unauthorized.problem_type_suffix(), "unauthorized");
        assert_eq!(CannonError::Forbidden("".into()).problem_type_suffix(), "forbidden");
        assert_eq!(CannonError::PaymentRequired.problem_type_suffix(), "payment-required");
        assert_eq!(CannonError::Conflict("".into()).problem_type_suffix(), "conflict");
        assert_eq!(CannonError::QuotaExceeded("".into()).problem_type_suffix(), "quota-exceeded");
        assert_eq!(CannonError::PayloadTooLarge.problem_type_suffix(), "payload-too-large");
        assert_eq!(CannonError::Internal("".into()).problem_type_suffix(), "internal-error");
        assert_eq!(CannonError::Other(anyhow::anyhow!("")).problem_type_suffix(), "internal-error");
    }

    #[test]
    fn test_problem_title_all_variants() {
        assert_eq!(CannonError::NotFound("".into()).problem_title(), "Not found");
        assert_eq!(CannonError::BadRequest("".into()).problem_title(), "Bad request");
        assert_eq!(CannonError::ValidationError("".into()).problem_title(), "Validation failed");
        assert_eq!(CannonError::Unauthorized.problem_title(), "Unauthorized");
        assert_eq!(CannonError::Forbidden("".into()).problem_title(), "Forbidden");
        assert_eq!(CannonError::PaymentRequired.problem_title(), "Payment required");
        assert_eq!(CannonError::Conflict("".into()).problem_title(), "Conflict");
        assert_eq!(CannonError::QuotaExceeded("".into()).problem_title(), "Quota exceeded");
        assert_eq!(CannonError::PayloadTooLarge.problem_title(), "Payload too large");
        assert_eq!(CannonError::Internal("".into()).problem_title(), "Internal server error");
    }

    #[test]
    fn test_problem_detail_json_serialization() {
        let pd = ProblemDetail {
            problem_type: "https://api.kanoniv.com/errors/not-found".into(),
            title: "Not found".into(),
            status: 404,
            detail: "Spec 'abc' not found".into(),
            instance: Some("/v1/specs/abc".into()),
            errors: None,
        };

        let json: serde_json::Value = serde_json::to_value(&pd).unwrap();
        assert_eq!(json["type"], "https://api.kanoniv.com/errors/not-found");
        assert_eq!(json["title"], "Not found");
        assert_eq!(json["status"], 404);
        assert_eq!(json["detail"], "Spec 'abc' not found");
        assert_eq!(json["instance"], "/v1/specs/abc");
        // errors should be omitted when None
        assert!(json.get("errors").is_none());
    }

    #[test]
    fn test_problem_detail_json_omits_none_fields() {
        let pd = ProblemDetail {
            problem_type: "https://api.kanoniv.com/errors/bad-request".into(),
            title: "Bad request".into(),
            status: 400,
            detail: "Invalid input".into(),
            instance: None,
            errors: None,
        };

        let json: serde_json::Value = serde_json::to_value(&pd).unwrap();
        assert!(json.get("instance").is_none(), "instance should be omitted when None");
        assert!(json.get("errors").is_none(), "errors should be omitted when None");
    }

    #[test]
    fn test_problem_detail_json_with_field_errors() {
        let pd = ProblemDetail {
            problem_type: "https://api.kanoniv.com/errors/validation-error".into(),
            title: "Validation failed".into(),
            status: 400,
            detail: "Request body has validation errors".into(),
            instance: None,
            errors: Some(vec![
                FieldError { field: "name".into(), message: "required".into() },
                FieldError { field: "email".into(), message: "invalid format".into() },
            ]),
        };

        let json: serde_json::Value = serde_json::to_value(&pd).unwrap();
        let errors = json["errors"].as_array().unwrap();
        assert_eq!(errors.len(), 2);
        assert_eq!(errors[0]["field"], "name");
        assert_eq!(errors[0]["message"], "required");
        assert_eq!(errors[1]["field"], "email");
        assert_eq!(errors[1]["message"], "invalid format");
    }

    #[test]
    fn test_problem_detail_preserves_user_message() {
        // Verify that user-facing variants preserve the original message as detail
        let msg = "Spec with name 'customer_360' already exists";
        let err = CannonError::Conflict(msg.to_string());
        let pd = err.to_problem_detail(None);
        assert_eq!(pd.detail, msg);
    }

    #[test]
    fn test_problem_detail_hides_internal_details() {
        // Internal errors should NOT leak internal error messages to the client
        let err = CannonError::Internal("connection pool exhausted at line 42".into());
        let pd = err.to_problem_detail(None);
        assert_eq!(pd.detail, "An internal server error occurred");
        assert!(!pd.detail.contains("connection pool"));
    }
}
