// cSpell:ignore smol
#![allow(dead_code)]

use super::async_cache::AsyncCache;
use crate::cosmos_request::CosmosRequest;
use crate::handler::retry_handler::{BackOffRetryHandler, RetryHandler};
use crate::operation_context::OperationType;
use crate::routing::global_endpoint_manager::GlobalEndpointManager;
use crate::{models::ContainerProperties, resource_context::ResourceLink, ReadContainerOptions};
use azure_core::http::{Pipeline, Response};
use azure_core::Error;
use std::time::Duration;

/// Cache for Cosmos DB container metadata and properties.
///
/// # Summary
/// Maintains an in-memory cache of container properties (partition keys, indexing policies, etc.)
/// to minimize redundant metadata requests to the Cosmos DB service. Uses a 5-minute TTL by default
/// to balance freshness with performance. Integrates with retry handler for resilient metadata fetching
/// across regional endpoints.
#[derive(Clone, Debug)]
pub struct ContainerCache {
    pipeline: Pipeline,
    global_endpoint_manager: GlobalEndpointManager,
    container_properties_cache: AsyncCache<String, ContainerProperties>,
    retry_handler: BackOffRetryHandler,
}

impl ContainerCache {
    /// Creates a new `ContainerCache` with default configuration.
    ///
    /// # Summary
    /// Initializes a container cache with a 5-minute TTL for container properties.
    /// Sets up retry handler for resilient metadata operations across Azure regions.
    /// The cache automatically refreshes stale entries when accessed after expiration.
    ///
    /// # Arguments
    /// * `pipeline` - HTTP pipeline for making requests to Cosmos DB service
    /// * `global_endpoint_manager` - Manager for multi-region endpoint routing and failover
    ///
    /// # Returns
    /// A new `ContainerCache` instance ready for caching container metadata
    pub(crate) fn new(pipeline: Pipeline, global_endpoint_manager: GlobalEndpointManager) -> Self {
        let container_properties_cache = AsyncCache::new(
            Duration::from_secs(300), // Default 5 minutes TTL
        );
        let retry_handler = BackOffRetryHandler::new(global_endpoint_manager.clone());

        Self {
            pipeline,
            global_endpoint_manager,
            container_properties_cache,
            retry_handler,
        }
    }

    /// Retrieves container properties from cache or fetches from service if not cached.
    ///
    /// # Summary
    /// Returns container metadata (partition key definition, indexing policy, etc.) for the
    /// specified container. Checks the cache first; if not found or expired, fetches fresh
    /// metadata from the Cosmos DB service and updates the cache. Uses retry handler for
    /// resilience against transient failures and regional outages.
    ///
    /// # Arguments
    /// * `container_id` - Unique identifier of the container (used as cache key)
    /// * `container_link` - Resource link to the container in Cosmos DB
    /// * `options` - Optional request options including consistency level and context
    ///
    /// # Returns
    /// `Ok(ContainerProperties)` with container metadata, or `Err` if fetch fails
    pub async fn resolve_by_id(
        &self,
        container_id: String,
        container_link: ResourceLink,
        options: Option<ReadContainerOptions<'_>>,
        force_refresh: bool,
    ) -> Result<ContainerProperties, Error> {
        self.container_properties_cache
            .get(container_id, force_refresh, || async {
                let response = self
                    .read_container_properties_by_id(container_link, options)
                    .await?;
                response.into_model()
            })
            .await
    }

    /// Removes container metadata from the cache, forcing refresh on next access.
    ///
    /// # Summary
    /// Invalidates the cached container properties for the specified container ID.
    /// The next call to `resolve_by_id` for this container will fetch fresh metadata
    /// from the service. Useful when container configuration changes (e.g., partition
    /// key updates, indexing policy modifications) and stale cache must be cleared.
    ///
    /// # Arguments
    /// * `container_id` - Unique identifier of the container to remove from cache
    pub async fn remove_by_id(&self, container_id: &str) {
        self.container_properties_cache
            .remove(&container_id.to_string())
            .await;
    }

