use std::path::Path;
use std::sync::Arc;
use std::time::Duration;

use anyhow::{anyhow, Context, Result};
use russh::client;
use russh::keys::{load_secret_key, PrivateKeyWithHashAlg};
use ssh_key::{KnownHosts, PublicKey};
use tokio::time::timeout;
use tracing;

use super::super::super::models::config::{AuthMethod, HostKeyPolicy, SshConfig};

/// Parsed known_hosts entries.
struct KnownHostsStore {
    entries: Vec<ssh_key::known_hosts::Entry>,
}

impl KnownHostsStore {
    /// Load known_hosts from a file.
    fn load_from_file(path: &Path) -> Result<Self> {
        let entries = KnownHosts::read_file(path)
            .map_err(|e| anyhow!("failed to read known_hosts {:?}: {}", path, e))?;
        Ok(Self { entries })
    }

    /// Check if the given host key matches in known_hosts.
    fn is_known(&self, host: &str, port: u16, server_key: &PublicKey) -> bool {
        let host_with_port = format!("[{}]:{}", host, port);
        let host_without_port = host.to_string();

        self.entries.iter().any(|entry| {
            // Skip revoked entries
            if entry.marker() == Some(&ssh_key::known_hosts::Marker::Revoked) {
                return false;
            }

            // Check if host pattern matches
            let host_matches = match entry.host_patterns() {
                ssh_key::known_hosts::HostPatterns::Patterns(patterns) => {
                    patterns.iter().any(|p| {
                        // Simple match: exact host or host:port
                        p == &host_without_port
                            || p == &host_with_port
                            || p == &format!("{}:22", host_without_port) && port == 22
                    })
                }
                ssh_key::known_hosts::HostPatterns::HashedName { salt, hash } => {
                    // OpenSSH hashes [host]:port for non-standard ports
                    let host_to_hash = if port == 22 {
                        host_without_port.clone()
                    } else {
                        format!("[{}]:{}", host, port)
                    };
                    verify_hashed_host(&host_to_hash, salt, hash)
                }
            };

            host_matches && entry.public_key() == server_key
        })
    }
}

/// Verify a hashed hostname (HMAC-SHA1).
///
/// The known_hosts hashed hostname format is `|1|<base64-salt>|<base64-hash>`,
/// where hash = HMAC-SHA1(salt, hostname).
fn verify_hashed_host(hostname: &str, salt: &[u8], expected_hash: &[u8; 20]) -> bool {
    use hmac::{Hmac, Mac};
    use sha1::Sha1;
    type HmacSha1 = Hmac<Sha1>;

    let mut mac = match HmacSha1::new_from_slice(salt) {
        Ok(mac) => mac,
        Err(_) => return false,
    };
    mac.update(hostname.as_bytes());
    let result = mac.finalize().into_bytes();
    result.as_slice() == expected_hash.as_slice()
}

/// Determine the default known_hosts path based on SshConfig's host_key_policy.
fn default_known_hosts_path() -> Option<std::path::PathBuf> {
    dirs_home().map(|home| home.join(".ssh").join("known_hosts"))
}

fn dirs_home() -> Option<std::path::PathBuf> {
    // Prefer HOME env var, compatible with most Unix systems
    std::env::var("HOME")
        .ok()
        .map(std::path::PathBuf::from)
        .filter(|p| p.is_dir())
}

pub(crate) struct ClientHandler {
    host: String,
    port: u16,
    policy: HostKeyPolicy,
    known_hosts: Option<Arc<KnownHostsStore>>,
    known_hosts_path: Option<std::path::PathBuf>,
}

impl ClientHandler {
    fn new(cfg: &SshConfig) -> Self {
        let known_hosts_path = cfg
            .known_hosts_path
            .as_ref()
            .map(std::path::PathBuf::from)
            .or_else(default_known_hosts_path);

        let known_hosts = known_hosts_path
            .as_ref()
            .and_then(|p| KnownHostsStore::load_from_file(p).ok())
            .map(Arc::new);

        Self {
            host: cfg.host.clone(),
            port: cfg.port,
            policy: cfg.host_key_policy.clone(),
            known_hosts,
            known_hosts_path,
        }
    }
}

impl client::Handler for ClientHandler {
    type Error = russh::Error;

