//! Object store configuration for cloud storage backends (S3/GCS/Azure)
//!
//! Provides `ObjectStoreConfig` for configuring Lance dataset access to
//! remote object stores (S3, GCS, Azure Blob, or local filesystem).
//!
//! # Architecture
//!
//! ```text
//! ObjectStoreConfig ──┐
//!                     ├── to_storage_options() ──→ HashMap<String, String>
//!                     │                             │
//!                     │                             ▼
//!                     │                      StorageOptionsAccessor
//!                     │                             │
//!                     ├── to_object_store_params()  ▼
//!                     │                      ObjectStoreParams
//!                     │                             │
//!                     └──────────────────────→ DatasetBuilder
//!                                              .with_storage_options()
//! ```

use std::collections::HashMap;
use std::sync::Arc;

use serde::{Deserialize, Serialize};

use lance::io::{ObjectStoreParams, StorageOptionsAccessor};

/// Supported object store backend types.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ObjectStoreType {
    /// Local filesystem (default, no credentials needed)
    Local,
    /// Amazon S3
    S3,
    /// Google Cloud Storage
    Gcs,
    /// Azure Blob Storage
    Azure,
}

impl Default for ObjectStoreType {
    fn default() -> Self {
        Self::Local
    }
}

impl std::fmt::Display for ObjectStoreType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Local => write!(f, "local"),
            Self::S3 => write!(f, "s3"),
            Self::Gcs => write!(f, "gcs"),
            Self::Azure => write!(f, "azure"),
        }
    }
}

/// Configuration for connecting to an object store backend.
///
/// Supports S3, GCS, Azure Blob, and local filesystem storage.
/// Credentials can be provided explicitly or via environment variables.
///
/// # S3 Configuration
///
/// ```no_run
/// use aifastdb_core::storage::lance::ObjectStoreConfig;
///
/// let config = ObjectStoreConfig::s3()
///     .with_region("us-east-1")
///     .with_access_key("AKIAIOSFODNN7EXAMPLE", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY")
///     .with_endpoint("https://s3.us-east-1.amazonaws.com");
/// ```
///
/// # GCS Configuration
///
/// ```no_run
/// use aifastdb_core::storage::lance::ObjectStoreConfig;
///
/// let config = ObjectStoreConfig::gcs()
///     .with_option("service_account", "/path/to/service-account.json");
/// ```
///
/// # Azure Configuration
///
/// ```no_run
/// use aifastdb_core::storage::lance::ObjectStoreConfig;
///
/// let config = ObjectStoreConfig::azure()
///     .with_option("account_name", "myaccount")
///     .with_option("account_key", "base64key==");
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObjectStoreConfig {
    /// Object store backend type
    pub store_type: ObjectStoreType,

    /// Storage-specific options (key-value pairs).
    ///
    /// Common keys per backend:
    ///
    /// **S3**: `region`, `aws_access_key_id`, `aws_secret_access_key`,
    ///         `aws_session_token`, `endpoint`, `aws_allow_http`
    ///
    /// **GCS**: `service_account`, `google_application_credentials`,
    ///          `project_id`
    ///
    /// **Azure**: `account_name`, `account_key`, `sas_token`,
    ///            `tenant_id`, `client_id`, `client_secret`, `endpoint`
    pub options: HashMap<String, String>,

    /// Local cache directory for remote data (LRU eviction).
    ///
    /// When accessing remote storage, frequently-read data files are cached
    /// locally in this directory. If `None`, a system temp directory is used.
    pub local_cache_dir: Option<String>,

    /// Maximum cache size in bytes (default: 1 GB).
    pub cache_size_bytes: u64,

    /// Block size for reading remote data (default: Lance default).
    pub block_size: Option<usize>,
}

impl Default for ObjectStoreConfig {
    fn default() -> Self {
        Self {
            store_type: ObjectStoreType::Local,
            options: HashMap::new(),
            local_cache_dir: None,
            cache_size_bytes: 1024 * 1024 * 1024, // 1 GB
            block_size: None,
        }
    }
}

impl ObjectStoreConfig {
    /// Create a local filesystem configuration (no remote storage).
    pub fn local() -> Self {
        Self::default()
    }

    /// Create an S3 configuration.
    pub fn s3() -> Self {
        Self {
            store_type: ObjectStoreType::S3,
            ..Default::default()
        }
    }

