//! Visibility context for session-aware tool/resource/prompt filtering
//!
//! Provides contextual information that tools, resources, and prompts can use
//! to determine whether they should be visible in a given session.
//!
//! # Example
//!
//! ```rust,ignore
//! struct GitCommitTool;
//!
//! impl Tool for GitCommitTool {
//!     fn is_visible(&self, ctx: &VisibilityContext) -> bool {
//!         ctx.environment
//!             .map(|e| e.has_git_repo() && !e.git_is_clean())
//!             .unwrap_or(false)
//!     }
//!     // ...
//! }
//! ```

use std::path::Path;

use serde_json::Value;

use crate::server::multiplexer::ClientRequester;
use crate::server::session::Session;

/// Context provided to tools/resources/prompts for visibility decisions
///
/// Contains session information and optional environment state that can be
/// used to determine whether a component should be visible.
pub struct VisibilityContext<'a> {
    /// Reference to the current session
    pub session: &'a Session,

    /// Optional environment state for contextual checks
    pub environment: Option<&'a dyn Environment>,
}

impl<'a> VisibilityContext<'a> {
    /// Create a new visibility context
    pub fn new(session: &'a Session) -> Self {
        Self {
            session,
            environment: None,
        }
    }

    /// Create a visibility context with environment
    pub fn with_environment(session: &'a Session, environment: &'a dyn Environment) -> Self {
        Self {
            session,
            environment: Some(environment),
        }
    }

    /// Get a session state value
    pub fn get_state<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
        self.session
            .get_state(key)
            .and_then(|v| serde_json::from_value(v).ok())
    }

    /// Check if session has a specific role
    pub fn has_role(&self, role: &str) -> bool {
        self.session
            .get_state("roles")
            .and_then(|v| v.as_array().cloned())
            .map(|roles| roles.iter().any(|r| r.as_str() == Some(role)))
            .unwrap_or(false)
    }

    /// Check if session is admin
    pub fn is_admin(&self) -> bool {
        self.has_role("admin")
    }

    /// Check if session is VIP
    pub fn is_vip(&self) -> bool {
        self.has_role("vip")
    }
}

/// Execution context passed to tools, resources, and prompts during execution
///
/// Provides access to:
/// - Input parameters (for tools) or arguments (for prompts)
/// - Session information (roles, state, client info)
/// - Environment state (git status, filesystem)
///
/// # Example
///
/// ```rust,ignore
/// impl Tool for MyTool {
///     async fn execute(&self, ctx: ExecutionContext<'_>) -> Result<Vec<Box<dyn Content>>, ToolError> {
///         // Access session info
///         if ctx.is_admin() {
///             // Admin-only behavior
///         }
///
///         // Access parameters
///         let name = ctx.params.get("name").and_then(|v| v.as_str());
///
///         // Access environment
///         if let Some(env) = ctx.environment {
///             if env.has_git_repo() {
///                 // Git-specific behavior
///             }
///         }
///
///         Ok(vec![])
///     }
/// }
/// ```
pub struct ExecutionContext<'a> {
    /// Input parameters (from tool call or prompt arguments)
    pub params: Value,

    /// URI parameters (extracted from resource URI template)
    pub uri_params: std::collections::HashMap<String, String>,

    /// Reference to the current session
    pub session: &'a Session,

    /// Logger for sending log notifications to the client
    pub logger: &'a crate::logging::McpLogger,

    /// Optional environment state for contextual checks
    pub environment: Option<&'a dyn Environment>,

    /// Optional client requester for server→client requests
    client_requester: Option<ClientRequester>,
}

impl<'a> ExecutionContext<'a> {
    /// Create a new execution context
    pub fn new(params: Value, session: &'a Session, logger: &'a crate::logging::McpLogger) -> Self {
        Self {
            params,
            uri_params: std::collections::HashMap::new(),
            session,
            logger,
            environment: None,
            client_requester: None,
        }
    }

