//! Local VIP ownership management for FiberLB.
//!
//! This module claims and releases VIP `/32` addresses on a local interface so
//! the kernel actually treats the advertised VIP as a local destination.

use std::net::{IpAddr, Ipv4Addr};
use std::path::Path;

use tokio::process::Command;

/// Result type for VIP ownership operations.
pub type Result<T> = std::result::Result<T, VipOwnershipError>;

/// VIP ownership errors.
#[derive(Debug, thiserror::Error)]
pub enum VipOwnershipError {
    #[error("I/O error: {0}")]
    Io(#[from] std::io::Error),
    #[error("kernel VIP ownership command failed: {0}")]
    CommandFailed(String),
    #[error("unsupported VIP ownership operation: {0}")]
    Unsupported(String),
}

/// Trait for local VIP ownership.
#[tonic::async_trait]
pub trait VipAddressOwner: Send + Sync {
    /// Ensure a VIP is locally owned on this node.
    async fn ensure_present(&self, vip: IpAddr) -> Result<()>;

    /// Ensure a VIP is no longer locally owned on this node.
    async fn ensure_absent(&self, vip: IpAddr) -> Result<()>;
}

/// Linux kernel-backed VIP owner using `ip addr add/del`.
pub struct KernelVipAddressOwner {
    interface: String,
    ip_command: String,
}

impl KernelVipAddressOwner {
    /// Create a kernel-backed VIP owner for the given interface.
    pub fn new(interface: impl Into<String>) -> Self {
        Self::with_ip_command(interface, None::<String>)
    }

    /// Create a kernel-backed VIP owner with an optional explicit `ip` command path.
    pub fn with_ip_command(
        interface: impl Into<String>,
        ip_command: Option<impl Into<String>>,
    ) -> Self {
        Self {
            interface: interface.into(),
            ip_command: resolve_ip_command(ip_command.map(Into::into)),
        }
    }

    async fn run_ip(&self, args: &[String]) -> Result<()> {
        let output = Command::new(&self.ip_command).args(args).output().await?;
        if output.status.success() {
            return Ok(());
        }

        Err(VipOwnershipError::CommandFailed(format!(
            "{} {}: {}",
            self.ip_command,
            args.join(" "),
            render_command_output(&output)
        )))
    }

    async fn add_v4(&self, vip: Ipv4Addr) -> Result<()> {
        let args = vec![
            "-4".to_string(),
            "addr".to_string(),
            "add".to_string(),
            format!("{vip}/32"),
            "dev".to_string(),
            self.interface.clone(),
        ];

        match self.run_ip(&args).await {
            Ok(()) => Ok(()),
            Err(VipOwnershipError::CommandFailed(message)) if message.contains("File exists") => {
                Ok(())
            }
            Err(error) => Err(error),
        }
    }

    async fn del_v4(&self, vip: Ipv4Addr) -> Result<()> {
        let args = vec![
            "-4".to_string(),
            "addr".to_string(),
            "del".to_string(),
            format!("{vip}/32"),
            "dev".to_string(),
            self.interface.clone(),
        ];

        match self.run_ip(&args).await {
            Ok(()) => Ok(()),
            Err(VipOwnershipError::CommandFailed(message))
                if message.contains("Cannot assign requested address") =>
            {
                Ok(())
            }
            Err(error) => Err(error),
        }
    }
}

#[tonic::async_trait]
impl VipAddressOwner for KernelVipAddressOwner {
    async fn ensure_present(&self, vip: IpAddr) -> Result<()> {
        match vip {
            IpAddr::V4(vip) => self.add_v4(vip).await,
            IpAddr::V6(_) => Err(VipOwnershipError::Unsupported(format!(
                "VIP ownership currently supports IPv4 only (got {vip})"
            ))),
        }
    }

    async fn ensure_absent(&self, vip: IpAddr) -> Result<()> {
        match vip {
            IpAddr::V4(vip) => self.del_v4(vip).await,
            IpAddr::V6(_) => Err(VipOwnershipError::Unsupported(format!(
                "VIP ownership currently supports IPv4 only (got {vip})"
            ))),
        }
    }
}

fn resolve_ip_command(configured: Option<String>) -> String {
    if let Some(path) = configured {
        let trimmed = path.trim();
        if !trimmed.is_empty() {
            return trimmed.to_string();
        }
    }

    if let Ok(path) = std::env::var("FIBERLB_IP_COMMAND") {
        let trimmed = path.trim();
        if !trimmed.is_empty() {
            return trimmed.to_string();
        }
    }

    for candidate in [
        "/run/current-system/sw/bin/ip",
        "/usr/sbin/ip",
        "/sbin/ip",
        "/usr/bin/ip",
        "/bin/ip",
    ] {
        if Path::new(candidate).exists() {
            return candidate.to_string();
        }
    }

    "ip".to_string()
}

fn render_command_output(output: &std::process::Output) -> String {
    let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
    if !stderr.is_empty() {
        return stderr;
    }

    let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
    if !stdout.is_empty() {
        return stdout;
    }

    format!("exit status {}", output.status)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::{Mutex, OnceLock};

    fn env_lock() -> &'static Mutex<()> {
        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
        LOCK.get_or_init(|| Mutex::new(()))
    }

    #[test]
    fn configured_ip_command_wins_over_env() {
        let _guard = env_lock().lock().unwrap();
        std::env::set_var("FIBERLB_IP_COMMAND", "/env/ip");

        assert_eq!(
            resolve_ip_command(Some("  /configured/ip  ".to_string())),
            "/configured/ip"
        );

        std::env::remove_var("FIBERLB_IP_COMMAND");
    }

    #[test]
    fn env_ip_command_is_used_when_config_is_absent() {
        let _guard = env_lock().lock().unwrap();
        std::env::set_var("FIBERLB_IP_COMMAND", " /env/ip ");

        assert_eq!(resolve_ip_command(None), "/env/ip");

        std::env::remove_var("FIBERLB_IP_COMMAND");
    }
}