    /// Create a GCS configuration.
    pub fn gcs() -> Self {
        Self {
            store_type: ObjectStoreType::Gcs,
            ..Default::default()
        }
    }

    /// Create an Azure Blob Storage configuration.
    pub fn azure() -> Self {
        Self {
            store_type: ObjectStoreType::Azure,
            ..Default::default()
        }
    }

    /// Set the AWS region (S3 only).
    pub fn with_region(mut self, region: &str) -> Self {
        self.options.insert("region".to_string(), region.to_string());
        self
    }

    /// Set S3 access key credentials.
    pub fn with_access_key(mut self, access_key_id: &str, secret_access_key: &str) -> Self {
        self.options.insert("aws_access_key_id".to_string(), access_key_id.to_string());
        self.options.insert("aws_secret_access_key".to_string(), secret_access_key.to_string());
        self
    }

    /// Set S3 session token (for temporary credentials).
    pub fn with_session_token(mut self, token: &str) -> Self {
        self.options.insert("aws_session_token".to_string(), token.to_string());
        self
    }

    /// Set the endpoint URL (useful for S3-compatible services like MinIO).
    pub fn with_endpoint(mut self, endpoint: &str) -> Self {
        self.options.insert("endpoint".to_string(), endpoint.to_string());
        self
    }

    /// Allow HTTP connections (for local S3-compatible services like MinIO).
    pub fn with_allow_http(mut self, allow: bool) -> Self {
        self.options.insert("aws_allow_http".to_string(), allow.to_string());
        self
    }

    /// Set a generic storage option key-value pair.
    pub fn with_option(mut self, key: &str, value: &str) -> Self {
        self.options.insert(key.to_string(), value.to_string());
        self
    }

    /// Set the local cache directory for remote data.
    pub fn with_cache_dir(mut self, dir: &str) -> Self {
        self.local_cache_dir = Some(dir.to_string());
        self
    }

    /// Set the maximum cache size in bytes.
    pub fn with_cache_size(mut self, bytes: u64) -> Self {
        self.cache_size_bytes = bytes;
        self
    }

    /// Set the block size for reading remote data.
    pub fn with_block_size(mut self, size: usize) -> Self {
        self.block_size = Some(size);
        self
    }

    /// Whether this config represents a remote (non-local) storage backend.
    pub fn is_remote(&self) -> bool {
        !matches!(self.store_type, ObjectStoreType::Local)
    }

    /// Convert to storage options HashMap for Lance's `DatasetBuilder`.
    pub fn to_storage_options(&self) -> HashMap<String, String> {
        self.options.clone()
    }

    /// Convert to Lance `ObjectStoreParams` for read/write operations.
    pub fn to_object_store_params(&self) -> ObjectStoreParams {
        let mut params = ObjectStoreParams::default();

        if !self.options.is_empty() {
            let accessor = StorageOptionsAccessor::with_static_options(self.options.clone());
            params.storage_options_accessor = Some(Arc::new(accessor));
        }

        if let Some(block_size) = self.block_size {
            params.block_size = Some(block_size);
        }

        params
    }
}

/// Helper: detect storage type from a URI string.
///
/// Returns `ObjectStoreType` based on the URI scheme:
/// - `s3://` → S3
/// - `gs://` → GCS
/// - `az://` or `abfs://` → Azure
/// - Everything else → Local
pub fn detect_store_type(uri: &str) -> ObjectStoreType {
    let lower = uri.to_lowercase();
    if lower.starts_with("s3://") || lower.starts_with("s3a://") {
        ObjectStoreType::S3
    } else if lower.starts_with("gs://") || lower.starts_with("gcs://") {
        ObjectStoreType::Gcs
    } else if lower.starts_with("az://") || lower.starts_with("abfs://") || lower.starts_with("abfss://") {
        ObjectStoreType::Azure
    } else {
        ObjectStoreType::Local
    }
}

