//! Unified error types for mi6-core.
//!
//! This module consolidates all error types into a single location, providing
//! both domain-specific errors and a unified [`Mi6Error`] type for convenient
//! error handling.
//!
//! # Error Types
//!
//! - [`Mi6Error`]: Unified error type that wraps all domain errors
//! - [`StorageError`]: Database and storage operations
//! - [`ConfigError`]: Configuration file loading
//! - [`TtlParseError`]: TTL/retention string parsing
//! - [`InitError`]: Hook installation and initialization
//! - [`TranscriptError`]: Transcript file parsing
//! - [`ScanError`]: Transcript scanning with storage integration
//!
//! # Example
//!
//! ```
//! use mi6_core::{Mi6Error, StorageError, ConfigError};
//!
//! fn example() -> Result<(), Mi6Error> {
//!     // Domain errors automatically convert to Mi6Error
//!     // via From implementations
//!     Ok(())
//! }
//! ```

use thiserror::Error;

// =============================================================================
// Generic error utilities
// =============================================================================

/// A boxed error type for wrapping underlying errors.
///
/// This is commonly used when interfacing with libraries that return
/// different error types, allowing them to be stored uniformly.
pub type BoxError = Box<dyn std::error::Error + Send + Sync>;

/// A simple string-based error for cases where we need to create
/// an error from a message without an underlying error source.
///
/// # Example
///
/// ```
/// use mi6_core::StringError;
///
/// let err = StringError("something went wrong".to_string());
/// assert_eq!(err.to_string(), "something went wrong");
/// ```
#[derive(Debug, Clone)]
pub struct StringError(pub String);

impl std::fmt::Display for StringError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

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

// =============================================================================
// Unified Mi6Error
// =============================================================================

