use axum::{
    Json,
    http::StatusCode,
    response::{IntoResponse, Response},
};
use crank_mapping::MappingError;
use crank_registry::RegistryError;
use crank_runtime::RuntimeError;
use crank_schema::SchemaError;
use serde_json::{Value, json};
use thiserror::Error;
use tracing::{error, warn};

use crate::storage::StorageError;

#[derive(Debug, Error)]
pub enum ApiError {
    #[error("{message}")]
    Unauthorized {
        message: String,
        context: Option<Value>,
    },
    #[error("{message}")]
    Forbidden {
        message: String,
        context: Option<Value>,
    },
    #[error("{message}")]
    Validation {
        message: String,
        context: Option<Value>,
    },
    #[error("{message}")]
    NotFound {
        message: String,
        context: Option<Value>,
    },
    #[error("{message}")]
    Conflict {
        message: String,
        context: Option<Value>,
    },
    #[error("{message}")]
    RateLimited {
        message: String,
        context: Option<Value>,
    },
    #[error("{message}")]
    Internal {
        message: String,
        context: Option<Value>,
    },
}

impl ApiError {
    pub fn unauthorized(message: impl Into<String>) -> Self {
        Self::Unauthorized {
            message: message.into(),
            context: None,
        }
    }

    pub fn forbidden(message: impl Into<String>) -> Self {
        Self::Forbidden {
            message: message.into(),
            context: None,
        }
    }

    pub fn validation(message: impl Into<String>) -> Self {
        Self::Validation {
            message: message.into(),
            context: None,
        }
    }

    pub fn internal(message: impl Into<String>) -> Self {
        Self::Internal {
            message: message.into(),
            context: None,
        }
    }

    pub(crate) fn rate_limited_with_context(message: impl Into<String>, context: Value) -> Self {
        Self::RateLimited {
            message: message.into(),
            context: Some(context),
        }
    }

    pub(crate) fn validation_with_context(message: impl Into<String>, context: Value) -> Self {
        Self::Validation {
            message: message.into(),
            context: Some(context),
        }
    }

    pub(crate) fn not_found_with_context(message: impl Into<String>, context: Value) -> Self {
        Self::NotFound {
            message: message.into(),
            context: Some(context),
        }
    }

    pub(crate) fn conflict_with_context(message: impl Into<String>, context: Value) -> Self {
        Self::Conflict {
            message: message.into(),
            context: Some(context),
        }
    }

    pub(crate) fn forbidden_with_context(message: impl Into<String>, context: Value) -> Self {
        Self::Forbidden {
            message: message.into(),
            context: Some(context),
        }
    }

    fn status_code(&self) -> StatusCode {
        match self {
            Self::Unauthorized { .. } => StatusCode::UNAUTHORIZED,
            Self::Forbidden { .. } => StatusCode::FORBIDDEN,
            Self::Validation { .. } => StatusCode::BAD_REQUEST,
            Self::NotFound { .. } => StatusCode::NOT_FOUND,
            Self::Conflict { .. } => StatusCode::CONFLICT,
            Self::RateLimited { .. } => StatusCode::TOO_MANY_REQUESTS,
            Self::Internal { .. } => StatusCode::INTERNAL_SERVER_ERROR,
        }
    }

    fn code(&self) -> &'static str {
        match self {
            Self::Unauthorized { .. } => "unauthorized",
            Self::Forbidden { .. } => "forbidden",
            Self::Validation { .. } => "validation_error",
            Self::NotFound { .. } => "not_found",
            Self::Conflict { .. } => "conflict",
            Self::RateLimited { .. } => "rate_limited",
            Self::Internal { .. } => "internal_error",
        }
    }
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        match &self {
            Self::Internal { message, .. } => {
                error!(error_code = self.code(), error_message = %message)
            }
            Self::Unauthorized { message, .. }
            | Self::Forbidden { message, .. }
            | Self::Validation { message, .. }
            | Self::NotFound { message, .. }
            | Self::Conflict { message, .. }
            | Self::RateLimited { message, .. } => {
                warn!(error_code = self.code(), error_message = %message)
            }
        }

        let mut error = json!({
            "code": self.code(),
            "message": self.to_string(),
        });
        if let Some(context) = self.context() {
            error["context"] = context;
        }

        let body = Json(json!({
            "error": error
        }));

        (self.status_code(), body).into_response()
    }
}