/// Check if a URI represents a remote (non-local) storage location.
pub fn is_remote_uri(uri: &str) -> bool {
    !matches!(detect_store_type(uri), ObjectStoreType::Local)
}

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

    #[test]
    fn test_detect_store_type() {
        assert_eq!(detect_store_type("s3://bucket/path"), ObjectStoreType::S3);
        assert_eq!(detect_store_type("S3://BUCKET/path"), ObjectStoreType::S3);
        assert_eq!(detect_store_type("s3a://bucket/path"), ObjectStoreType::S3);
        assert_eq!(detect_store_type("gs://bucket/path"), ObjectStoreType::Gcs);
        assert_eq!(detect_store_type("gcs://bucket/path"), ObjectStoreType::Gcs);
        assert_eq!(detect_store_type("az://container/path"), ObjectStoreType::Azure);
        assert_eq!(detect_store_type("abfs://container/path"), ObjectStoreType::Azure);
        assert_eq!(detect_store_type("abfss://container/path"), ObjectStoreType::Azure);
        assert_eq!(detect_store_type("/local/path"), ObjectStoreType::Local);
        assert_eq!(detect_store_type("C:\\local\\path"), ObjectStoreType::Local);
        assert_eq!(detect_store_type("./relative/path"), ObjectStoreType::Local);
    }

    #[test]
    fn test_is_remote_uri() {
        assert!(is_remote_uri("s3://bucket/path"));
        assert!(is_remote_uri("gs://bucket/path"));
        assert!(is_remote_uri("az://container/path"));
        assert!(!is_remote_uri("/local/path"));
        assert!(!is_remote_uri("./relative/path"));
    }

    #[test]
    fn test_s3_config() {
        let config = ObjectStoreConfig::s3()
            .with_region("us-east-1")
            .with_access_key("AKID", "SECRET")
            .with_endpoint("http://localhost:9000")
            .with_allow_http(true);

        assert_eq!(config.store_type, ObjectStoreType::S3);
        assert!(config.is_remote());

        let opts = config.to_storage_options();
        assert_eq!(opts.get("region").unwrap(), "us-east-1");
        assert_eq!(opts.get("aws_access_key_id").unwrap(), "AKID");
        assert_eq!(opts.get("aws_secret_access_key").unwrap(), "SECRET");
        assert_eq!(opts.get("endpoint").unwrap(), "http://localhost:9000");
        assert_eq!(opts.get("aws_allow_http").unwrap(), "true");
    }

    #[test]
    fn test_gcs_config() {
        let config = ObjectStoreConfig::gcs()
            .with_option("service_account", "/path/to/sa.json")
            .with_option("project_id", "my-project");

        assert_eq!(config.store_type, ObjectStoreType::Gcs);
        assert!(config.is_remote());

        let opts = config.to_storage_options();
        assert_eq!(opts.get("service_account").unwrap(), "/path/to/sa.json");
        assert_eq!(opts.get("project_id").unwrap(), "my-project");
    }

    #[test]
    fn test_azure_config() {
        let config = ObjectStoreConfig::azure()
            .with_option("account_name", "myaccount")
            .with_option("account_key", "base64key==");

        assert_eq!(config.store_type, ObjectStoreType::Azure);
        assert!(config.is_remote());
    }

    #[test]
    fn test_local_config() {
        let config = ObjectStoreConfig::local();
        assert_eq!(config.store_type, ObjectStoreType::Local);
        assert!(!config.is_remote());
    }

    #[test]
    fn test_to_object_store_params() {
        let config = ObjectStoreConfig::s3()
            .with_region("us-west-2")
            .with_block_size(4096);

        let params = config.to_object_store_params();
        assert!(params.storage_options_accessor.is_some());
        assert_eq!(params.block_size, Some(4096));
    }

    #[test]
    fn test_empty_local_params() {
        let config = ObjectStoreConfig::local();
        let params = config.to_object_store_params();
        assert!(params.storage_options_accessor.is_none());
        assert!(params.block_size.is_none());
    }

    #[test]
    fn test_serde_roundtrip() {
        let config = ObjectStoreConfig::s3()
            .with_region("eu-west-1")
            .with_cache_dir("/tmp/lance-cache")
            .with_cache_size(2 * 1024 * 1024 * 1024);

        let json = serde_json::to_string(&config).unwrap();
        let deserialized: ObjectStoreConfig = serde_json::from_str(&json).unwrap();

        assert_eq!(deserialized.store_type, ObjectStoreType::S3);
        assert_eq!(deserialized.options.get("region").unwrap(), "eu-west-1");
        assert_eq!(deserialized.local_cache_dir, Some("/tmp/lance-cache".to_string()));
        assert_eq!(deserialized.cache_size_bytes, 2 * 1024 * 1024 * 1024);
    }
}
