//! Error types for the Archivist.
//!
//! This module defines all error types that can occur during archival operations,
//! including I/O errors, JSON errors, and domain-specific errors for connectors
//! and sessions.

use std::path::PathBuf;
use thiserror::Error;
use uuid::Uuid;

/// Result type alias for Archivist operations
pub type Result<T> = std::result::Result<T, ArchivistError>;

/// Errors that can occur during archival operations
#[derive(Debug, Error)]
pub enum ArchivistError {
    /// Connector with the given UID was not found
    #[error("Connector not found: {0}")]
    ConnectorUnknown(Uuid),

    /// Session with the given scroll ID was not found
    #[error("Session not found: {0}")]
    SessionUnknown(Uuid),

    /// UUID collision detected with inconsistent data
    ///
    /// This occurs when a custom UUID is provided that matches an existing
    /// entity but with different attributes (e.g., different connector type).
    #[error("UUID collision: {0}")]
    CollisionInconsistent(Uuid),

    /// Invalid request (e.g., missing required fields, invalid format)
    #[error("Invalid request: {0}")]
    InvalidRequest(String),

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

    /// JSON serialization/deserialization error
    #[error("JSON error: {0}")]
    Json(#[from] serde_json::Error),

    // Multi-archive errors
    /// Invalid archive name (empty or contains invalid characters)
    #[error("Invalid archive name: {0}")]
    InvalidArchiveName(String),

    /// Archive already exists with the given name
    #[error("Archive already exists: {0}")]
    ArchiveAlreadyExists(String),

    /// Archive not found with the given name
    #[error("Archive not found: {0}")]
    ArchiveNotFound(String),

    /// Archive path conflict (path is already used by another archive)
    #[error("Archive path conflict: {0}")]
    ArchivePathConflict(PathBuf),

    /// Cannot remove default archive without force flag
    #[error("Cannot remove default archive without force flag")]
    CannotRemoveDefaultArchive,

    /// Archive is not empty (has sessions)
    #[error("Archive '{name}' is not empty ({session_count} sessions)")]
    ArchiveNotEmpty {
        name: String,
        session_count: usize,
    },

    /// No archives configured
    #[error("No archives configured")]
    NoArchivesConfigured,

    /// Failed to load registry file
    #[error("Failed to load registry: {0}")]
    RegistryLoadError(String),

    /// Failed to parse registry JSON
    #[error("Failed to parse registry: {0}")]
    RegistryParseError(String),

    /// Failed to serialize registry to JSON
    #[error("Failed to serialize registry: {0}")]
    RegistrySerializeError(String),

    /// Failed to write registry file
    #[error("Failed to write registry: {0}")]
    RegistryWriteError(String),

    /// Backend is unavailable (e.g., disk full, connection lost, degraded state)
    #[error("Backend {name} is unavailable")]
    BackendUnavailable { name: String },

    /// Backend does not support the requested capability
    #[error("Backend {backend} does not support capability {capability:?}")]
    CapabilityNotSupported {
        capability: crate::backend::ArchiveCapability,
        backend: String,
    },

    /// Health check for a backend failed
    #[error("Health check for backend {name} failed: {reason}")]
    BackendHealthCheckFailed { name: String, reason: String },

    /// Primary write backend is unavailable or misconfigured.
    #[error("primary write backend `{name}` is unavailable: {reason}")]
    PrimaryUnavailable { name: String, reason: String },

    /// Session exists on a read-only (not write_active) backend; deletion impossible.
    #[error("session {scroll_id} exists in read-only backend `{backend}`; cannot delete")]
    DeleteOnReadOnlyBackend { backend: String, scroll_id: uuid::Uuid },

    /// Move succeeded at the destination but source-side delete failed.
    #[error("partial move: copy to `{copied_to}` succeeded but source-side delete failed: {delete_error}")]
    PartialMove {
        copied_to: String,
        delete_error: Box<ArchivistError>,
    },

    /// Queued-write backend's queue is full.
    #[error("write queue full for backend `{backend}` (op `{op}`)")]
    WriteQueueFull {
        backend: String,
        op: &'static str,
    },

    /// The coordinator has no archive configured (ephemeral mode).
    #[error("no archive is configured (ephemeral mode)")]
    NoArchiveConfigured,

    /// A requested archive name does not exist in the registry.
    #[error("archive name `{0}` is unknown")]
    ArchiveNameUnknown(String),

    /// Runtime mutation of the archive registry is not supported in Phase 3.
    #[error("dynamic registry mutation is not supported (Phase 3 is startup-only)")]
    DynamicRegistryUnsupported,

    /// Catch-all for injected failures / legacy call sites. Prefer a typed variant when possible.
    #[error("{0}")]
    Other(String),
}

/// Errors raised exclusively at boot, by `Archivist::from_config`.
#[derive(Debug, thiserror::Error)]
pub enum ArchivistBootError {
    #[error("duplicate archive name `{0}` in config")]
    DuplicateName(String),