/// Unified error type for all mi6-core operations.
///
/// This enum wraps all domain-specific error types, providing a single
/// error type that can be used throughout the library. Each variant
/// implements `From` for convenient `?` operator usage.
///
/// # Example
///
/// ```ignore
/// use mi6_core::{Mi6Error, Config, Storage};
///
/// fn init_storage() -> Result<(), Mi6Error> {
///     let config = Config::load()?;  // ConfigError -> Mi6Error
///     let db_path = config.db_path()?;  // ConfigError -> Mi6Error
///     // ... storage operations would convert StorageError -> Mi6Error
///     Ok(())
/// }
/// ```
#[derive(Debug, Error)]
pub enum Mi6Error {
    /// Storage/database operation errors
    #[error(transparent)]
    Storage(#[from] StorageError),

    /// Configuration loading errors
    #[error(transparent)]
    Config(#[from] ConfigError),

    /// TTL/retention parsing errors
    #[error(transparent)]
    TtlParse(#[from] TtlParseError),

    /// Hook installation/initialization errors
    #[error(transparent)]
    Init(#[from] InitError),

    /// Transcript parsing errors
    #[error(transparent)]
    Transcript(#[from] TranscriptError),

    /// Transcript scanning errors
    #[error(transparent)]
    Scan(#[from] ScanError),
}

// =============================================================================
// Domain-specific errors
// =============================================================================

/// Errors from storage/database operations.
#[derive(Error, Debug)]
pub enum StorageError {
    /// Failed to establish database connection
    #[error("connection error: {0}")]
    Connection(#[source] BoxError),

    /// Failed to execute a database query
    #[error("query error: {0}")]
    Query(#[source] BoxError),

    /// I/O error during storage operations
    #[error("io error: {0}")]
    Io(#[from] std::io::Error),
}

/// Errors from configuration loading.
#[derive(Error, Debug)]
pub enum ConfigError {
    /// Failed to read config file
    #[error("failed to read config file: {0}")]
    ReadError(#[from] std::io::Error),

    /// Failed to parse TOML config
    #[error("failed to parse config file: {0}")]
    ParseError(#[from] toml::de::Error),

    /// Failed to serialize TOML config
    #[error("failed to serialize config: {0}")]
    TomlSerialize(String),

    /// Could not determine home directory
    #[error("failed to determine home directory")]
    NoHomeDir,
}

/// Errors from parsing TTL/retention strings.
#[derive(Error, Debug, PartialEq, Eq)]
pub enum TtlParseError {
    /// Invalid TTL format (e.g., not a recognized suffix)
    #[error("invalid TTL format: {0}")]
    InvalidFormat(String),

    /// Invalid number in TTL string
    #[error("invalid TTL number: {0}")]
    InvalidNumber(String),
}

/// Errors from hook installation and initialization.
#[derive(Debug, Error)]
pub enum InitError {
    /// Unknown framework name
    #[error("unknown framework: {0}")]
    UnknownFramework(String),

    /// Failed to determine settings path
    #[error("failed to determine settings path: {0}")]
    SettingsPath(String),

    /// Failed to read or write settings file
    #[error("settings file error: {0}")]
    SettingsFile(#[from] std::io::Error),

    /// Failed to parse or serialize configuration
    #[error("configuration error: {0}")]
    Config(String),

    /// Settings file contains invalid syntax (JSON or TOML)
    #[error("{path} contains invalid {format}: {error}")]
    InvalidSettings {
        /// Path to the settings file
        path: std::path::PathBuf,
        /// Format of the settings file (e.g., "JSON" or "TOML")
        format: &'static str,
        /// The parse error message
        error: String,
    },
}

/// Errors from transcript file parsing.
#[derive(Debug, Error)]
pub enum TranscriptError {
    /// I/O error reading transcript
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    /// JSON parse error at specific line
    #[error("JSON parse error at line {line}: {message}")]
    Parse {
        /// Line number where error occurred
        line: u64,
        /// Error message
        message: String,
    },

    /// Transcript file not found
    #[error("File not found: {0}")]
    NotFound(String),
}

/// Errors from transcript scanning with storage integration.
#[derive(Error, Debug)]
pub enum ScanError {
    /// Error reading/parsing the transcript file
    #[error("transcript parse error: {0}")]
    Parse(#[from] TranscriptError),

    /// Error interacting with storage
    #[error("storage error: {0}")]
    Storage(#[from] StorageError),
}

/// Errors from framework resolution.
#[derive(Debug, Error)]
pub enum FrameworkResolutionError {
    /// Unknown framework name
    #[error("unknown framework: {0}")]
    UnknownFramework(String),

    /// No frameworks found (auto-detection failed)
    #[error("{0}")]
    NoFrameworksFound(String),
}

// =============================================================================
// Tests
// =============================================================================

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

    #[test]
    fn test_string_error_display() {
        let msg = "test error message";
        let err = StringError(msg.to_string());
        assert_eq!(err.to_string(), msg);
    }

    #[test]
    fn test_string_error_is_error() {
        let err = StringError("test".to_string());
        // Verify it implements std::error::Error
        let _: &dyn std::error::Error = &err;
    }

    #[test]
    fn test_string_error_clone() {
        let err = StringError("cloneable".to_string());
        let cloned = err.clone();
        assert_eq!(err.0, cloned.0);
    }

    #[test]
    fn test_box_error_from_string_error() {
        let err = StringError("boxed".to_string());
        let boxed: BoxError = Box::new(err);
        assert_eq!(boxed.to_string(), "boxed");
    }

    #[test]
    fn test_mi6_error_from_storage_error() {
        let storage_err = StorageError::Io(std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "not found",
        ));
        let mi6_err: Mi6Error = storage_err.into();
        assert!(matches!(mi6_err, Mi6Error::Storage(_)));
    }

    #[test]
    fn test_mi6_error_from_config_error() {
        let config_err = ConfigError::NoHomeDir;
        let mi6_err: Mi6Error = config_err.into();
        assert!(matches!(mi6_err, Mi6Error::Config(_)));
    }

    #[test]
    fn test_mi6_error_from_ttl_parse_error() {
        let ttl_err = TtlParseError::InvalidFormat("bad".to_string());
        let mi6_err: Mi6Error = ttl_err.into();
        assert!(matches!(mi6_err, Mi6Error::TtlParse(_)));
    }

    #[test]
    fn test_mi6_error_from_init_error() {
        let init_err = InitError::UnknownFramework("test".to_string());
        let mi6_err: Mi6Error = init_err.into();
        assert!(matches!(mi6_err, Mi6Error::Init(_)));
    }

    #[test]
    fn test_mi6_error_from_transcript_error() {
        let transcript_err = TranscriptError::NotFound("file.json".to_string());
        let mi6_err: Mi6Error = transcript_err.into();
        assert!(matches!(mi6_err, Mi6Error::Transcript(_)));
    }

    #[test]
    fn test_mi6_error_from_scan_error() {
        let scan_err = ScanError::Parse(TranscriptError::NotFound("file.json".to_string()));
        let mi6_err: Mi6Error = scan_err.into();
        assert!(matches!(mi6_err, Mi6Error::Scan(_)));
    }

    #[test]
    fn test_mi6_error_display_transparent() {
        let storage_err = StorageError::Query(Box::new(StringError("query failed".to_string())));
        let mi6_err: Mi6Error = storage_err.into();
        // #[error(transparent)] means it displays the inner error's message
        assert!(mi6_err.to_string().contains("query failed"));
    }

    #[test]
    fn test_storage_error_source_preserved() {
        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
        let storage_err = StorageError::Connection(Box::new(io_err));

        use std::error::Error;
        let source = storage_err.source();
        assert!(source.is_some());
        assert!(source.is_some_and(|s| s.to_string().contains("file not found")));
    }
}
