BareGit
//! Stable errors used by Lisa's foundation components.

use thiserror::Error;

/// Reports a configuration problem before Lisa connects to Matrix.
#[derive(Debug, Error)]
pub enum ConfigurationError {
    /// Reports an invalid configuration value without exposing secrets.
    #[error("invalid configuration for {key}: {reason}")]
    InvalidValue {
        /// Names the invalid configuration key.
        key: &'static str,
        /// Describes why the public, non-secret value is invalid.
        reason: String,
    },

    /// Reports a failed configuration file read or TOML parse.
    #[error("could not load configuration: {0}")]
    Load(#[from] std::io::Error),

    /// Reports an invalid TOML document.
    #[error("could not parse configuration: {0}")]
    Parse(#[from] toml::de::Error),
}

/// Reports a durable-state failure.
#[derive(Debug, Error)]
pub enum StateStoreError {
    /// Reports an SQLite operation failure.
    #[error("state database operation failed: {0}")]
    Database(#[from] rusqlite::Error),

    /// Reports that the state worker stopped before answering a request.
    #[error("state database worker stopped")]
    WorkerStopped,

    /// Reports a malformed persisted value.
    #[error("invalid persisted state: {0}")]
    InvalidData(String),
}

/// Reports Matrix client or protocol failures.
#[derive(Debug, Error)]
pub enum MatrixError {
    /// Wraps a Matrix SDK failure.
    #[error("Matrix SDK request failed: {0}")]
    Sdk(#[from] matrix_sdk::Error),

    /// Reports a response that cannot be safely ingested.
    #[error("invalid Matrix response: {0}")]
    InvalidResponse(String),

    /// Reports a Matrix session identity mismatch.
    #[error("Matrix identity does not match the configured account")]
    IdentityMismatch,

    /// Reports a rejected Matrix password without exposing it.
    #[error("Matrix credentials were rejected")]
    InvalidCredentials,
}

impl MatrixError {
    /// Returns whether rebuilding the Matrix session can recover this error.
    #[must_use]
    pub fn needsReauthentication(&self) -> bool {
        let Self::Sdk(error) = self else {
            return false;
        };
        let matrix_sdk::Error::Http(error) = error else {
            return false;
        };
        matches!(
            error.client_api_error_kind(),
            Some(matrix_sdk::ruma::api::error::ErrorKind::UnknownToken(_))
        ) || matches!(error.as_ref(), matrix_sdk::HttpError::RefreshToken(_))
    }
}

/// Reports a bounded LLM completion failure without exposing request content.
#[derive(Debug, Error)]
pub enum LlmError {
    /// Reports an HTTP transport or timeout failure.
    #[error("LLM request failed")]
    Request(#[source] reqwest::Error),

    /// Reports an unsuccessful LLM HTTP status.
    #[error("LLM service returned HTTP status {0}")]
    Status(reqwest::StatusCode),

    /// Reports malformed or disallowed model output.
    #[error("LLM response did not match Lisa's final-answer contract")]
    Protocol,
}

/// Reports an application-level failure.
#[derive(Debug, Error)]
pub enum AppError {
    /// Wraps configuration errors.
    #[error(transparent)]
    Configuration(#[from] ConfigurationError),

    /// Wraps state-store errors.
    #[error(transparent)]
    State(#[from] StateStoreError),

    /// Wraps Matrix errors.
    #[error(transparent)]
    Matrix(#[from] MatrixError),

    /// Wraps LLM failures.
    #[error(transparent)]
    Llm(#[from] LlmError),
}

impl AppError {
    /// Returns a safe, stable code that may be shown in a room reply.
    #[must_use]
    pub fn userVisibleCode(&self) -> &'static str {
        match self {
            Self::Configuration(_) => "E_CONFIGURATION",
            Self::State(_) => "E_STATE",
            Self::Matrix(MatrixError::InvalidCredentials) => "E_MATRIX_CREDENTIALS",
            Self::Matrix(MatrixError::IdentityMismatch) => "E_MATRIX_IDENTITY",
            Self::Matrix(_) => "E_MATRIX_REQUEST",
            Self::Llm(LlmError::Request(error)) if error.is_timeout() => "E_LLM_TIMEOUT",
            Self::Llm(LlmError::Request(_)) => "E_LLM_REQUEST",
            Self::Llm(LlmError::Status(status)) if status.as_u16() == 429 => "E_LLM_HTTP_429",
            Self::Llm(LlmError::Status(status)) if status.is_server_error() => "E_LLM_HTTP_5XX",
            Self::Llm(LlmError::Status(_)) => "E_LLM_HTTP",
            Self::Llm(LlmError::Protocol) => "E_LLM_PROTOCOL",
        }
    }

    /// Returns whether the Matrix session must be recreated before continuing.
    #[must_use]
    pub fn needsMatrixReauthentication(&self) -> bool {
        matches!(self, Self::Matrix(error) if error.needsReauthentication())
    }

    /// Returns whether systemd should stop restarting for this error.
    #[must_use]
    pub fn isPermanentConfigurationFailure(&self) -> bool {
        matches!(
            self,
            Self::Configuration(_)
                | Self::Matrix(MatrixError::IdentityMismatch | MatrixError::InvalidCredentials)
        )
    }
}

#[cfg(test)]
mod tests {
    use super::{AppError, LlmError};

    #[test]
    fn exposes_only_stable_llm_error_codes() {
        assert_eq!(
            AppError::from(LlmError::Protocol).userVisibleCode(),
            "E_LLM_PROTOCOL"
        );
    }
}