//! Resource registry for MCP servers
//!
//! Provides registration and reading of MCP resources with URI templates and retry logic

use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::{future::Future, pin::Pin};

use chrono_machines::{BackoffStrategy, ExponentialBackoff};
use dashmap::DashMap;
use rand::SeedableRng;
use rand::rngs::SmallRng;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::sync::mpsc;

use crate::content::resource::ResourceContent;
use crate::server::session::Session;
use crate::server::visibility::{ExecutionContext, VisibilityContext};
use crate::transport::traits::JsonRpcNotification;

/// Resource execution errors
#[derive(Debug, Error)]
pub enum ResourceError {
    /// Resource not found
    #[error("Resource not found: {0}")]
    NotFound(String),

    /// Invalid URI
    #[error("Invalid URI: {0}")]
    InvalidUri(String),

    /// Read error (retryable)
    #[error("Read error: {0}")]
    Read(String),

    /// Internal error
    #[error("Internal error: {0}")]
    Internal(String),

    /// Retry exhausted
    #[error("Retry exhausted after {attempts} attempts: {message}")]
    RetryExhausted { attempts: u8, message: String },
}

/// Type alias for resource lookup results to avoid type complexity warnings
pub type ResourceLookupResult = Result<(Arc<dyn Resource>, HashMap<String, String>), ResourceError>;

impl ResourceError {
    /// Check if this error is retryable
    pub fn is_retryable(&self) -> bool {
        matches!(self, Self::Read(_) | Self::Internal(_))
    }
}

/// Retry configuration for resources
#[derive(Debug, Clone)]
pub struct ResourceRetryConfig {
    /// Maximum number of retry attempts
    pub max_attempts: u8,
    /// Base delay in milliseconds
    pub base_delay_ms: u64,
    /// Exponential multiplier
    pub multiplier: f64,
    /// Maximum delay cap in milliseconds
    pub max_delay_ms: u64,
    /// Jitter factor (0.0 = no jitter, 1.0 = full jitter)
    pub jitter_factor: f64,
}

impl Default for ResourceRetryConfig {
    fn default() -> Self {
        Self {
            max_attempts: 3,
            base_delay_ms: 100,
            multiplier: 2.0,
            max_delay_ms: 10_000,
            jitter_factor: 1.0, // Full jitter by default
        }
    }
}

/// Resource metadata for listing
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceInfo {
    /// Resource URI or URI template
    pub uri: String,

    /// Resource name
    pub name: String,

    /// Resource description
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,

    /// MIME type
    #[serde(skip_serializing_if = "Option::is_none", rename = "mimeType")]
    pub mime_type: Option<String>,
}

/// Resource template metadata for listing
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceTemplateInfo {
    /// URI template with placeholders (e.g., "file:///{path}")
    pub uri_template: String,

    /// Template name
    pub name: String,

    /// Optional display title
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,

    /// Template description
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,

    /// MIME type for resources matching this template
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mime_type: Option<String>,
}

/// Boxed future for resource/template reads.
pub type ResourceReadFuture<'a> =
    Pin<Box<dyn Future<Output = Result<Vec<ResourceContent>, ResourceError>> + Send + 'a>>;

/// Resource trait for implementing MCP resources
pub trait Resource: Send + Sync {
    /// Get resource URI or URI template
    fn uri(&self) -> &str;

    /// Get resource name
    fn name(&self) -> &str;

    /// Get resource description
    fn description(&self) -> Option<&str> {
        None
    }

    /// Get MIME type
    fn mime_type(&self) -> Option<&str> {
        None
    }

    /// Check if this resource should be visible in the given context
    ///
    /// Override this to implement contextual visibility. The default implementation
    /// always returns true (always visible).
    fn is_visible(&self, _ctx: &VisibilityContext) -> bool {
        true
    }

    /// Read the resource with execution context
    ///
    /// The execution context provides access to:
    /// - `ctx.uri_params`: Parameters extracted from URI template
    /// - `ctx.session`: Current session (roles, state, client info)
    /// - `ctx.environment`: Optional environment state
    ///
    /// Returns ResourceContent which automatically includes uri and mimeType.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// async fn read(&self, ctx: ExecutionContext<'_>) -> Result<Vec<ResourceContent>, ResourceError> {
    ///     let id = ctx.get_uri_param("id").unwrap_or("unknown");
    ///     let content = format!("Resource {}", id);
    ///
    ///     Ok(vec![self.text_content(content)])
    /// }
    /// ```
    fn read<'a>(&'a self, ctx: ExecutionContext<'a>) -> ResourceReadFuture<'a>;

    /// Helper: Create a text ResourceContent with this resource's URI and mime_type
    ///
    /// This is a convenience method that automatically includes the resource's
    /// URI and MIME type in the response.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// async fn read(&self, ctx: ExecutionContext<'_>) -> Result<Vec<ResourceContent>, ResourceError> {
    ///     Ok(vec![self.text_content("Resource content here")])
    /// }
    /// ```
    fn text_content(&self, text: &str) -> ResourceContent {
        ResourceContent {
            uri: self.uri().to_string(),
            mime_type: self.mime_type().map(|s| s.to_string()),
            text: Some(text.to_string()),
            blob: None,
        }
    }

    /// Helper: Create a blob ResourceContent with this resource's URI and mime_type
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// async fn read(&self, ctx: ExecutionContext<'_>) -> Result<Vec<ResourceContent>, ResourceError> {
    ///     Ok(vec![self.blob_content(base64_data)])
    /// }
    /// ```
    fn blob_content(&self, data: &str) -> ResourceContent {
        ResourceContent {
            uri: self.uri().to_string(),
            mime_type: self.mime_type().map(|s| s.to_string()),
            text: None,
            blob: Some(data.to_string()),
        }
    }
}