impl ApiError {
    fn context(&self) -> Option<Value> {
        match self {
            Self::Unauthorized { context, .. }
            | Self::Forbidden { context, .. }
            | Self::Validation { context, .. }
            | Self::NotFound { context, .. }
            | Self::Conflict { context, .. }
            | Self::RateLimited { context, .. }
            | Self::Internal { context, .. } => context.clone(),
        }
    }
}

impl From<RegistryError> for ApiError {
    fn from(value: RegistryError) -> Self {
        match value {
            RegistryError::WorkspaceNotFound { workspace_id } => Self::not_found_with_context(
                format!("workspace {workspace_id} was not found"),
                json!({ "workspace_id": workspace_id }),
            ),
            RegistryError::UserNotFound { user_id } => Self::not_found_with_context(
                format!("user {user_id} was not found"),
                json!({ "user_id": user_id }),
            ),
            RegistryError::MembershipNotFound {
                workspace_id,
                user_id,
            } => Self::not_found_with_context(
                format!("membership for user {user_id} in workspace {workspace_id} was not found"),
                json!({
                    "workspace_id": workspace_id,
                    "user_id": user_id,
                }),
            ),
            RegistryError::AgentNotFound { agent_id } => Self::not_found_with_context(
                format!("agent {agent_id} was not found"),
                json!({ "agent_id": agent_id }),
            ),
            RegistryError::InvitationNotFound { invitation_id } => Self::not_found_with_context(
                format!("invitation {invitation_id} was not found"),
                json!({ "invitation_id": invitation_id }),
            ),
            RegistryError::PlatformApiKeyNotFound { key_id } => Self::not_found_with_context(
                format!("platform api key {key_id} was not found"),
                json!({ "key_id": key_id }),
            ),
            RegistryError::SecretNotFound { secret_id } => Self::not_found_with_context(
                format!("secret {secret_id} was not found"),
                json!({ "secret_id": secret_id }),
            ),
            RegistryError::StreamSessionNotFound { session_id } => Self::not_found_with_context(
                format!("stream session {session_id} was not found"),
                json!({ "session_id": session_id }),
            ),
            RegistryError::AsyncJobNotFound { job_id } => Self::not_found_with_context(
                format!("async job {job_id} was not found"),
                json!({ "job_id": job_id }),
            ),
            RegistryError::InvocationLogNotFound { log_id } => Self::not_found_with_context(
                format!("invocation log {log_id} was not found"),
                json!({ "log_id": log_id }),
            ),
            RegistryError::PublishedAgentNotFound {
                workspace_slug,
                agent_slug,
            } => Self::not_found_with_context(
                format!("published agent {workspace_slug}/{agent_slug} was not found"),
                json!({
                    "workspace_slug": workspace_slug,
                    "agent_slug": agent_slug,
                }),
            ),
            RegistryError::OperationNotFound { operation_id } => Self::not_found_with_context(
                format!("operation {operation_id} was not found"),
                json!({ "operation_id": operation_id }),
            ),
            RegistryError::OperationVersionNotFound {
                operation_id,
                version,
            } => Self::not_found_with_context(
                format!("operation version {version} for {operation_id} was not found"),
                json!({
                    "operation_id": operation_id,
                    "version": version,
                }),
            ),
            RegistryError::OperationHasPublishedAgentBindings { operation_id } => {
                Self::conflict_with_context(
                    format!(
                        "operation {operation_id} cannot be deleted while it is bound to a published agent"
                    ),
                    json!({ "operation_id": operation_id }),
                )
            }
            RegistryError::AuthProfileNotFound { auth_profile_id } => Self::not_found_with_context(
                format!("auth profile {auth_profile_id} was not found"),
                json!({ "auth_profile_id": auth_profile_id }),
            ),
            RegistryError::OperationAlreadyExists { operation_id } => Self::conflict_with_context(
                format!("operation {operation_id} already exists"),
                json!({ "operation_id": operation_id }),
            ),
            RegistryError::WorkspaceSlugAlreadyExists { slug } => Self::conflict_with_context(
                format!("workspace with slug {slug} already exists"),
                json!({ "slug": slug }),
            ),
            RegistryError::SecretNameAlreadyExists { workspace_id, name } => {
                Self::conflict_with_context(
                    format!("secret with name {name} already exists in workspace {workspace_id}"),
                    json!({
                        "workspace_id": workspace_id,
                        "name": name,
                    }),
                )
            }
            RegistryError::SecretReferencedByAuthProfile {
                secret_id,
                auth_profile_id,
            } => Self::conflict_with_context(
                format!("secret {secret_id} is referenced by auth profile {auth_profile_id}"),
                json!({
                    "secret_id": secret_id,
                    "auth_profile_id": auth_profile_id,
                }),
            ),
            RegistryError::InvalidStreamSessionTransition {
                session_id,
                from,
                to,
            } => Self::conflict_with_context(
                format!("invalid stream session transition for {session_id}: {from} -> {to}"),
                json!({
                    "session_id": session_id,
                    "from": from,
                    "to": to,
                }),
            ),
            RegistryError::InvalidAsyncJobTransition { job_id, from, to } => {
                Self::conflict_with_context(
                    format!("invalid async job transition for {job_id}: {from} -> {to}"),
                    json!({
                        "job_id": job_id,
                        "from": from,
                        "to": to,
                    }),
                )
            }
            RegistryError::UserEmailAlreadyExists { email } => Self::conflict_with_context(
                format!("user with email {email} already exists"),
                json!({ "email": email }),
            ),
            RegistryError::InvalidInitialVersion {
                operation_id,
                version,
            } => Self::validation_with_context(
                format!("operation {operation_id} must start with version 1, got {version}"),
                json!({
                    "operation_id": operation_id,
                    "version": version,
                }),
            ),
            RegistryError::InvalidVersionSequence {
                operation_id,
                expected,
                actual,
            } => Self::validation_with_context(
                format!("operation {operation_id} expected next version {expected}, got {actual}"),
                json!({
                    "operation_id": operation_id,
                    "expected": expected,
                    "actual": actual,
                }),
            ),
            RegistryError::ImmutableOperationFieldChanged {
                operation_id,
                field,
            } => Self::validation_with_context(
                format!("operation {operation_id} changed immutable field {field}"),
                json!({
                    "operation_id": operation_id,
                    "field": field,
                }),
            ),
            RegistryError::InvalidEnumRepresentation { field } => Self::validation_with_context(
                format!("unsupported enum representation for field {field}"),
                json!({ "field": field }),
            ),
            RegistryError::InvalidNumericValue { field, value } => Self::validation_with_context(
                format!("invalid numeric value for field {field}: {value}"),
                json!({
                    "field": field,
                    "value": value,
                }),
            ),
            RegistryError::YamlImportJobNotFound { job_id } => Self::validation_with_context(
                format!("yaml import job {job_id} was not found"),
                json!({ "job_id": job_id }),
            ),
            RegistryError::Storage(_) | RegistryError::Serialization(_) => {
                Self::internal(value.to_string())
            }
        }
    }
}