    /// Create an execution context with environment
    pub fn with_environment(
        params: Value,
        session: &'a Session,
        logger: &'a crate::logging::McpLogger,
        environment: &'a dyn Environment,
    ) -> Self {
        Self {
            params,
            uri_params: std::collections::HashMap::new(),
            session,
            logger,
            environment: Some(environment),
            client_requester: None,
        }
    }

    /// Create an execution context for resources (with URI params)
    pub fn for_resource(
        uri_params: std::collections::HashMap<String, String>,
        session: &'a Session,
        logger: &'a crate::logging::McpLogger,
    ) -> Self {
        Self {
            params: Value::Null,
            uri_params,
            session,
            logger,
            environment: None,
            client_requester: None,
        }
    }

    /// Create a resource execution context with environment
    pub fn for_resource_with_environment(
        uri_params: std::collections::HashMap<String, String>,
        session: &'a Session,
        logger: &'a crate::logging::McpLogger,
        environment: &'a dyn Environment,
    ) -> Self {
        Self {
            params: Value::Null,
            uri_params,
            session,
            logger,
            environment: Some(environment),
            client_requester: None,
        }
    }

    /// Set the client requester for server→client requests
    pub fn with_client_requester(mut self, requester: ClientRequester) -> Self {
        self.client_requester = Some(requester);
        self
    }

    /// Get the client requester for making server→client requests
    ///
    /// Returns None if the server hasn't set up bidirectional communication.
    pub fn client_requester(&self) -> Option<&ClientRequester> {
        self.client_requester.as_ref()
    }

    /// Get a URI parameter by name
    pub fn get_uri_param(&self, name: &str) -> Option<&str> {
        self.uri_params.get(name).map(|s| s.as_str())
    }

    /// Get a session state value
    pub fn get_state<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
        self.session
            .get_state(key)
            .and_then(|v| serde_json::from_value(v).ok())
    }

    /// Check if session has a specific role
    pub fn has_role(&self, role: &str) -> bool {
        self.session
            .get_state("roles")
            .and_then(|v| v.as_array().cloned())
            .map(|roles| roles.iter().any(|r| r.as_str() == Some(role)))
            .unwrap_or(false)
    }

    /// Check if session is admin
    pub fn is_admin(&self) -> bool {
        self.has_role("admin")
    }

    /// Check if session is VIP
    pub fn is_vip(&self) -> bool {
        self.has_role("vip")
    }

    /// Get session ID
    pub fn session_id(&self) -> &str {
        &self.session.id
    }

    /// Get client name if available
    pub fn client_name(&self) -> Option<&str> {
        self.session.client_info.as_ref().map(|c| c.name.as_str())
    }

    /// Get client version if available
    pub fn client_version(&self) -> Option<&str> {
        self.session
            .client_info
            .as_ref()
            .map(|c| c.version.as_str())
    }

    /// Get negotiated protocol version if available
    pub fn protocol_version(&self) -> Option<&str> {
        self.session.protocol_version()
    }

    /// Get client capabilities if available
    pub fn client_capabilities(
        &self,
    ) -> Option<&crate::protocol::capabilities::ClientCapabilities> {
        self.session.capabilities.as_ref()
    }

    /// Convert to VisibilityContext (for visibility checks)
    pub fn as_visibility_context(&self) -> VisibilityContext<'a> {
        match self.environment {
            Some(env) => VisibilityContext::with_environment(self.session, env),
            None => VisibilityContext::new(self.session),
        }
    }
}

/// Environment trait for external state checks
///
/// Implement this trait to provide contextual information about the
/// execution environment (git status, filesystem state, etc.) that
/// tools can use for visibility decisions.
///
/// # Example Implementation
///
/// ```rust,ignore
/// struct GitEnvironment {
///     repo_path: PathBuf,
/// }
///
/// impl Environment for GitEnvironment {
///     fn has_git_repo(&self) -> bool {
///         self.repo_path.join(".git").exists()
///     }
///
///     fn git_is_clean(&self) -> bool {
///         // Check via git2 or command
///         Command::new("git")
///             .args(["status", "--porcelain"])
///             .output()
///             .map(|o| o.stdout.is_empty())
///             .unwrap_or(true)
///     }
///
///     fn git_commits_ahead(&self) -> usize {
///         // Parse git rev-list --count origin/main..HEAD
///         0
///     }
///
///     fn cwd(&self) -> &Path {
///         &self.repo_path
///     }
/// }
/// ```
pub trait Environment: Send + Sync {
    /// Check if current directory is a git repository
    fn has_git_repo(&self) -> bool {
        false
    }