/// Resource template trait for implementing URI templates
///
/// Resource templates allow dynamic resources with URI patterns like `file:///{path}`.
/// The template defines the pattern, and resources matching it provide the actual data.
pub trait ResourceTemplate: Send + Sync {
    /// Get URI template pattern (e.g., "file:///{path}")
    fn uri_template(&self) -> &str;

    /// Get template name
    fn name(&self) -> &str;

    /// Get optional display title
    fn title(&self) -> Option<&str> {
        None
    }

    /// Get template description
    fn description(&self) -> Option<&str> {
        None
    }

    /// Get MIME type for resources matching this template
    fn mime_type(&self) -> Option<&str> {
        None
    }

    /// Check if this template should be visible in the given context
    fn is_visible(&self, _ctx: &VisibilityContext) -> bool {
        true
    }

    /// Read a resource matching this template
    ///
    /// The URI parameters are extracted from the concrete URI and passed via `ctx.uri_params`.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// async fn read(&self, ctx: ExecutionContext<'_>) -> Result<Vec<ResourceContent>, ResourceError> {
    ///     let path = ctx.get_uri_param("path").ok_or_else(||
    ///         ResourceError::InvalidUri("Missing path parameter".into())
    ///     )?;
    ///
    ///     let content = std::fs::read_to_string(path)
    ///         .map_err(|e| ResourceError::Read(e.to_string()))?;
    ///
    ///     Ok(vec![ResourceContent::text(
    ///         format!("file:///{}", path),
    ///         content
    ///     )])
    /// }
    /// ```
    fn read<'a>(&'a self, ctx: ExecutionContext<'a>) -> ResourceReadFuture<'a>;
}

/// Dynamic resource that wraps a template for on-demand resource creation
struct DynamicTemplateResource {
    template: Arc<dyn ResourceTemplate>,
    uri: String,
}

impl Resource for DynamicTemplateResource {
    fn uri(&self) -> &str {
        &self.uri
    }

    fn name(&self) -> &str {
        self.template.name()
    }

    fn description(&self) -> Option<&str> {
        self.template.description()
    }

    fn mime_type(&self) -> Option<&str> {
        self.template.mime_type()
    }

    fn is_visible(&self, ctx: &VisibilityContext) -> bool {
        self.template.is_visible(ctx)
    }

    fn read<'a>(&'a self, ctx: ExecutionContext<'a>) -> ResourceReadFuture<'a> {
        Box::pin(async move { self.template.read(ctx).await })
    }
}

/// Resource manager for managing available resources with retry logic
#[derive(Clone)]
pub struct ResourceManager {
    resources: Arc<DashMap<String, Arc<dyn Resource>>>,
    templates: Arc<DashMap<String, Arc<dyn ResourceTemplate>>>,
    retry_config: Arc<RwLock<ResourceRetryConfig>>,
    notification_tx: Option<mpsc::UnboundedSender<JsonRpcNotification>>,
}

impl ResourceManager {
    /// Create new resource manager
    pub fn new() -> Self {
        Self {
            resources: Arc::new(DashMap::new()),
            templates: Arc::new(DashMap::new()),
            retry_config: Arc::new(RwLock::new(ResourceRetryConfig::default())),
            notification_tx: None,
        }
    }

    /// Create resource manager with notification channel
    pub fn with_notifications(notification_tx: mpsc::UnboundedSender<JsonRpcNotification>) -> Self {
        Self {
            resources: Arc::new(DashMap::new()),
            templates: Arc::new(DashMap::new()),
            retry_config: Arc::new(RwLock::new(ResourceRetryConfig::default())),
            notification_tx: Some(notification_tx),
        }
    }

    /// Set notification channel
    pub fn set_notification_tx(&mut self, tx: mpsc::UnboundedSender<JsonRpcNotification>) {
        self.notification_tx = Some(tx);
    }

    /// Configure retry behavior
    pub fn set_retry_config(&self, config: ResourceRetryConfig) {
        if let Ok(mut cfg) = self.retry_config.write() {
            *cfg = config;
        }
    }

    /// Send notification
    fn send_notification(&self, method: &str, params: Option<serde_json::Value>) {
        if let Some(tx) = &self.notification_tx {
            let notification = JsonRpcNotification::new(method, params);
            let _ = tx.send(notification);
        }
    }

    /// Notify resources list changed
    fn notify_resources_changed(&self) {
        self.send_notification("notifications/resources/list_changed", None);
    }

    /// Send logging message notification (visible to LLM)
    fn notify_message(&self, level: &str, logger: &str, message: &str) {
        self.send_notification(
            "notifications/message",
            Some(serde_json::json!({
                "level": level,
                "logger": logger,
                "data": message
            })),
        );
    }