impl From<MappingError> for ApiError {
    fn from(value: MappingError) -> Self {
        Self::validation(value.to_string())
    }
}

impl From<SchemaError> for ApiError {
    fn from(value: SchemaError) -> Self {
        Self::validation(value.to_string())
    }
}

impl From<StorageError> for ApiError {
    fn from(value: StorageError) -> Self {
        match value {
            StorageError::InvalidStorageRef { details } => Self::validation_with_context(
                format!("invalid storage reference: {details}"),
                json!({ "details": details }),
            ),
            StorageError::Io(_) | StorageError::Serialization(_) => {
                Self::internal(value.to_string())
            }
        }
    }
}

pub fn runtime_test_failure(error: &RuntimeError) -> Value {
    let mut payload = json!({
        "code": runtime_test_failure_code(error),
        "message": error.to_string()
    });
    if let Some(context) = runtime_error_context(error) {
        payload["context"] = context;
    }
    payload
}

fn runtime_test_failure_code(error: &RuntimeError) -> &'static str {
    match error {
        RuntimeError::Schema(_) => "runtime_schema_error",
        RuntimeError::Mapping(_) => "runtime_mapping_error",
        RuntimeError::GraphqlAdapter(_) => "runtime_graphql_error",
        RuntimeError::GrpcAdapter(_) => "runtime_grpc_error",
        RuntimeError::RestAdapter(_) => "runtime_rest_error",
        RuntimeError::SoapAdapter(_) => "runtime_soap_error",
        RuntimeError::WebsocketAdapter(_) => "runtime_websocket_error",
        RuntimeError::UnsupportedProtocol { .. } => "runtime_protocol_error",
        RuntimeError::ConcurrencyLimitExceeded { .. } => "runtime_overloaded",
        RuntimeError::InvalidPreparedRequest { .. } => "runtime_request_error",
        RuntimeError::MissingStreamingConfig { .. } => "runtime_streaming_config_error",
        RuntimeError::UnsupportedExecutionMode { .. } => "runtime_streaming_mode_error",
        RuntimeError::InvalidStreamingPayload { .. } => "runtime_streaming_payload_error",
        RuntimeError::MissingAuthProfile { .. } => "runtime_auth_profile_error",
        RuntimeError::MissingSecret { .. } | RuntimeError::MissingSecretVersion { .. } => {
            "runtime_secret_error"
        }
        RuntimeError::InvalidAuthSecretValue { .. } => "runtime_secret_value_error",
        RuntimeError::SecretCrypto { .. } => "runtime_secret_crypto_error",
    }
}