    /// Fetches container properties directly from the Cosmos DB service.
    ///
    /// # Summary
    /// Executes an HTTP GET request to retrieve container metadata from the service.
    /// Resolves the appropriate regional endpoint using the global endpoint manager,
    /// constructs the request with proper routing context, and delegates to the retry
    /// handler for resilient execution with automatic failover on errors.
    ///
    /// # Arguments
    /// * `container_link` - Resource link identifying the target container
    /// * `options` - Optional request options including consistency level and context
    ///
    /// # Returns
    /// `Ok(Response<ContainerProperties>)` on success, or `Err` if request fails
    async fn read_container_properties_by_id(
        &self,
        container_link: ResourceLink,
        options: Option<ReadContainerOptions<'_>>,
    ) -> azure_core::Result<Response<ContainerProperties>> {
        let options = options.unwrap_or_default();
        let mut cosmos_request =
            CosmosRequest::builder(OperationType::Read, container_link.clone()).build()?;

        let location_endpoint = self
            .global_endpoint_manager
            .resolve_service_endpoint(&cosmos_request);
        cosmos_request
            .request_context
            .route_to_location_endpoint(cosmos_request.resource_link.url(&location_endpoint));

        let ctx_owned = options
            .method_options
            .context
            .with_value(container_link)
            .into_owned();

        // Prepare a callback delegate to invoke the http request.
        let sender = move |req: &mut CosmosRequest| {
            let mut raw_req = req.clone().into_raw_request();
            let ctx = ctx_owned.clone();
            async move { self.pipeline.send(&ctx, &mut raw_req, None).await }
        };

        // Delegate to the retry handler, providing the sender callback
        let res = self.retry_handler.send(&mut cosmos_request, sender).await;
        res.map(Into::into)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use azure_core::http::ClientOptions;
    use std::borrow::Cow;
    use url::Url;

    // Helper function to create a test pipeline
    fn create_test_pipeline() -> Pipeline {
        azure_core::http::Pipeline::new(
            option_env!("CARGO_PKG_NAME"),
            option_env!("CARGO_PKG_VERSION"),
            ClientOptions::default(),
            Vec::new(),
            Vec::new(),
            None,
        )
    }

    // Helper function to create a test GlobalEndpointManager
    fn create_test_endpoint_manager() -> GlobalEndpointManager {
        let pipeline = create_test_pipeline();
        let endpoint = Url::parse("https://test.documents.azure.com").unwrap();
        GlobalEndpointManager::new(endpoint, vec![], pipeline)
    }

    // Helper function to create a test GlobalEndpointManager with preferred locations
    fn create_test_endpoint_manager_with_locations() -> GlobalEndpointManager {
        let pipeline = create_test_pipeline();
        let endpoint = Url::parse("https://test.documents.azure.com").unwrap();
        GlobalEndpointManager::new(
            endpoint,
            vec![Cow::Borrowed("East US"), Cow::Borrowed("West US")],
            pipeline,
        )
    }

    #[tokio::test]
    async fn remove_by_id() {
        let pipeline = create_test_pipeline();
        let global_endpoint_manager = create_test_endpoint_manager();
        let cache = ContainerCache::new(pipeline, global_endpoint_manager);

        // Test that remove_by_id doesn't panic when removing non-existent items
        cache.remove_by_id("non-existent-container").await;

        // Test passes if no panic occurs
    }

    #[tokio::test]
    async fn new_container_cache() {
        let pipeline = create_test_pipeline();
        let global_endpoint_manager = create_test_endpoint_manager();
        let cache = ContainerCache::new(pipeline, global_endpoint_manager);

        // Verify the cache was created successfully
        assert!(std::mem::size_of_val(&cache) > 0);
    }

    #[tokio::test]
    async fn new_container_cache_with_preferred_locations() {
        let pipeline = create_test_pipeline();
        let global_endpoint_manager = create_test_endpoint_manager_with_locations();
        let cache = ContainerCache::new(pipeline, global_endpoint_manager);

        // Verify the cache can be cloned (Debug trait is implemented)
        let cloned_cache = cache.clone();
        assert!(std::mem::size_of_val(&cloned_cache) > 0);
    }

    #[tokio::test]
    async fn remove_by_id_idempotency() {
        // Test that removing the same item multiple times is safe
        let pipeline = create_test_pipeline();
        let global_endpoint_manager = create_test_endpoint_manager();
        let cache = ContainerCache::new(pipeline, global_endpoint_manager);
        let container_id = "test-container";

        // Remove the same ID multiple times
        cache.remove_by_id(container_id).await;
        cache.remove_by_id(container_id).await;
        cache.remove_by_id(container_id).await;

        // Test passes if no panic occurs
    }

    #[tokio::test]
    async fn container_cache_clone() {
        // Test that ContainerCache can be cloned properly
        let pipeline = create_test_pipeline();
        let global_endpoint_manager = create_test_endpoint_manager();
        let cache = ContainerCache::new(pipeline, global_endpoint_manager);

        let cloned_cache = cache.clone();

        // Both should be valid instances
        cache.remove_by_id("test1").await;
        cloned_cache.remove_by_id("test2").await;
    }

    #[tokio::test]
    async fn remove_by_id_with_different_ids() {
        // Test removing different container IDs
        let pipeline = create_test_pipeline();
        let global_endpoint_manager = create_test_endpoint_manager();
        let cache = ContainerCache::new(pipeline, global_endpoint_manager);

        cache.remove_by_id("container1").await;
        cache.remove_by_id("container2").await;
        cache.remove_by_id("container-with-dashes").await;
        cache.remove_by_id("container_with_underscores").await;

        // Test passes if no panic occurs
    }
}