    /// Register a resource
    pub fn register<R: Resource + 'static>(&self, resource: R) {
        let uri = resource.uri().to_string();
        self.resources.insert(uri, Arc::new(resource));
    }

    /// Register a boxed resource
    pub fn register_boxed(&self, resource: Arc<dyn Resource>) {
        let uri = resource.uri().to_string();
        self.resources.insert(uri, resource);
    }

    /// Register a resource template
    pub fn register_template<T: ResourceTemplate + 'static>(&self, template: T) {
        let name = template.name().to_string();
        self.templates.insert(name, Arc::new(template));
    }

    /// Register a boxed resource template
    pub fn register_template_boxed(&self, template: Arc<dyn ResourceTemplate>) {
        let name = template.name().to_string();
        self.templates.insert(name, template);
    }

    /// Get a template by name
    pub fn get_template(&self, name: &str) -> Option<Arc<dyn ResourceTemplate>> {
        self.templates.get(name).map(|t| Arc::clone(&t))
    }

    /// Get a resource by URI
    pub fn get(&self, uri: &str) -> Option<Arc<dyn Resource>> {
        self.resources.get(uri).map(|r| Arc::clone(&r))
    }

    /// List all registered resources
    pub fn list(&self) -> Vec<ResourceInfo> {
        self.resources
            .iter()
            .map(|entry| {
                let resource = entry.value();
                ResourceInfo {
                    uri: resource.uri().to_string(),
                    name: resource.name().to_string(),
                    description: resource.description().map(|s| s.to_string()),
                    mime_type: resource.mime_type().map(|s| s.to_string()),
                }
            })
            .collect()
    }

    /// List all resource templates
    pub fn list_templates(&self) -> Vec<ResourceTemplateInfo> {
        self.templates
            .iter()
            .map(|entry| {
                let template = entry.value();
                ResourceTemplateInfo {
                    uri_template: template.uri_template().to_string(),
                    name: template.name().to_string(),
                    title: template.title().map(|s| s.to_string()),
                    description: template.description().map(|s| s.to_string()),
                    mime_type: template.mime_type().map(|s| s.to_string()),
                }
            })
            .collect()
    }

    /// List templates visible in the given session/context
    pub fn list_templates_for_session(
        &self,
        _session: &Session,
        ctx: &VisibilityContext<'_>,
    ) -> Vec<ResourceTemplateInfo> {
        self.templates
            .iter()
            .filter(|entry| entry.value().is_visible(ctx))
            .map(|entry| {
                let template = entry.value();
                ResourceTemplateInfo {
                    uri_template: template.uri_template().to_string(),
                    name: template.name().to_string(),
                    title: template.title().map(|s| s.to_string()),
                    description: template.description().map(|s| s.to_string()),
                    mime_type: template.mime_type().map(|s| s.to_string()),
                }
            })
            .collect()
    }

    /// Read a resource by URI with parameters and retry logic
    pub async fn read(
        &self,
        uri: &str,
        params: HashMap<String, String>,
        session: &Session,
        logger: &crate::logging::McpLogger,
    ) -> Result<Vec<ResourceContent>, ResourceError> {
        // Find the resource first
        let (resource, combined_params) = self.find_any_resource(uri, params)?;
        self.read_resolved_resource(uri, resource, combined_params, session, logger, None)
            .await
    }

    /// Find a resource by URI (exact match or template match).
    ///
    /// When multiple templates match the same URI, the most specific included
    /// one wins (see [`template_specificity`]). This means a literal path like
    /// `scheme:///foo/{id}/bar` beats a greedy one like `scheme:///foo/{id}`,
    /// regardless of registration order or underlying hash-map iteration order.
    fn find_resource<F>(
        &self,
        uri: &str,
        params: HashMap<String, String>,
        mut include: F,
    ) -> ResourceLookupResult
    where
        F: FnMut(&Arc<dyn Resource>) -> bool,
    {
        // Try exact match first
        if let Some(resource) = self.get(uri) {
            if include(&resource) {
                return Ok((resource, params));
            }
        }

        let mut best_score: Option<(usize, usize)> = None;
        let mut best: Option<(Arc<dyn Resource>, HashMap<String, String>)> = None;

        // Template matches in static resources
        for entry in self.resources.iter() {
            let template_uri = entry.key();
            if let Some(extracted_params) = self.match_template(template_uri, uri) {
                let resource = Arc::clone(entry.value());
                if !include(&resource) {
                    continue;
                }

                let score = Self::template_specificity(template_uri);
                if best_score.map_or(true, |best| score > best) {
                    best_score = Some(score);
                    let mut combined_params = params.clone();
                    combined_params.extend(extracted_params);
                    best = Some((resource, combined_params));
                }
            }
        }

        // Template matches in resource templates
        for entry in self.templates.iter() {
            let template = entry.value();
            let template_uri = template.uri_template();
            if let Some(extracted_params) = self.match_template(template_uri, uri) {
                let resource: Arc<dyn Resource> = Arc::new(DynamicTemplateResource {
                    template: Arc::clone(template),
                    uri: uri.to_string(),
                });
                if !include(&resource) {
                    continue;
                }

                let score = Self::template_specificity(template_uri);
                if best_score.map_or(true, |best| score > best) {
                    best_score = Some(score);
                    let mut combined_params = params.clone();
                    combined_params.extend(extracted_params);
                    best = Some((resource, combined_params));
                }
            }
        }

        best.ok_or_else(|| ResourceError::NotFound(uri.to_string()))
    }

    fn find_any_resource(
        &self,
        uri: &str,
        params: HashMap<String, String>,
    ) -> ResourceLookupResult {
        self.find_resource(uri, params, |_| true)
    }

    fn find_visible_resource(
        &self,
        uri: &str,
        params: HashMap<String, String>,
        visibility_ctx: &VisibilityContext<'_>,
    ) -> ResourceLookupResult {
        self.find_resource(uri, params, |resource| resource.is_visible(visibility_ctx))
    }

    /// Match URI against template and extract parameters
    /// Template format: "scheme://path/{param}/more/{param2}"
    /// If the last part is a parameter, it greedily matches the rest of the URI
    fn match_template(&self, template: &str, uri: &str) -> Option<HashMap<String, String>> {
        let template_parts: Vec<&str> = template.split('/').collect();
        let uri_parts: Vec<&str> = uri.split('/').collect();

        // Check if template has fewer parts (only valid if last part is a parameter)
        if template_parts.len() > uri_parts.len() {
            return None;
        }

        let mut params = HashMap::new();
        let last_idx = template_parts.len() - 1;

        for (i, template_part) in template_parts.iter().enumerate() {
            if template_part.starts_with('{') && template_part.ends_with('}') {
                // Extract parameter name, stripping RFC 6570 operators (+, #, ., /, ;, ?, &)
                // e.g., {+url} → "url", {/path} → "path"
                let param_name = template_part[1..template_part.len() - 1]
                    .trim_start_matches(&['+', '#', '.', '/', ';', '?', '&']);

                if i == last_idx {
                    // Last parameter: greedily match remaining URI parts
                    let remaining = uri_parts[i..].join("/");
                    params.insert(param_name.to_string(), remaining);
                    break;
                } else {
                    // Middle parameter: match single part
                    if i >= uri_parts.len() {
                        return None;
                    }
                    params.insert(param_name.to_string(), uri_parts[i].to_string());
                }
            } else if i >= uri_parts.len() || template_part != &uri_parts[i] {
                // Static part doesn't match or URI too short
                return None;
            }
        }

        // If last template part wasn't a greedy parameter, ensure lengths match exactly
        let last_was_greedy_param = template_parts
            .last()
            .is_some_and(|p| p.starts_with('{') && p.ends_with('}'));

        if !last_was_greedy_param && template_parts.len() != uri_parts.len() {
            return None;
        }

        Some(params)
    }

    /// Score a URI template by specificity — higher is more specific.
    ///
    /// The tuple is compared lexicographically:
    /// - More literal segments always win.
    /// - If literal counts tie, more required path depth before any greedy tail wins.
    ///
    /// This keeps literal matches dominant while still preferring
    /// `scheme:///posts/{id}/{tab}` over `scheme:///posts/{id}` for
    /// `scheme:///posts/123/thread`.
    fn template_specificity(template: &str) -> (usize, usize) {
        let parts: Vec<&str> = template.split('/').collect();
        let literal_segments = parts
            .iter()
            .filter(|part| !(part.starts_with('{') && part.ends_with('}')))
            .count();
        let required_segments = match parts.last() {
            Some(last) if last.starts_with('{') && last.ends_with('}') => parts.len() - 1,
            Some(_) => parts.len(),
            None => 0,
        };

        (literal_segments, required_segments)
    }

    // ==================== Session-Aware Methods ====================

    /// List resources visible to a specific session
    ///
    /// Resolution order:
    /// 1. Session overrides (replace global resources with session-specific implementations)
    /// 2. Session extras (additional resources added to session)
    /// 3. Global resources (filtered by hidden list and visibility predicate)
    pub fn list_for_session(
        &self,
        session: &Session,
        ctx: &VisibilityContext<'_>,
    ) -> Vec<ResourceInfo> {
        let mut resources = std::collections::HashMap::new();

        // 1. Add global resources (filtered by hidden and visibility)
        for entry in self.resources.iter() {
            let uri = entry.key().clone();
            if !session.is_resource_hidden(&uri) {
                let resource = entry.value();
                if resource.is_visible(ctx) {
                    resources.insert(
                        uri,
                        ResourceInfo {
                            uri: resource.uri().to_string(),
                            name: resource.name().to_string(),
                            description: resource.description().map(|s| s.to_string()),
                            mime_type: resource.mime_type().map(|s| s.to_string()),
                        },
                    );
                }
            }
        }

        // 2. Add session extras
        for entry in session.resource_extras().iter() {
            let uri = entry.key().clone();
            let resource = entry.value();
            if resource.is_visible(ctx) {
                resources.insert(
                    uri,
                    ResourceInfo {
                        uri: resource.uri().to_string(),
                        name: resource.name().to_string(),
                        description: resource.description().map(|s| s.to_string()),
                        mime_type: resource.mime_type().map(|s| s.to_string()),
                    },
                );
            }
        }

        // 3. Apply session overrides
        for entry in session.resource_overrides().iter() {
            let uri = entry.key().clone();
            let resource = entry.value();
            if resource.is_visible(ctx) {
                resources.insert(
                    uri,
                    ResourceInfo {
                        uri: resource.uri().to_string(),
                        name: resource.name().to_string(),
                        description: resource.description().map(|s| s.to_string()),
                        mime_type: resource.mime_type().map(|s| s.to_string()),
                    },
                );
            }
        }

        resources.into_values().collect()
    }

    /// Read a resource with session context
    ///
    /// Resolution order:
    /// 1. Check session overrides
    /// 2. Check session extras
    /// 3. Check session hidden
    /// 4. Check visibility predicate
    /// 5. Fall back to global registry with retry logic
    pub async fn read_for_session(
        &self,
        uri: &str,
        params: HashMap<String, String>,
        session: &Session,
        logger: &crate::logging::McpLogger,
        visibility_ctx: &VisibilityContext<'_>,
    ) -> Result<Vec<ResourceContent>, ResourceError> {
        // Create execution context (reuse environment from visibility context)
        let exec_ctx = match visibility_ctx.environment {
            Some(env) => ExecutionContext::for_resource_with_environment(
                params.clone(),
                session,
                logger,
                env,
            ),
            None => ExecutionContext::for_resource(params.clone(), session, logger),
        };

        // 1. Check session override first
        if let Some(resource) = session.get_resource_override(uri) {
            if !resource.is_visible(visibility_ctx) {
                return Err(ResourceError::NotFound(uri.to_string()));
            }
            return resource.read(exec_ctx).await;
        }

        // 2. Check session extras
        if let Some(resource) = session.get_resource_extra(uri) {
            if !resource.is_visible(visibility_ctx) {
                return Err(ResourceError::NotFound(uri.to_string()));
            }
            return resource.read(exec_ctx).await;
        }

        // 3. Check if hidden in session
        if session.is_resource_hidden(uri) {
            return Err(ResourceError::NotFound(uri.to_string()));
        }

        // 4. Check global registry with visibility check
        if let Some(resource) = self.get(uri)
            && !resource.is_visible(visibility_ctx)
        {
            return Err(ResourceError::NotFound(uri.to_string()));
        }

        // 5. Resolve among visible global candidates and read with retry logic
        let (resource, combined_params) =
            self.find_visible_resource(uri, params, visibility_ctx)?;
        self.read_resolved_resource(
            uri,
            resource,
            combined_params,
            session,
            logger,
            visibility_ctx.environment,
        )
        .await
    }

    async fn read_resolved_resource(
        &self,
        uri: &str,
        resource: Arc<dyn Resource>,
        combined_params: HashMap<String, String>,
        session: &Session,
        logger: &crate::logging::McpLogger,
        environment: Option<&dyn crate::server::visibility::Environment>,
    ) -> Result<Vec<ResourceContent>, ResourceError> {
        let retry_config = self
            .retry_config
            .read()
            .map(|c| c.clone())
            .unwrap_or_default();

        let backoff = ExponentialBackoff::default()
            .max_attempts(retry_config.max_attempts)
            .base_delay_ms(retry_config.base_delay_ms)
            .multiplier(retry_config.multiplier)
            .max_delay_ms(retry_config.max_delay_ms)
            .jitter_factor(retry_config.jitter_factor);

        let mut rng = {
            let mut seed_rng = rand::rng();
            SmallRng::from_rng(&mut seed_rng)
        };
        let mut attempt: u8 = 1;

        loop {
            let ctx = match environment {
                Some(env) => ExecutionContext::for_resource_with_environment(
                    combined_params.clone(),
                    session,
                    logger,
                    env,
                ),
                None => ExecutionContext::for_resource(combined_params.clone(), session, logger),
            };

            match resource.read(ctx).await {
                Ok(content) => {
                    if attempt > 1 {
                        self.notify_message(
                            "info",
                            "chrono-machines",
                            &format!("Resource '{}' succeeded after {} attempts", uri, attempt),
                        );
                    }
                    return Ok(content);
                }
                Err(e) => {
                    if !e.is_retryable() {
                        return Err(e);
                    }

                    if !backoff.should_retry(attempt) {
                        self.notify_resources_changed();
                        self.notify_message(
                            "warning",
                            "chrono-machines",
                            &format!(
                                "Resource '{}' failed after {} attempts: {}",
                                uri, attempt, e
                            ),
                        );

                        return Err(ResourceError::RetryExhausted {
                            attempts: attempt,
                            message: e.to_string(),
                        });
                    }

                    if let Some(delay_ms) = backoff.delay(attempt, &mut rng) {
                        tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await;
                    }

                    attempt = attempt.saturating_add(1);
                }
            }
        }
    }

    /// Get number of registered resources
    pub fn len(&self) -> usize {
        self.resources.len()
    }

    /// Check if manager is empty
    pub fn is_empty(&self) -> bool {
        self.resources.is_empty()
    }
}