pub fn runtime_error_context(error: &RuntimeError) -> Option<Value> {
    match error {
        RuntimeError::InvalidPreparedRequest { field, reason } => Some(json!({
            "field": field,
            "reason": reason,
        })),
        RuntimeError::InvalidStreamingPayload { field, reason } => Some(json!({
            "field": field,
            "reason": reason,
        })),
        RuntimeError::InvalidAuthSecretValue { secret_id, reason } => Some(json!({
            "secret_id": secret_id,
            "reason": reason,
        })),
        RuntimeError::SecretCrypto { operation, details } => Some(json!({
            "operation": operation,
            "details": details,
        })),
        RuntimeError::MissingAuthProfile { auth_profile_id } => Some(json!({
            "auth_profile_id": auth_profile_id,
        })),
        RuntimeError::MissingSecret { secret_id } => Some(json!({
            "secret_id": secret_id,
        })),
        RuntimeError::MissingSecretVersion { secret_id, version } => Some(json!({
            "secret_id": secret_id,
            "version": version,
        })),
        RuntimeError::MissingStreamingConfig { operation_id } => Some(json!({
            "operation_id": operation_id,
        })),
        RuntimeError::UnsupportedExecutionMode { operation_id, mode } => Some(json!({
            "operation_id": operation_id,
            "mode": mode,
        })),
        RuntimeError::UnsupportedProtocol { protocol } => Some(json!({
            "protocol": protocol,
        })),
        RuntimeError::ConcurrencyLimitExceeded { kind, limit } => Some(json!({
            "kind": kind,
            "limit": limit,
        })),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::{ApiError, runtime_error_context, runtime_test_failure};
    use crank_registry::RegistryError;
    use crank_runtime::RuntimeError;

    #[test]
    fn runtime_test_failure_includes_structured_context() {
        let payload = runtime_test_failure(&RuntimeError::InvalidPreparedRequest {
            field: "request.headers".to_owned(),
            reason: "must be an object".to_owned(),
        });

        assert_eq!(payload["code"], "runtime_request_error");
        assert_eq!(
            payload["context"],
            json!({
                "field": "request.headers",
                "reason": "must be an object"
            })
        );
    }

    #[test]
    fn runtime_error_context_includes_secret_crypto_operation() {
        let context = runtime_error_context(&RuntimeError::SecretCrypto {
            operation: "decode secret envelope",
            details: "bad base64".to_owned(),
        })
        .unwrap();

        assert_eq!(
            context,
            json!({
                "operation": "decode secret envelope",
                "details": "bad base64"
            })
        );
    }

    #[test]
    fn runtime_test_failure_includes_runtime_overload_context() {
        let payload = runtime_test_failure(&RuntimeError::ConcurrencyLimitExceeded {
            kind: "window",
            limit: 16,
        });

        assert_eq!(payload["code"], "runtime_overloaded");
        assert_eq!(
            payload["context"],
            json!({
                "kind": "window",
                "limit": 16
            })
        );
    }

    #[test]
    fn registry_errors_preserve_structured_context_in_api_error() {
        let error = ApiError::from(RegistryError::InvalidAsyncJobTransition {
            job_id: "job_123".to_owned(),
            from: "running".to_owned(),
            to: "completed".to_owned(),
        });

        match error {
            ApiError::Conflict { context, .. } => {
                assert_eq!(
                    context,
                    Some(json!({
                        "job_id": "job_123",
                        "from": "running",
                        "to": "completed"
                    }))
                );
            }
            other => panic!("unexpected error variant: {other:?}"),
        }
    }
}