    fn check_server_key(
        &mut self,
        server_public_key: &PublicKey,
    ) -> impl std::future::Future<Output = Result<bool, Self::Error>> + Send {
        let host = self.host.clone();
        let port = self.port;
        let policy = self.policy.clone();
        let known_hosts = self.known_hosts.clone();
        let known_hosts_path = self.known_hosts_path.clone();

        async move {
            match policy {
                HostKeyPolicy::Disable => {
                    tracing::warn!(host = %host, port = port, "host key verification disabled");
                    Ok(true)
                }
                HostKeyPolicy::Strict => match &known_hosts {
                    Some(store) if store.is_known(&host, port, server_public_key) => {
                        tracing::debug!(host = %host, "host key verified");
                        Ok(true)
                    }
                    _ => {
                        tracing::error!(
                            host = %host, port = port,
                            "host key not found in known_hosts (strict mode)"
                        );
                        Err(russh::Error::UnknownKey)
                    }
                },
                HostKeyPolicy::AcceptNew => {
                    match &known_hosts {
                        Some(store) if store.is_known(&host, port, server_public_key) => {
                            tracing::debug!(host = %host, "host key verified");
                            Ok(true)
                        }
                        _ => {
                            tracing::warn!(
                                host = %host, port = port,
                                "host key not in known_hosts, accepting and persisting new key"
                            );
                            // Persist new key to known_hosts file
                            if let Some(path) = known_hosts_path {
                                if let Err(e) =
                                    persist_host_key(&path, &host, server_public_key).await
                                {
                                    tracing::warn!(
                                        host = %host, error = %e,
                                        "failed to persist host key to known_hosts"
                                    );
                                }
                            }
                            Ok(true)
                        }
                    }
                }
            }
        }
    }
}

/// Append a new host key to the known_hosts file.
async fn persist_host_key(
    path: &std::path::Path,
    host: &str,
    server_key: &PublicKey,
) -> Result<()> {
    // Format: hostname key-type base64-key-data
    let key_str = server_key.to_string();
    let line = format!("{} {}\n", host, key_str);

    // Ensure .ssh directory exists
    if let Some(parent) = path.parent() {
        tokio::fs::create_dir_all(parent)
            .await
            .with_context(|| format!("failed to create directory: {:?}", parent))?;
    }

    // Append to file
    let mut file = tokio::fs::OpenOptions::new()
        .create(true)
        .append(true)
        .open(path)
        .await
        .with_context(|| format!("failed to open known_hosts for writing: {:?}", path))?;

    tokio::io::AsyncWriteExt::write_all(&mut file, line.as_bytes())
        .await
        .with_context(|| format!("failed to write to known_hosts: {:?}", path))?;

    tracing::info!(host = %host, path = ?path, "persisted new host key to known_hosts");
    Ok(())
}

pub(crate) async fn connect_and_auth(cfg: SshConfig) -> Result<client::Handle<ClientHandler>> {
    let addr = tokio::net::lookup_host(format!("{}:{}", cfg.host, cfg.port))
        .await
        .with_context(|| format!("failed to resolve host: {}:{}", cfg.host, cfg.port))?
        .next()
        .ok_or_else(|| anyhow!("no addresses resolved for {}:{}", cfg.host, cfg.port))?;

    let config = Arc::new(client::Config::default());
    let handler = ClientHandler::new(&cfg);

    let start = std::time::Instant::now();
    tracing::debug!(host = %cfg.host, port = cfg.port, "ssh connect start");
    let mut session = timeout(
        Duration::from_millis(cfg.timeout_ms),
        client::connect(config, addr, handler),
    )
    .await
    .map_err(|_| {
        tracing::error!(host = %cfg.host, port = cfg.port, "ssh connect failed: timeout");
        anyhow!("timeout connecting to {}:{}", cfg.host, cfg.port)
    })?
    .with_context(|| "ssh connect failed")?;

    match cfg.auth {
        AuthMethod::Password { password } => {
            let auth_result = timeout(
                Duration::from_millis(cfg.timeout_ms),
                session.authenticate_password(cfg.user, password),
            )
            .await
            .map_err(|_| anyhow!("timeout during password auth"))?
            .with_context(|| "password auth failed")?;

            if !auth_result.success() {
                return Err(anyhow!("password authentication rejected"));
            }
        }
        AuthMethod::PrivateKey {
            private_key_path,
            passphrase,
        } => {
            let path = Path::new(&private_key_path);
            let key_pair = load_secret_key(path, passphrase.as_deref())
                .with_context(|| format!("failed to load private key: {}", private_key_path))?;

            let hash_alg = session.best_supported_rsa_hash().await?.flatten();

            let auth_result = timeout(
                Duration::from_millis(cfg.timeout_ms),
                session.authenticate_publickey(
                    cfg.user,
                    PrivateKeyWithHashAlg::new(Arc::new(key_pair), hash_alg),
                ),
            )
            .await
            .map_err(|_| anyhow!("timeout during publickey auth"))?
            .with_context(|| "publickey auth failed")?;

            if !auth_result.success() {
                return Err(anyhow!("publickey authentication rejected"));
            }
        }
    }

    tracing::info!(host = %cfg.host, port = cfg.port, elapsed = ?start.elapsed(), "ssh connect success");
    Ok(session)
}
