use redis::Cmd;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::error::Error;
use std::fmt;
use std::process::{Command, Output};

/// Default Redis URL for standard Redis connection on localhost
const DEFAULT_REDIS_URL: &str = "redis://127.0.0.1/";

/// Redis key where git configuration is stored
const REDIS_GIT_CONFIG_KEY: &str = "herocontext:git";

// Simple redis client functionality with configurable connection
fn execute_redis_command(cmd: &mut redis::Cmd) -> redis::RedisResult<String> {
    // Get Redis URL from environment variables with fallback to standard localhost
    let redis_url = get_redis_url();
    log::debug!("Connecting to Redis at: {}", mask_redis_url(&redis_url));

    let client = redis::Client::open(redis_url)?;
    let mut con = client.get_connection()?;
    cmd.query(&mut con)
}

/// Get Redis URL from environment variables.
///
/// Checks the following in order:
/// 1. `REDIS_URL` environment variable
/// 2. `SAL_REDIS_URL` environment variable
/// 3. Falls back to standard Redis connection: `redis://127.0.0.1/`
///
/// All environment variables are optional. If none are set, uses standard localhost Redis.
pub fn get_redis_url() -> String {
    std::env::var("REDIS_URL")
        .or_else(|_| std::env::var("SAL_REDIS_URL"))
        .unwrap_or_else(|_| DEFAULT_REDIS_URL.to_string())
}

/// Check if Redis is explicitly configured via environment variables.
/// Returns true if REDIS_URL or SAL_REDIS_URL is set.
pub fn is_redis_configured() -> bool {
    std::env::var("REDIS_URL").is_ok() || std::env::var("SAL_REDIS_URL").is_ok()
}

/// Mask sensitive information in Redis URL for logging
fn mask_redis_url(url: &str) -> String {
    if let Ok(parsed) = url::Url::parse(url) {
        if parsed.password().is_some() {
            format!(
                "{}://{}:***@{}:{}/{}",
                parsed.scheme(),
                parsed.username(),
                parsed.host_str().unwrap_or("unknown"),
                parsed.port().unwrap_or(6379),
                parsed.path().trim_start_matches('/')
            )
        } else {
            url.to_string()
        }
    } else {
        "redis://***masked***".to_string()
    }
}

// Define a custom error type for GitExecutor operations
#[derive(Debug)]
pub enum GitExecutorError {
    GitCommandFailed(String),
    CommandExecutionError(std::io::Error),
    RedisError(redis::RedisError),
    JsonError(serde_json::Error),
    AuthenticationError(String),
    SshAgentNotLoaded,
    InvalidAuthConfig(String),
}

// Implement Display for GitExecutorError
impl fmt::Display for GitExecutorError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            GitExecutorError::GitCommandFailed(e) => write!(f, "Git command failed: {}", e),
            GitExecutorError::CommandExecutionError(e) => {
                write!(f, "Command execution error: {}", e)
            }
            GitExecutorError::RedisError(e) => write!(f, "Redis error: {}", e),
            GitExecutorError::JsonError(e) => write!(f, "JSON error: {}", e),
            GitExecutorError::AuthenticationError(e) => write!(f, "Authentication error: {}", e),
            GitExecutorError::SshAgentNotLoaded => write!(f, "SSH agent is not loaded"),
            GitExecutorError::InvalidAuthConfig(e) => {
                write!(f, "Invalid authentication configuration: {}", e)
            }
        }
    }
}

// Implement Error trait for GitExecutorError
impl Error for GitExecutorError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            GitExecutorError::CommandExecutionError(e) => Some(e),
            GitExecutorError::RedisError(e) => Some(e),
            GitExecutorError::JsonError(e) => Some(e),
            _ => None,
        }
    }
}

// From implementations for error conversion
impl From<redis::RedisError> for GitExecutorError {
    fn from(err: redis::RedisError) -> Self {
        GitExecutorError::RedisError(err)
    }
}

impl From<serde_json::Error> for GitExecutorError {
    fn from(err: serde_json::Error) -> Self {
        GitExecutorError::JsonError(err)
    }
}

impl From<std::io::Error> for GitExecutorError {
    fn from(err: std::io::Error) -> Self {
        GitExecutorError::CommandExecutionError(err)
    }
}

// Status enum for GitConfig
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub enum GitConfigStatus {
    #[serde(rename = "error")]
    Error,
    #[serde(rename = "ok")]
    Ok,
}

// Auth configuration for a specific git server
#[derive(Debug, Serialize, Deserialize)]
pub struct GitServerAuth {
    pub sshagent: Option<bool>,
    pub key: Option<String>,
    pub username: Option<String>,
    pub password: Option<String>,
}

// Main configuration structure from Redis
#[derive(Debug, Serialize, Deserialize)]
pub struct GitConfig {
    pub status: GitConfigStatus,
    pub auth: HashMap<String, GitServerAuth>,
}

/// GitExecutor struct for executing git commands with optional authentication.
///
/// The executor can load authentication configuration from Redis, but this is entirely optional.
/// If Redis is not available or not configured, git commands will execute with default behavior.
pub struct GitExecutor {
    config: Option<GitConfig>,
}

