//! System DNS configuration management for Windows/Linux

use std::process::Command;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum SystemDnsError {
    #[error("io error: {0}")]
    Io(#[from] std::io::Error),
    #[error("command failed: {0}")]
    CommandFailed(String),
    #[error("unsupported platform")]
    UnsupportedPlatform,
    #[error("permission denied")]
    PermissionDenied,
}

/// Stores original DNS settings for restoration
#[derive(Debug, Clone)]
pub struct DnsBackup {
    #[cfg(target_os = "windows")]
    pub interface: String,
    #[cfg(target_os = "windows")]
    pub original_dns: Vec<String>,
    #[cfg(target_os = "linux")]
    pub resolv_conf_backup: String,
}

/// System DNS manager - sets DNS to 127.0.0.1 and restores on drop
pub struct SystemDnsManager {
    backup: Option<DnsBackup>,
}

impl SystemDnsManager {
    pub fn new() -> Self {
        Self { backup: None }
    }

    /// Set system DNS to 127.0.0.1 (requires admin/root)
    pub async fn set_local_dns(&mut self) -> Result<(), SystemDnsError> {
        #[cfg(target_os = "windows")]
        {
            self.set_windows_dns().await
        }

        #[cfg(target_os = "linux")]
        {
            self.set_linux_dns().await
        }

        #[cfg(not(any(target_os = "windows", target_os = "linux")))]
        {
            Err(SystemDnsError::UnsupportedPlatform)
        }
    }

    /// Restore original DNS settings
    pub async fn restore(&self) -> Result<(), SystemDnsError> {
        if self.backup.is_none() {
            return Ok(());
        }

        #[cfg(target_os = "windows")]
        {
            self.restore_windows_dns().await
        }

        #[cfg(target_os = "linux")]
        {
            self.restore_linux_dns().await
        }

        #[cfg(not(any(target_os = "windows", target_os = "linux")))]
        {
            Err(SystemDnsError::UnsupportedPlatform)
        }
    }

    #[cfg(target_os = "windows")]
    async fn set_windows_dns(&mut self) -> Result<(), SystemDnsError> {
        // Get active network interface
        let output = Command::new("netsh")
            .args(&["interface", "show", "interface"])
            .output()?;

        let interfaces = String::from_utf8_lossy(&output.stdout);
        let active_interface = interfaces
            .lines()
            .find(|line| line.contains("Connected") && !line.contains("Disconnected"))
            .and_then(|line| line.split_whitespace().last())
            .ok_or_else(|| SystemDnsError::CommandFailed("No active interface found".into()))?;

        // Backup current DNS settings
        let output = Command::new("netsh")
            .args(&[
                "interface",
                "ipv4",
                "show",
                "dnsservers",
                active_interface,
            ])
            .output()?;

        let dns_output = String::from_utf8_lossy(&output.stdout);
        let original_dns: Vec<String> = dns_output
            .lines()
            .filter_map(|line| {
                if line.trim().starts_with(|c: char| c.is_numeric()) {
                    Some(line.trim().to_string())
                } else {
                    None
                }
            })
            .collect();

        self.backup = Some(DnsBackup {
            interface: active_interface.to_string(),
            original_dns,
        });

        // Set DNS to 127.0.0.1
        let status = Command::new("netsh")
            .args(&[
                "interface",
                "ipv4",
                "set",
                "dnsservers",
                active_interface,
                "static",
                "127.0.0.1",
                "primary",
            ])
            .status()?;

        if !status.success() {
            return Err(SystemDnsError::CommandFailed(
                "Failed to set DNS".into(),
            ));
        }

        tracing::info!("System DNS set to 127.0.0.1 on interface {}", active_interface);
        Ok(())
    }

    #[cfg(target_os = "windows")]
    async fn restore_windows_dns(&self) -> Result<(), SystemDnsError> {
        let backup = self.backup.as_ref().unwrap();

        if backup.original_dns.is_empty() {
            // Set to DHCP
            let status = Command::new("netsh")
                .args(&[
                    "interface",
                    "ipv4",
                    "set",
                    "dnsservers",
                    &backup.interface,
                    "dhcp",
                ])
                .status()?;

            if !status.success() {
                return Err(SystemDnsError::CommandFailed(
                    "Failed to restore DNS to DHCP".into(),
                ));
            }
        } else {
            // Restore first DNS as primary
            let status = Command::new("netsh")
                .args(&[
                    "interface",
                    "ipv4",
                    "set",
                    "dnsservers",
                    &backup.interface,
                    "static",
                    &backup.original_dns[0],
                    "primary",
                ])
                .status()?;

            if !status.success() {
                return Err(SystemDnsError::CommandFailed(
                    "Failed to restore primary DNS".into(),
                ));
            }

            // Add additional DNS servers
            for dns in backup.original_dns.iter().skip(1) {
                let _ = Command::new("netsh")
                    .args(&[
                        "interface",
                        "ipv4",
                        "add",
                        "dnsservers",
                        &backup.interface,
                        dns,
                    ])
                    .status();
            }
        }

        tracing::info!("System DNS restored on interface {}", backup.interface);
        Ok(())
    }

    #[cfg(target_os = "linux")]
    async fn set_linux_dns(&mut self) -> Result<(), SystemDnsError> {
        use tokio::fs;

        // Backup /etc/resolv.conf
        let resolv_conf = fs::read_to_string("/etc/resolv.conf").await?;
        self.backup = Some(DnsBackup {
            resolv_conf_backup: resolv_conf,
        });

        // Write new resolv.conf
        let new_conf = "# Managed by OSTP\nnameserver 127.0.0.1\n";
        fs::write("/etc/resolv.conf", new_conf).await?;

        tracing::info!("System DNS set to 127.0.0.1 in /etc/resolv.conf");
        Ok(())
    }

    #[cfg(target_os = "linux")]
    async fn restore_linux_dns(&self) -> Result<(), SystemDnsError> {
        use tokio::fs;

        let backup = self.backup.as_ref().unwrap();
        fs::write("/etc/resolv.conf", &backup.resolv_conf_backup).await?;

        tracing::info!("System DNS restored from backup");
        Ok(())
    }
}

impl Drop for SystemDnsManager {
    fn drop(&mut self) {
        if self.backup.is_some() {
            // Best effort restore - use blocking runtime
            let rt = tokio::runtime::Handle::try_current();
            if let Ok(handle) = rt {
                let _ = handle.block_on(self.restore());
            }
        }
    }
}

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

    #[tokio::test]
    #[ignore] // Requires admin/root
    async fn test_set_and_restore_dns() {
        let mut manager = SystemDnsManager::new();
        manager.set_local_dns().await.unwrap();
        manager.restore().await.unwrap();
    }
}