    /// Check if git working tree is clean (no uncommitted changes)
    fn git_is_clean(&self) -> bool {
        true
    }

    /// Get number of commits ahead of remote tracking branch
    fn git_commits_ahead(&self) -> usize {
        0
    }

    /// Check if there are staged changes ready to commit
    fn git_has_staged(&self) -> bool {
        false
    }

    /// Check if there are stashed changes
    fn git_has_stash(&self) -> bool {
        false
    }

    /// Get current working directory
    fn cwd(&self) -> &Path;

    /// Check if a file exists relative to cwd
    fn file_exists(&self, path: &str) -> bool {
        self.cwd().join(path).exists()
    }

    /// Check if a directory exists relative to cwd
    fn dir_exists(&self, path: &str) -> bool {
        let full_path = self.cwd().join(path);
        full_path.exists() && full_path.is_dir()
    }

    /// Get a custom environment value
    fn get_custom(&self, _key: &str) -> Option<String> {
        None
    }
}

/// Simple environment implementation backed by filesystem checks
pub struct SimpleEnvironment {
    cwd: std::path::PathBuf,
}

impl SimpleEnvironment {
    /// Create a new simple environment
    pub fn new(cwd: impl Into<std::path::PathBuf>) -> Self {
        Self { cwd: cwd.into() }
    }

    /// Create from current directory
    pub fn current_dir() -> std::io::Result<Self> {
        Ok(Self {
            cwd: std::env::current_dir()?,
        })
    }
}

impl Environment for SimpleEnvironment {
    fn has_git_repo(&self) -> bool {
        self.cwd.join(".git").exists()
    }

    fn cwd(&self) -> &Path {
        &self.cwd
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::server::session::Session;

    #[test]
    fn test_visibility_context_creation() {
        let session = Session::new();
        let ctx = VisibilityContext::new(&session);

        assert!(ctx.environment.is_none());
    }

    #[test]
    fn test_visibility_context_with_environment() {
        let session = Session::new();
        let env = SimpleEnvironment::new("/tmp");
        let ctx = VisibilityContext::with_environment(&session, &env);

        assert!(ctx.environment.is_some());
        assert_eq!(ctx.environment.unwrap().cwd(), Path::new("/tmp"));
    }

    #[test]
    fn test_role_checks() {
        let session = Session::new();
        session.set_state("roles", serde_json::json!(["admin", "vip"]));

        let ctx = VisibilityContext::new(&session);

        assert!(ctx.has_role("admin"));
        assert!(ctx.has_role("vip"));
        assert!(!ctx.has_role("guest"));
        assert!(ctx.is_admin());
        assert!(ctx.is_vip());
    }

    #[test]
    fn test_role_checks_empty() {
        let session = Session::new();
        let ctx = VisibilityContext::new(&session);

        assert!(!ctx.has_role("admin"));
        assert!(!ctx.is_admin());
        assert!(!ctx.is_vip());
    }

    #[test]
    fn test_simple_environment() {
        let env = SimpleEnvironment::new("/tmp");

        assert_eq!(env.cwd(), Path::new("/tmp"));
        assert!(env.git_is_clean()); // Uses trait default: true
        assert!(!env.has_git_repo()); // /tmp likely doesn't have .git
    }

    #[test]
    fn test_environment_defaults() {
        struct MinimalEnv;
        impl Environment for MinimalEnv {
            fn cwd(&self) -> &Path {
                Path::new("/")
            }
        }

        let env = MinimalEnv;
        assert!(!env.has_git_repo());
        assert!(env.git_is_clean());
        assert_eq!(env.git_commits_ahead(), 0);
        assert!(!env.git_has_staged());
        assert!(!env.git_has_stash());
        assert!(env.get_custom("anything").is_none());
    }
}