impl GitExecutor {
    /// Create a new GitExecutor without loading configuration.
    /// Use `init()` or `init_with_redis()` to load configuration from Redis.
    pub fn new() -> Self {
        GitExecutor { config: None }
    }

    /// Initialize by attempting to load configuration from Redis.
    ///
    /// This method is **optional** - the executor works without calling init().
    /// If Redis is not available, logs a warning and continues with default git behavior.
    ///
    /// # Redis Configuration
    /// - Redis URL is determined by environment variables (all optional):
    ///   1. `REDIS_URL` - Primary Redis URL
    ///   2. `SAL_REDIS_URL` - Alternative Redis URL
    ///   3. Falls back to standard `redis://127.0.0.1/` if none set
    /// - Configuration is stored at key: `herocontext:git`
    pub fn init(&mut self) -> Result<(), GitExecutorError> {
        self.init_with_redis(true)
    }

    /// Initialize with explicit control over Redis behavior.
    ///
    /// # Arguments
    /// * `try_redis` - If true, attempts to load config from Redis. If false, skips Redis entirely.
    ///
    /// When `try_redis` is true but Redis is unavailable, logs a warning and continues.
    /// When `try_redis` is false, the executor uses default git behavior without any Redis lookup.
    pub fn init_with_redis(&mut self, try_redis: bool) -> Result<(), GitExecutorError> {
        if !try_redis {
            log::debug!("Redis configuration loading skipped by request");
            self.config = None;
            return Ok(());
        }

        // Try to load config from Redis
        match self.load_config_from_redis() {
            Ok(config) => {
                self.config = Some(config);
                Ok(())
            }
            Err(e) => {
                // If Redis error, we'll proceed without config
                // This is not a fatal error as we might use default git behavior
                log::warn!(
                    "Failed to load git config from Redis: {} - using default git behavior",
                    e
                );
                self.config = None;
                Ok(())
            }
        }
    }

    /// Check if the executor has loaded configuration from Redis.
    pub fn has_config(&self) -> bool {
        self.config.is_some()
    }

    // Load configuration from Redis
    fn load_config_from_redis(&self) -> Result<GitConfig, GitExecutorError> {
        // Create Redis command to get the herocontext:git key
        let mut cmd = Cmd::new();
        cmd.arg("GET").arg(REDIS_GIT_CONFIG_KEY);

        // Execute the command
        let result: redis::RedisResult<String> = execute_redis_command(&mut cmd);

        match result {
            Ok(json_str) => {
                // Parse the JSON string into GitConfig
                let config: GitConfig = serde_json::from_str(&json_str)?;

                // Validate the config
                if config.status == GitConfigStatus::Error {
                    return Err(GitExecutorError::InvalidAuthConfig(
                        "Config status is error".to_string(),
                    ));
                }

                Ok(config)
            }
            Err(e) => Err(GitExecutorError::RedisError(e)),
        }
    }

    // Check if SSH agent is loaded
    fn is_ssh_agent_loaded(&self) -> bool {
        let output = Command::new("ssh-add").arg("-l").output();

        match output {
            Ok(output) => output.status.success() && !output.stdout.is_empty(),
            Err(_) => false,
        }
    }

    // Get authentication configuration for a git URL
    fn get_auth_for_url(&self, url: &str) -> Option<&GitServerAuth> {
        if let Some(config) = &self.config {
            let (server, _, _) = super::parse_git_url(url);
            if !server.is_empty() {
                return config.auth.get(&server);
            }
        }
        None
    }

    // Validate authentication configuration
    fn validate_auth_config(&self, auth: &GitServerAuth) -> Result<(), GitExecutorError> {
        // Rule: If sshagent is true, other fields should be empty
        if let Some(true) = auth.sshagent {
            if auth.key.is_some() || auth.username.is_some() || auth.password.is_some() {
                return Err(GitExecutorError::InvalidAuthConfig(
                    "When sshagent is true, key, username, and password must be empty".to_string(),
                ));
            }
            // Check if SSH agent is actually loaded
            if !self.is_ssh_agent_loaded() {
                return Err(GitExecutorError::SshAgentNotLoaded);
            }
        }

        // Rule: If key is set, other fields should be empty
        if auth.key.is_some()
            && (auth.sshagent.unwrap_or(false)
                || auth.username.is_some()
                || auth.password.is_some())
        {
            return Err(GitExecutorError::InvalidAuthConfig(
                "When key is set, sshagent, username, and password must be empty".to_string(),
            ));
        }

        // Rule: If username is set, password should be set and other fields empty
        if auth.username.is_some() {
            if auth.sshagent.unwrap_or(false) || auth.key.is_some() {
                return Err(GitExecutorError::InvalidAuthConfig(
                    "When username is set, sshagent and key must be empty".to_string(),
                ));
            }
            if auth.password.is_none() {
                return Err(GitExecutorError::InvalidAuthConfig(
                    "When username is set, password must also be set".to_string(),
                ));
            }
        }

        Ok(())
    }

