//! Error post-processing module for sanitizing and formatting errors.
//!
//! This module provides utilities for:
//! - Categorizing errors by type
//! - Sanitizing SQL queries from error messages
//! - Converting technical errors to user-friendly messages
//! - Generating structured error responses with trace IDs and metadata

use actix_web::http::StatusCode;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value, json};
use std::fmt;

pub mod sql_sanitizer;
pub mod sqlx_parser;

/// Categorizes errors by their nature for proper handling and response formatting.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ErrorCategory {
    /// Database constraint violations (unique, foreign key, check, etc.)
    DatabaseConstraint,
    /// Database connection issues (pool exhaustion, timeouts, disconnects)
    DatabaseConnection,
    /// SQL syntax errors or invalid queries
    QuerySyntax,
    /// Authentication failures
    Authentication,
    /// Authorization/permission issues
    Authorization,
    /// Requested resource not found
    NotFound,
    /// Input validation errors
    Validation,
    /// Unexpected internal errors
    Internal,
    /// Rate limiting or throttling
    RateLimiting,
}

impl fmt::Display for ErrorCategory {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ErrorCategory::DatabaseConstraint => write!(f, "database_constraint"),
            ErrorCategory::DatabaseConnection => write!(f, "database_connection"),
            ErrorCategory::QuerySyntax => write!(f, "query_syntax"),
            ErrorCategory::Authentication => write!(f, "authentication"),
            ErrorCategory::Authorization => write!(f, "authorization"),
            ErrorCategory::NotFound => write!(f, "not_found"),
            ErrorCategory::Validation => write!(f, "validation"),
            ErrorCategory::Internal => write!(f, "internal"),
            ErrorCategory::RateLimiting => write!(f, "rate_limiting"),
        }
    }
}

/// A processed, sanitized error ready for client response.
///
/// This structure ensures:
/// - No SQL queries are exposed
/// - User-friendly messages
/// - Proper HTTP status codes
/// - Actionable hints when available
/// - Structured metadata for debugging
#[derive(Debug, Clone)]
pub struct ProcessedError {
    /// Error category for classification
    pub category: ErrorCategory,
    /// HTTP status code to return
    pub status_code: StatusCode,
    /// Machine-readable error code
    pub error_code: &'static str,
    /// User-friendly error message
    pub message: String,
    /// Optional actionable hint for resolving the error
    pub hint: Option<String>,
    /// Trace ID for correlating with server logs
    pub trace_id: String,
    /// Additional safe metadata (no SQL or sensitive data)
    pub metadata: Map<String, Value>,
}

impl ProcessedError {
    /// Creates a new ProcessedError with the given parameters.
    pub fn new(
        category: ErrorCategory,
        status_code: StatusCode,
        error_code: &'static str,
        message: impl Into<String>,
        trace_id: impl Into<String>,
    ) -> Self {
        Self {
            category,
            status_code,
            error_code,
            message: message.into(),
            hint: None,
            trace_id: trace_id.into(),
            metadata: Map::new(),
        }
    }

    /// Sets an actionable hint for this error.
    pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
        self.hint = Some(hint.into());
        self
    }

    /// Adds a metadata key-value pair.
    pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
        self.metadata.insert(key.into(), value);
        self
    }

    /// Adds multiple metadata entries at once.
    pub fn with_metadata_map(mut self, metadata: Map<String, Value>) -> Self {
        self.metadata.extend(metadata);
        self
    }

    /// Converts this error into a JSON response value.
    pub fn to_json(&self) -> Value {
        let mut details = self.metadata.clone();
        details.insert("category".to_string(), json!(self.category));
        details.insert("trace_id".to_string(), json!(self.trace_id));

        let mut response = json!({
            "status": "error",
            "code": self.error_code,
            "message": self.message,
            "details": details,
            "trace_id": self.trace_id,
        });

        if let Some(hint) = &self.hint {
            response["hint"] = json!(hint);
        }

        response
    }
}

impl fmt::Display for ProcessedError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "[{}] {}: {}",
            self.error_code, self.category, self.message
        )
    }
}

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

/// Generates a trace ID for error correlation.
///
/// This creates a short, URL-safe identifier for tracking errors
/// across logs and responses.
pub fn generate_trace_id() -> String {
    use std::time::{SystemTime, UNIX_EPOCH};
    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_micros();
    format!("{:x}", timestamp & 0xFFFFFFFF)
}

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

    #[test]
    fn test_error_category_serialization() {
        let category = ErrorCategory::DatabaseConstraint;
        let json = serde_json::to_string(&category).unwrap();
        assert_eq!(json, "\"database_constraint\"");
    }

    #[test]
    fn test_processed_error_builder() {
        let error = ProcessedError::new(
            ErrorCategory::QuerySyntax,
            StatusCode::BAD_REQUEST,
            "invalid_query",
            "Invalid query syntax",
            "trace123",
        )
        .with_hint("Check your column names")
        .with_metadata("table", json!("users"));

        assert_eq!(error.category, ErrorCategory::QuerySyntax);
        assert_eq!(error.status_code, StatusCode::BAD_REQUEST);
        assert_eq!(error.error_code, "invalid_query");
        assert_eq!(error.hint, Some("Check your column names".to_string()));
        assert_eq!(error.metadata.get("table").unwrap(), "users");
    }

    #[test]
    fn test_processed_error_to_json() {
        let error = ProcessedError::new(
            ErrorCategory::DatabaseConstraint,
            StatusCode::CONFLICT,
            "unique_violation",
            "A record with this value already exists",
            "abc123",
        )
        .with_hint("Try a different value")
        .with_metadata("constraint", json!("users_email_key"));

        let json = error.to_json();
        assert_eq!(json["status"], "error");
        assert_eq!(json["code"], "unique_violation");
        assert_eq!(json["hint"], "Try a different value");
        assert_eq!(json["trace_id"], "abc123");
        assert_eq!(json["details"]["category"], "database_constraint");
        assert_eq!(json["details"]["constraint"], "users_email_key");
    }

    #[test]
    fn test_generate_trace_id() {
        let trace_id = generate_trace_id();
        assert!(!trace_id.is_empty());
        assert!(trace_id.len() <= 16);
    }
}
