use std::time::Duration;

use thiserror::Error;

/// Every fallible call in the SDK returns `Result<T, HeyoError>`. Variants
/// mirror the TypeScript SDK's error classes so the same recovery patterns
/// translate across languages.
#[derive(Debug, Error)]
pub enum HeyoError {
    /// No API key was provided and `HEYO_API_KEY` is unset.
    #[error("Missing API key. Pass `api_key` or set HEYO_API_KEY.")]
    Authentication,

    /// 4xx other than 401/403/404 (e.g. 400 Bad Request, 422 Unprocessable).
    #[error("{0}")]
    InvalidArgument(String),

    /// 404 Not Found.
    #[error("not found: {0}")]
    NotFound(String),

    /// 5xx, network error, or non-classified HTTP failure.
    #[error("api error ({status}): {message}")]
    Api {
        status: u16,
        message: String,
        body: Option<serde_json::Value>,
    },

    /// A `wait_for_*` call exceeded its budget.
    #[error("timeout after {0:?}: {1}")]
    Timeout(Duration, String),

    /// Sandbox provisioning ended in `failed`.
    #[error("sandbox {sandbox_id} failed: {reason}")]
    SandboxFailed {
        sandbox_id: String,
        reason: String,
    },

    /// Shell-stream WebSocket could not be (re)established.
    #[error("connection error: {0}")]
    Connection(String),

    /// Server refused to resume the shell session.
    #[error("shell session expired{}", .session_id.as_deref().map(|s| format!(" ({})", s)).unwrap_or_default())]
    SessionExpired { session_id: Option<String> },

    /// Remote shell exited unexpectedly.
    #[error("shell exited with code {0}")]
    ShellExit(i32),

    /// `Database::checkin` rejected because `data_version` advanced.
    #[error("checkin conflict: expected={expected:?} current={current}")]
    CheckinConflict {
        expected: Option<i64>,
        current: i64,
    },
}

impl HeyoError {
    pub(crate) fn invalid(msg: impl Into<String>) -> Self {
        HeyoError::InvalidArgument(msg.into())
    }
    pub(crate) fn api(status: u16, message: impl Into<String>) -> Self {
        HeyoError::Api {
            status,
            message: message.into(),
            body: None,
        }
    }
    pub(crate) fn api_with_body(
        status: u16,
        message: impl Into<String>,
        body: Option<serde_json::Value>,
    ) -> Self {
        HeyoError::Api {
            status,
            message: message.into(),
            body,
        }
    }
}