    // Execute a git command with authentication
    pub fn execute(&self, args: &[&str]) -> Result<Output, GitExecutorError> {
        // Extract the git URL if this is a command that needs authentication
        let url_arg = self.extract_git_url_from_args(args);

        // If we have a URL and authentication config, use it
        if let Some(url) = url_arg {
            if let Some(auth) = self.get_auth_for_url(url) {
                // Validate the authentication configuration
                self.validate_auth_config(auth)?;

                // Execute with the appropriate authentication method
                return self.execute_with_auth(args, auth);
            }
        }

        // No special authentication needed, execute normally
        self.execute_git_command(args)
    }

    // Extract git URL from command arguments
    fn extract_git_url_from_args<'a>(&self, args: &[&'a str]) -> Option<&'a str> {
        // Commands that might contain a git URL
        if args.contains(&"clone")
            || args.contains(&"fetch")
            || args.contains(&"pull")
            || args.contains(&"push")
        {
            // The URL is typically the last argument for clone, or after remote for others
            for (i, &arg) in args.iter().enumerate() {
                if arg == "clone" && i + 1 < args.len() {
                    return Some(args[i + 1]);
                }
                if (arg == "fetch" || arg == "pull" || arg == "push") && i + 1 < args.len() {
                    // For these commands, the URL might be specified as a remote name
                    // We'd need more complex logic to resolve remote names to URLs
                    // For now, we'll just return None
                    return None;
                }
            }
        }
        None
    }

    // Execute git command with authentication
    fn execute_with_auth(
        &self,
        args: &[&str],
        auth: &GitServerAuth,
    ) -> Result<Output, GitExecutorError> {
        // Handle different authentication methods
        if let Some(true) = auth.sshagent {
            // Use SSH agent (already validated that it's loaded)
            self.execute_git_command(args)
        } else if let Some(key) = &auth.key {
            // Use SSH key
            self.execute_with_ssh_key(args, key)
        } else if let Some(username) = &auth.username {
            // Use username/password
            if let Some(password) = &auth.password {
                self.execute_with_credentials(args, username, password)
            } else {
                // This should never happen due to validation
                Err(GitExecutorError::AuthenticationError(
                    "Password is required when username is set".to_string(),
                ))
            }
        } else {
            // No authentication method specified, use default
            self.execute_git_command(args)
        }
    }

    // Execute git command with SSH key
    fn execute_with_ssh_key(&self, args: &[&str], key: &str) -> Result<Output, GitExecutorError> {
        // Create a command with GIT_SSH_COMMAND to specify the key
        let ssh_command = format!("ssh -i {} -o IdentitiesOnly=yes", key);

        let mut command = Command::new("git");
        command.env("GIT_SSH_COMMAND", ssh_command);
        command.args(args);

        let output = command.output()?;

        if output.status.success() {
            Ok(output)
        } else {
            let error = String::from_utf8_lossy(&output.stderr);
            Err(GitExecutorError::GitCommandFailed(error.to_string()))
        }
    }

    // Execute git command with username/password using secure credential helper
    fn execute_with_credentials(
        &self,
        args: &[&str],
        username: &str,
        password: &str,
    ) -> Result<Output, GitExecutorError> {
        // Use git credential helper approach for security
        // Create a temporary credential helper script
        let temp_dir = std::env::temp_dir();
        let helper_script = temp_dir.join(format!("git_helper_{}", std::process::id()));

        // Create credential helper script content
        let script_content = format!(
            "#!/bin/bash\necho username={}\necho password={}\n",
            username, password
        );

        // Write the helper script
        std::fs::write(&helper_script, script_content)
            .map_err(GitExecutorError::CommandExecutionError)?;

        // Make it executable
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mut perms = std::fs::metadata(&helper_script)
                .map_err(GitExecutorError::CommandExecutionError)?
                .permissions();
            perms.set_mode(0o755);
            std::fs::set_permissions(&helper_script, perms)
                .map_err(GitExecutorError::CommandExecutionError)?;
        }

        // Execute git command with credential helper
        let mut command = Command::new("git");
        command.args(args);
        command.env("GIT_ASKPASS", &helper_script);
        command.env("GIT_TERMINAL_PROMPT", "0"); // Disable terminal prompts

        log::debug!("Executing git command with credential helper");
        let output = command.output()?;

        // Clean up the temporary helper script
        let _ = std::fs::remove_file(&helper_script);

        if output.status.success() {
            Ok(output)
        } else {
            let error = String::from_utf8_lossy(&output.stderr);
            log::error!("Git command failed: {}", error);
            Err(GitExecutorError::GitCommandFailed(error.to_string()))
        }
    }

    // Basic git command execution
    fn execute_git_command(&self, args: &[&str]) -> Result<Output, GitExecutorError> {
        let mut command = Command::new("git");
        command.args(args);

        let output = command.output()?;

        if output.status.success() {
            Ok(output)
        } else {
            let error = String::from_utf8_lossy(&output.stderr);
            Err(GitExecutorError::GitCommandFailed(error.to_string()))
        }
    }
}

// Implement Default for GitExecutor
impl Default for GitExecutor {
    fn default() -> Self {
        Self::new()
    }
}