impl Default for ResourceManager {
    fn default() -> Self {
        Self::new()
    }
}

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

    // Example static resource for testing
    struct HelloResource;

    impl Resource for HelloResource {
        fn uri(&self) -> &str {
            "test://hello"
        }

        fn name(&self) -> &str {
            "hello"
        }

        fn description(&self) -> Option<&str> {
            Some("Returns a greeting")
        }

        fn mime_type(&self) -> Option<&str> {
            Some("text/plain")
        }

        fn read<'a>(&'a self, _ctx: ExecutionContext<'a>) -> ResourceReadFuture<'a> {
            Box::pin(async move { Ok(vec![self.text_content("Hello, World!")]) })
        }
    }

    // Example template resource for testing
    struct UserResource;

    impl Resource for UserResource {
        fn uri(&self) -> &str {
            "test://users/{id}"
        }

        fn name(&self) -> &str {
            "user"
        }

        fn description(&self) -> Option<&str> {
            Some("Returns user information")
        }

        fn read<'a>(&'a self, ctx: ExecutionContext<'a>) -> ResourceReadFuture<'a> {
            Box::pin(async move {
                let id = ctx.get_uri_param("id").ok_or_else(|| {
                    ResourceError::InvalidUri("Missing 'id' parameter".to_string())
                })?;

                Ok(vec![self.text_content(&format!("User ID: {}", id))])
            })
        }
    }

    #[test]
    fn test_manager_creation() {
        let manager = ResourceManager::new();
        assert!(manager.is_empty());
    }

    #[test]
    fn test_resource_registration() {
        let manager = ResourceManager::new();
        manager.register(HelloResource);

        assert_eq!(manager.len(), 1);
        assert!(!manager.is_empty());
    }

    #[test]
    fn test_get_resource() {
        let manager = ResourceManager::new();
        manager.register(HelloResource);

        let resource = manager.get("test://hello");
        assert!(resource.is_some());
        assert_eq!(resource.unwrap().name(), "hello");

        let missing = manager.get("test://nonexistent");
        assert!(missing.is_none());
    }

    #[test]
    fn test_list_resources() {
        let manager = ResourceManager::new();
        manager.register(HelloResource);

        let resources = manager.list();
        assert_eq!(resources.len(), 1);
        assert_eq!(resources[0].uri, "test://hello");
        assert_eq!(resources[0].name, "hello");
        assert_eq!(
            resources[0].description,
            Some("Returns a greeting".to_string())
        );
        assert_eq!(resources[0].mime_type, Some("text/plain".to_string()));
    }

    #[tokio::test]
    async fn test_read_static_resource() {
        let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
        let logger = crate::logging::McpLogger::new(_tx, "test");
        let manager = ResourceManager::new();
        manager.register(HelloResource);
        let session = Session::new();

        let result = manager
            .read("test://hello", HashMap::new(), &session, &logger)
            .await
            .unwrap();
        assert_eq!(result.len(), 1);
    }

    #[tokio::test]
    async fn test_read_missing_resource() {
        let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
        let logger = crate::logging::McpLogger::new(_tx, "test");
        let manager = ResourceManager::new();
        let session = Session::new();

        let result = manager
            .read("test://nonexistent", HashMap::new(), &session, &logger)
            .await;
        assert!(matches!(result, Err(ResourceError::NotFound(_))));
    }

    #[tokio::test]
    async fn test_template_matching() {
        let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
        let logger = crate::logging::McpLogger::new(_tx, "test");
        let manager = ResourceManager::new();
        manager.register(UserResource);
        let session = Session::new();

        // Test template parameter extraction
        let result = manager
            .read("test://users/123", HashMap::new(), &session, &logger)
            .await
            .unwrap();
        assert_eq!(result.len(), 1);
    }

    #[test]
    fn test_template_matching_internal() {
        let manager = ResourceManager::new();

        // Test exact template matching
        let params = manager.match_template("test://users/{id}", "test://users/123");
        assert!(params.is_some());
        let params = params.unwrap();
        assert_eq!(params.get("id"), Some(&"123".to_string()));

        // Test multiple parameters
        let params = manager.match_template(
            "test://org/{org}/repo/{repo}",
            "test://org/myorg/repo/myrepo",
        );
        assert!(params.is_some());
        let params = params.unwrap();
        assert_eq!(params.get("org"), Some(&"myorg".to_string()));
        assert_eq!(params.get("repo"), Some(&"myrepo".to_string()));

        // Test non-matching
        let params = manager.match_template("test://users/{id}", "test://posts/123");
        assert!(params.is_none());

        // Test greedy matching (last parameter matches remaining segments)
        let params = manager.match_template("test://users/{id}", "test://users/123/extra");
        assert!(params.is_some());
        let params = params.unwrap();
        assert_eq!(params.get("id"), Some(&"123/extra".to_string()));
    }

    #[test]
    fn test_rfc6570_operator_stripping() {
        let manager = ResourceManager::new();

        // Test reserved expansion operator (+) - the most common RFC 6570 operator
        // Template: file:///{+url} should extract param name "url", not "+url"
        // RFC 6570: {+var} allows reserved characters to pass through unencoded
        let params = manager.match_template("file:///{+url}", "file:///path/to/file.txt");
        assert!(params.is_some(), "Template with {{+url}} should match");
        let params = params.unwrap();
        assert_eq!(
            params.get("url"),
            Some(&"path/to/file.txt".to_string()),
            "Parameter name should be 'url', not '+url'"
        );
        assert!(
            params.get("+url").is_none(),
            "Operator '+' should be stripped from parameter name"
        );

        // Test with multiple + operators in different positions
        let params = manager.match_template(
            "test://org/{+org}/repo/{+repo}",
            "test://org/my-org/repo/my-repo",
        );
        assert!(params.is_some());
        let params = params.unwrap();
        assert_eq!(params.get("org"), Some(&"my-org".to_string()));
        assert_eq!(params.get("repo"), Some(&"my-repo".to_string()));

        // Test that standard parameters (without operators) still work
        let params = manager.match_template("test://user/{id}/profile", "test://user/123/profile");
        assert!(params.is_some());
        let params = params.unwrap();
        assert_eq!(params.get("id"), Some(&"123".to_string()));
    }

    #[tokio::test]
    async fn test_resource_non_matching_template() {
        let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
        let logger = crate::logging::McpLogger::new(_tx, "test");
        let manager = ResourceManager::new();
        manager.register(UserResource);
        let session = Session::new();

        // This won't match because "posts" != "users"
        let result = manager
            .read("test://posts/123", HashMap::new(), &session, &logger)
            .await;
        assert!(matches!(result, Err(ResourceError::NotFound(_))));
    }

    #[tokio::test]
    async fn test_resource_greedy_matching() {
        let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
        let logger = crate::logging::McpLogger::new(_tx, "test");
        let manager = ResourceManager::new();
        manager.register(UserResource);
        let session = Session::new();

        // The {id} parameter greedily matches the remaining segments
        let result = manager
            .read("test://users/123/extra", HashMap::new(), &session, &logger)
            .await;
        assert!(result.is_ok());
        let content = result.unwrap();
        assert_eq!(content.len(), 1);
        // Verify the id parameter captured the greedy match
        assert!(content[0].text.as_ref().unwrap().contains("123/extra"));
    }

    // ==================== Resource Template Tests ====================

    // Example template for testing
    struct FileTemplate;

    impl ResourceTemplate for FileTemplate {
        fn uri_template(&self) -> &str {
            "file:///{path}"
        }

        fn name(&self) -> &str {
            "project_files"
        }

        fn title(&self) -> Option<&str> {
            Some("Project Files")
        }

        fn description(&self) -> Option<&str> {
            Some("Access files in the project directory")
        }

        fn mime_type(&self) -> Option<&str> {
            Some("application/octet-stream")
        }

        fn read<'a>(&'a self, ctx: ExecutionContext<'a>) -> ResourceReadFuture<'a> {
            Box::pin(async move {
                let path = ctx.get_uri_param("path").ok_or_else(|| {
                    ResourceError::InvalidUri("Missing 'path' parameter".to_string())
                })?;

                Ok(vec![ResourceContent::text(
                    format!("file:///{}", path),
                    format!("Mock content for file: {}", path),
                )])
            })
        }
    }

    // Template with visibility predicate
    struct AdminOnlyTemplate;

    impl ResourceTemplate for AdminOnlyTemplate {
        fn uri_template(&self) -> &str {
            "admin:///{resource}"
        }

        fn name(&self) -> &str {
            "admin_resources"
        }

        fn is_visible(&self, ctx: &VisibilityContext) -> bool {
            ctx.has_role("admin")
        }

        fn read<'a>(&'a self, _ctx: ExecutionContext<'a>) -> ResourceReadFuture<'a> {
            Box::pin(async move {
                Ok(vec![ResourceContent::text(
                    "admin:///test",
                    "Admin content",
                )])
            })
        }
    }

    #[test]
    fn test_template_registration() {
        let manager = ResourceManager::new();
        manager.register_template(FileTemplate);

        let template = manager.get_template("project_files");
        assert!(template.is_some());
        assert_eq!(template.unwrap().name(), "project_files");
    }

    #[test]
    fn test_get_template() {
        let manager = ResourceManager::new();
        manager.register_template(FileTemplate);

        let template = manager.get_template("project_files");
        assert!(template.is_some());
        assert_eq!(template.unwrap().uri_template(), "file:///{path}");

        let missing = manager.get_template("nonexistent");
        assert!(missing.is_none());
    }

    #[test]
    fn test_list_templates() {
        let manager = ResourceManager::new();
        manager.register_template(FileTemplate);

        let templates = manager.list_templates();
        assert_eq!(templates.len(), 1);
        assert_eq!(templates[0].uri_template, "file:///{path}");
        assert_eq!(templates[0].name, "project_files");
        assert_eq!(templates[0].title, Some("Project Files".to_string()));
        assert_eq!(
            templates[0].description,
            Some("Access files in the project directory".to_string())
        );
        assert_eq!(
            templates[0].mime_type,
            Some("application/octet-stream".to_string())
        );
    }

    #[test]
    fn test_list_templates_for_session_visibility() {
        let manager = ResourceManager::new();
        manager.register_template(FileTemplate);
        manager.register_template(AdminOnlyTemplate);

        // Regular session without admin role
        let session = Session::new();
        let ctx = VisibilityContext::new(&session);
        let templates = manager.list_templates_for_session(&session, &ctx);

        // Should only see FileTemplate, not AdminOnlyTemplate
        assert_eq!(templates.len(), 1);
        assert_eq!(templates[0].name, "project_files");

        // Admin session
        let admin_session = Session::new();
        admin_session.set_state("roles", serde_json::json!(["admin"]));
        let admin_ctx = VisibilityContext::new(&admin_session);
        let admin_templates = manager.list_templates_for_session(&admin_session, &admin_ctx);

        // Should see both templates
        assert_eq!(admin_templates.len(), 2);
        let names: Vec<_> = admin_templates.iter().map(|t| t.name.as_str()).collect();
        assert!(names.contains(&"project_files"));
        assert!(names.contains(&"admin_resources"));
    }

    #[test]
    fn test_template_info_serialization() {
        let manager = ResourceManager::new();
        manager.register_template(FileTemplate);

        let templates = manager.list_templates();
        let serialized = serde_json::to_value(&templates[0]).unwrap();

        // Verify MCP-compliant field names (camelCase)
        assert!(serialized.get("uriTemplate").is_some());
        assert_eq!(serialized["uriTemplate"], "file:///{path}");
        assert_eq!(serialized["name"], "project_files");
        assert_eq!(serialized["mimeType"], "application/octet-stream");

        // Verify snake_case fields are NOT present
        assert!(serialized.get("uri_template").is_none());
        assert!(serialized.get("mime_type").is_none());
    }

    #[tokio::test]
    async fn test_template_read_with_uri_params() {
        let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
        let logger = crate::logging::McpLogger::new(_tx, "test");
        let session = Session::new();
        let ctx = ExecutionContext::for_resource(
            vec![("path".to_string(), "src/main.rs".to_string())]
                .into_iter()
                .collect(),
            &session,
            &logger,
        );

        let template = FileTemplate;
        let result = template.read(ctx).await.unwrap();

        assert_eq!(result.len(), 1);
        assert_eq!(result[0].uri, "file:///src/main.rs");
        assert!(result[0].text.as_ref().unwrap().contains("src/main.rs"));
    }

    #[tokio::test]
    async fn test_template_read_missing_param() {
        let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
        let logger = crate::logging::McpLogger::new(_tx, "test");
        let session = Session::new();
        let ctx = ExecutionContext::for_resource(HashMap::new(), &session, &logger);

        let template = FileTemplate;
        let result = template.read(ctx).await;

        // Should fail with InvalidUri when path parameter is missing
        assert!(matches!(result, Err(ResourceError::InvalidUri(_))));
    }

    // ==================== Template Specificity Tests ====================

    #[test]
    fn test_template_specificity_prefers_literal_tails() {
        // `/a/{id}/b` is more specific than `/a/{id}` because of the literal
        // trailing segment. Both match `/a/123/b`, but only the former should
        // be chosen.
        let literal_tail = ResourceManager::template_specificity("scheme:///a/{id}/b");
        let greedy_tail = ResourceManager::template_specificity("scheme:///a/{id}");
        assert!(
            literal_tail > greedy_tail,
            "literal-tail template should outrank greedy-tail template: {:?} vs {:?}",
            literal_tail,
            greedy_tail
        );
    }

    #[test]
    fn test_template_specificity_more_literals_wins() {
        let more = ResourceManager::template_specificity("scheme:///a/b/c/{id}");
        let fewer = ResourceManager::template_specificity("scheme:///a/{id}");
        assert!(more > fewer);
    }

    #[test]
    fn test_template_specificity_more_required_depth_wins() {
        let deeper = ResourceManager::template_specificity("scheme:///posts/{id}/{tab}");
        let shallower = ResourceManager::template_specificity("scheme:///posts/{id}");
        assert!(deeper > shallower);
    }

    // Regression: card #1067.
    //
    // Two templates both match `posts/{id}/thread`, but only the more specific
    // one (with the literal `thread` suffix) is a legitimate match. The router
    // must not dispatch the URI to the generic `posts/{id}` handler just
    // because it happens to iterate the DashMap in that order first.
    //
    // Prior to the specificity-aware routing fix, this test would fail because
    // `posts/{id}` was picked first in hash-map iteration order, capturing
    // `id = "xjje9.../thread"` greedily.
    struct PostByIdTemplate;
    impl ResourceTemplate for PostByIdTemplate {
        fn uri_template(&self) -> &str {
            "agora:///posts/{post_id}"
        }
        fn name(&self) -> &str {
            "post"
        }
        fn read<'a>(&'a self, ctx: ExecutionContext<'a>) -> ResourceReadFuture<'a> {
            Box::pin(async move {
                let id = ctx.get_uri_param("post_id").unwrap_or_default();
                Ok(vec![ResourceContent::text(
                    format!("agora:///posts/{id}"),
                    format!("post={id}"),
                )])
            })
        }
    }

    struct PostThreadTemplate;
    impl ResourceTemplate for PostThreadTemplate {
        fn uri_template(&self) -> &str {
            "agora:///posts/{post_id}/thread"
        }
        fn name(&self) -> &str {
            "thread"
        }
        fn read<'a>(&'a self, ctx: ExecutionContext<'a>) -> ResourceReadFuture<'a> {
            Box::pin(async move {
                let id = ctx.get_uri_param("post_id").unwrap_or_default();
                Ok(vec![ResourceContent::text(
                    format!("agora:///posts/{id}/thread"),
                    format!("thread={id}"),
                )])
            })
        }
    }

    struct PostTabTemplate;
    impl ResourceTemplate for PostTabTemplate {
        fn uri_template(&self) -> &str {
            "agora:///posts/{post_id}/{tab}"
        }
        fn name(&self) -> &str {
            "post-tab"
        }
        fn read<'a>(&'a self, ctx: ExecutionContext<'a>) -> ResourceReadFuture<'a> {
            Box::pin(async move {
                let id = ctx.get_uri_param("post_id").unwrap_or_default();
                let tab = ctx.get_uri_param("tab").unwrap_or_default();
                Ok(vec![ResourceContent::text(
                    format!("agora:///posts/{id}/{tab}"),
                    format!("tab={id}:{tab}"),
                )])
            })
        }
    }

    struct AdminSecretFileTemplate;
    impl ResourceTemplate for AdminSecretFileTemplate {
        fn uri_template(&self) -> &str {
            "file:///{path}/secret"
        }
        fn name(&self) -> &str {
            "admin_secret_file"
        }
        fn is_visible(&self, ctx: &VisibilityContext) -> bool {
            ctx.has_role("admin")
        }
        fn read<'a>(&'a self, ctx: ExecutionContext<'a>) -> ResourceReadFuture<'a> {
            Box::pin(async move {
                let path = ctx.get_uri_param("path").unwrap_or_default();
                Ok(vec![ResourceContent::text(
                    format!("file:///{path}/secret"),
                    format!("secret={path}"),
                )])
            })
        }
    }

    #[tokio::test]
    async fn test_specific_template_wins_over_greedy_sibling() {
        let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
        let logger = crate::logging::McpLogger::new(_tx, "test");
        let manager = ResourceManager::new();
        manager.register_template(PostByIdTemplate);
        manager.register_template(PostThreadTemplate);
        let session = Session::new();

        // This URI matches BOTH templates. The thread template is more specific
        // (literal `/thread` suffix), so it must win.
        let content = manager
            .read(
                "agora:///posts/xjje9tppqbdn9jodc84z6zdw9c/thread",
                HashMap::new(),
                &session,
                &logger,
            )
            .await
            .expect("thread URI should resolve");
        assert_eq!(content.len(), 1);
        let text = content[0].text.as_ref().unwrap();
        assert!(
            text.starts_with("thread="),
            "expected thread template to win, got {text:?}"
        );
        assert_eq!(text, "thread=xjje9tppqbdn9jodc84z6zdw9c");

        // And the non-thread URI still resolves to the generic template.
        let content = manager
            .read(
                "agora:///posts/xjje9tppqbdn9jodc84z6zdw9c",
                HashMap::new(),
                &session,
                &logger,
            )
            .await
            .expect("plain post URI should resolve");
        let text = content[0].text.as_ref().unwrap();
        assert_eq!(text, "post=xjje9tppqbdn9jodc84z6zdw9c");
    }

    #[tokio::test]
    async fn test_parameter_only_deeper_template_wins_over_greedy_sibling() {
        let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
        let logger = crate::logging::McpLogger::new(_tx, "test");
        let manager = ResourceManager::new();
        manager.register_template(PostByIdTemplate);
        manager.register_template(PostTabTemplate);
        let session = Session::new();

        let content = manager
            .read(
                "agora:///posts/xjje9tppqbdn9jodc84z6zdw9c/thread",
                HashMap::new(),
                &session,
                &logger,
            )
            .await
            .expect("deeper parameterized URI should resolve");
        let text = content[0].text.as_ref().unwrap();
        assert_eq!(text, "tab=xjje9tppqbdn9jodc84z6zdw9c:thread");
    }

    #[tokio::test]
    async fn test_read_for_session_skips_hidden_template_and_uses_visible_fallback() {
        let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel();
        let logger = crate::logging::McpLogger::new(_tx, "test");
        let manager = ResourceManager::new();
        manager.register_template(FileTemplate);
        manager.register_template(AdminSecretFileTemplate);

        let session = Session::new();
        let visibility_ctx = VisibilityContext::new(&session);
        let content = manager
            .read_for_session(
                "file:///docs/secret",
                HashMap::new(),
                &session,
                &logger,
                &visibility_ctx,
            )
            .await
            .expect("visible fallback template should resolve");
        let text = content[0].text.as_ref().unwrap();
        assert_eq!(text, "Mock content for file: docs/secret");

        let admin_session = Session::new();
        admin_session.set_state("roles", serde_json::json!(["admin"]));
        let admin_visibility_ctx = VisibilityContext::new(&admin_session);
        let content = manager
            .read_for_session(
                "file:///docs/secret",
                HashMap::new(),
                &admin_session,
                &logger,
                &admin_visibility_ctx,
            )
            .await
            .expect("admin-visible template should resolve");
        let text = content[0].text.as_ref().unwrap();
        assert_eq!(text, "secret=docs");
    }
}