    #[error("archive `{name}` declares unknown type `{type_name}`")]
    UnknownType { name: String, type_name: String },

    #[error("no `required` write-active backend configured (need at least one primary)")]
    NoPrimary,

    #[error("backend `{name}` failed to build: {source}")]
    BackendBuild {
        name: String,
        #[source]
        source: crate::registry::BackendBuildError,
    },

    #[error("required backend `{name}` is unavailable at boot: {reason}")]
    UnavailableRequiredBackend { name: String, reason: String },

    #[error("no unrestricted write-active archive — at least one enabled, write_active backend must have an empty filter")]
    NoUnrestrictedPrimary,

    #[error("filter for archive `{archive}` rejects all sessions (include_connectors is empty)")]
    FilterRejectsEverything { archive: String },

    #[error("config validation failed: {0}")]
    Validation(#[from] crate::registry::ConfigValidationError),
}

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

    #[test]
    fn test_error_display() {
        let uuid = Uuid::now_v7();

        // Test ConnectorUnknown
        let err = ArchivistError::ConnectorUnknown(uuid);
        assert_eq!(err.to_string(), format!("Connector not found: {}", uuid));

        // Test SessionUnknown
        let err = ArchivistError::SessionUnknown(uuid);
        assert_eq!(err.to_string(), format!("Session not found: {}", uuid));

        // Test CollisionInconsistent
        let err = ArchivistError::CollisionInconsistent(uuid);
        assert_eq!(err.to_string(), format!("UUID collision: {}", uuid));

        // Test InvalidRequest
        let err = ArchivistError::InvalidRequest("missing field".to_string());
        assert_eq!(err.to_string(), "Invalid request: missing field");
    }

    #[test]
    fn test_io_error_conversion() {
        // Create an I/O error
        let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");

        // Convert to ArchivistError using From trait
        let archivist_err: ArchivistError = io_err.into();

        // Verify it's the right variant
        match archivist_err {
            ArchivistError::Io(e) => {
                assert_eq!(e.kind(), io::ErrorKind::NotFound);
                assert_eq!(e.to_string(), "file not found");
            }
            _ => panic!("Expected Io variant"),
        }
    }

    #[test]
    fn test_json_error_conversion() {
        // Create a JSON error by trying to parse invalid JSON
        let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();

        // Convert to ArchivistError using From trait
        let archivist_err: ArchivistError = json_err.into();

        // Verify it's the right variant
        match archivist_err {
            ArchivistError::Json(_) => {
                // Success - it's a JSON error
            }
            _ => panic!("Expected Json variant"),
        }
    }

    #[test]
    fn test_result_type_with_question_mark() {
        // Test that Result<T> works with the ? operator
        fn test_function() -> Result<String> {
            // This should compile and work with ?
            let _data: serde_json::Value = serde_json::from_str(r#"{"key": "value"}"#)?;
            Ok("success".to_string())
        }

        let result = test_function();
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), "success");
    }

    #[test]
    fn test_error_chain() {
        // Test that errors can be chained properly
        fn inner_function() -> std::io::Result<String> {
            Err(std::io::Error::new(
                std::io::ErrorKind::NotFound,
                "inner error",
            ))
        }

        fn outer_function() -> Result<String> {
            // The ? operator should automatically convert io::Error to ArchivistError
            let _result = inner_function()?;
            Ok("success".to_string())
        }

        let result = outer_function();
        assert!(result.is_err());

        match result {
            Err(ArchivistError::Io(e)) => {
                assert_eq!(e.kind(), std::io::ErrorKind::NotFound);
            }
            _ => panic!("Expected Io error"),
        }
    }

    #[test]
    fn test_error_debug() {
        let uuid = Uuid::now_v7();
        let err = ArchivistError::ConnectorUnknown(uuid);

        // Verify Debug implementation works
        let debug_str = format!("{:?}", err);
        assert!(debug_str.contains("ConnectorUnknown"));
        assert!(debug_str.contains(&uuid.to_string()));
    }

    #[test]
    fn test_all_error_variants() {
        let uuid = Uuid::now_v7();

        // Test all variants can be created
        let errors = vec![
            ArchivistError::ConnectorUnknown(uuid),
            ArchivistError::SessionUnknown(uuid),
            ArchivistError::CollisionInconsistent(uuid),
            ArchivistError::InvalidRequest("test".to_string()),
            ArchivistError::Io(io::Error::new(io::ErrorKind::Other, "test")),
            ArchivistError::Json(serde_json::from_str::<serde_json::Value>("bad").unwrap_err()),
        ];

        // Verify each error has a non-empty display string
        for err in errors {
            let display = err.to_string();
            assert!(!display.is_empty(), "Error display should not be empty");

            let debug = format!("{:?}", err);
            assert!(!debug.is_empty(), "Error debug should not be empty");
        }
    }
}
