//! `networksetup` 命令封装 + osascript 提权执行。
//!
//! 关键点：`setadditionalroutes` 是**全量覆盖**语义——它接收一个服务的所有路由。
//! 因此 [`set_routes`] 内部读取现有全部路由 → 修改 → 整体回写，对上层只暴露
//! "增/删一条" 的语义。
//!
//! 写操作需要 root，统一走 [`run_elevated`]（osascript 图形授权弹窗）。

use std::process::{Command, Stdio};

use crate::error::{AppError, AppResult};
// `cmd!` 是 #[macro_export] 全局宏，通过 $crate 路径调用，无需 import。
#[cfg(test)]
use crate::error::cmd_to_string;

use super::{route::Route, service::NetworkService};

// ───────────────────────── 读取 ─────────────────────────

/// 列出所有网络服务（含优先级、设备、启用状态）。
/// 对应 `networksetup -listnetworkserviceorder`。
pub fn list_services() -> AppResult<Vec<NetworkService>> {
    let out = run_simple(cmd!(networksetup -listnetworkserviceorder))?;
    super::service::parse_services(&out)
}

/// 读取某服务当前的附加路由。对应 `networksetup -getadditionalroutes <service>`。
///
/// 无路由时 networksetup 返回形如
/// `There are no additional IPv4 routes on Wi-Fi.`，此时返回空 Vec。
pub fn get_routes(service: &str) -> AppResult<Vec<Route>> {
    let out = run_simple(cmd!(networksetup -getadditionalroutes service))?;
    parse_routes(&out)
}

/// 读取某服务的 IPv4 配置（IP/掩码/网关）。对应 `networksetup -getinfo <service>`。
/// 用于推算直连网段与默认路由网关。
pub fn get_info(service: &str) -> AppResult<super::getinfo::ServiceIpv4> {
    let out = run_simple(cmd!(networksetup -getinfo service))?;
    super::getinfo::parse_getinfo(&out)
}

/// 执行 `netstat -rn -f inet`，返回分类后的路由（默认路由 + 直连网段）。
pub fn netstat_routes() -> AppResult<Vec<super::netstat::NetstatRoute>> {
    let out = run_simple(cmd!(netstat -rn -f "inet"))?;
    super::netstat::parse_netstat(&out)
}

/// 解析 `getadditionalroutes` 的输出。
/// 每行 `dest mask router`；router 字段对 on-link 是 `0.0.0.0`。
pub fn parse_routes(raw: &str) -> AppResult<Vec<Route>> {
    let trimmed = raw.trim();
    if trimmed.is_empty() || trimmed.contains("There are no additional IPv4 routes") {
        return Ok(Vec::new());
    }
    let mut out = Vec::new();
    for line in trimmed.lines() {
        let parts: Vec<&str> = line.split_whitespace().collect();
        if parts.len() < 2 {
            return Err(AppError::Parse(format!(
                "路由行字段不足 (期望至少 dest mask): {line:?}"
            )));
        }
        // parts[0]=dest parts[1]=mask parts[2]=router（可能缺失）
        let r = Route::from_dest_mask(parts[0], parts[1])?;
        out.push(r);
    }
    Ok(out)
}

// ───────────────────────── 写入（全量回写） ─────────────────────────

/// 把指定服务的附加路由**全量覆盖**为 `routes`。
///
/// 注意：调用方需自己先用 [`get_routes`] 读出现有路由，做增删后传进来。
/// 本函数内部不做合并。
///
/// 提权：内部通过 [`run_elevated`] 触发 macOS 图形授权对话框。
pub fn set_routes(service: &str, routes: &[Route]) -> AppResult<()> {
    let mut cmd_str = format!("networksetup -setadditionalroutes {service_quoted}", service_quoted = shell_quote(service));
    for r in routes {
        let (d, m, gw) = r.to_set_args();
        cmd_str.push(' ');
        cmd_str.push_str(&shell_quote(&d));
        cmd_str.push(' ');
        cmd_str.push_str(&shell_quote(&m));
        cmd_str.push(' ');
        cmd_str.push_str(&shell_quote(&gw));
    }
    run_elevated(&cmd_str)?;
    Ok(())
}

// ───────────────────────── 执行原语 ─────────────────────────

/// 执行不需要提权的命令，拿到 stdout 字符串。失败（非零退出/IO 错）转成 `AppError`。
fn run_simple(mut cmd: Command) -> AppResult<String> {
    cmd.stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());
    let out = cmd.output().map_err(|e| AppError::spawn(&cmd, e))?;
    if !out.status.success() {
        return Err(AppError::exit(
            &cmd,
            out.status.code(),
            String::from_utf8_lossy(&out.stderr),
        ));
    }
    Ok(String::from_utf8(out.stdout)?)
}

/// 通过 macOS 原生 osascript 提权执行一条 shell 命令。
///
/// 调用形式：
/// ```text
/// osascript -e 'do shell script "<cmd>" with administrator privileges'
/// ```
/// 会弹出系统图形授权对话框（用户名+密码），独立于终端，不干扰 TUI 屏幕。
///
/// 用户取消或密码错误时 osascript 非零退出，stderr 含
/// `User canceled` 或 `authentication failed`，统一转成 `AppError::Elevation`。
pub fn run_elevated(shell_cmd: &str) -> AppResult<()> {
    let applescript = format!(
        "do shell script \"{}\" with administrator privileges",
        applescript_escape(shell_cmd)
    );
    let mut c = cmd!(osascript -e applescript);
    c.stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());
    let out = c.output().map_err(|e| AppError::spawn(&c, e))?;
    if !out.status.success() {
        let stderr = String::from_utf8_lossy(&out.stderr);
        let stderr_stripped = stderr.trim();
        // 区分"用户取消"与"授权失败/命令失败"，让 UI 给出更准确的提示
        let msg = if stderr_stripped.contains("User canceled")
            || stderr_stripped.contains("-128")
            || stderr_stripped.contains("user canceled")
        {
            "用户取消了授权".to_string()
        } else if stderr_stripped.to_lowercase().contains("authentication") {
            format!("授权失败（密码错误？）: {stderr_stripped}")
        } else {
            format!("命令执行失败: {stderr_stripped}")
        };
        return Err(AppError::Elevation(format!(
            "{msg}\n原始命令: {shell_cmd}"
        )));
    }
    Ok(())
}

/// AppleScript 字符串字面量转义：仅 `"` 与 `\` 需要反斜杠化。
pub fn applescript_escape(s: &str) -> String {
    s.replace('\\', "\\\\").replace('"', "\\\"")
}

/// POSIX shell 单层转义：用单引号包裹，内部单引号走 `'\''` 序列。
/// 用于拼出可被 sh 解析的命令字符串。
pub fn shell_quote(s: &str) -> String {
    // 形如 en0 / 0.0.0.0 等不含特殊字符的，直接返回保持可读性。
    if !s.is_empty()
        && s.chars()
            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-' | '/' | ':' | '+'))
    {
        s.to_string()
    } else {
        format!("'{}'", s.replace('\'', "'\\''"))
    }
}

// ───────────────────────── 测试 ─────────────────────────

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

    #[test]
    fn parse_no_routes_line() {
        assert!(parse_routes("There are no additional IPv4 routes on Wi-Fi.\n").unwrap().is_empty());
        assert!(parse_routes("").unwrap().is_empty());
    }

    #[test]
    fn parse_multiple_routes() {
        let raw = "10.0.0.0 255.0.0.0 0.0.0.0\n172.16.0.0 255.240.0.0 0.0.0.0\n";
        let v = parse_routes(raw).unwrap();
        assert_eq!(v.len(), 2);
        assert_eq!(v[0], Route { dest: Ipv4Addr::new(10, 0, 0, 0), prefix_len: 8 });
        assert_eq!(v[1].prefix_len, 12);
    }

    #[test]
    fn applescript_escape_quotes_and_backslashes() {
        assert_eq!(applescript_escape(r#"a"b\c"#), r#"a\"b\\c"#);
        assert_eq!(applescript_escape("Wi-Fi"), "Wi-Fi");
        // 含中文/空格保持原样（AppleScript 字面量支持 UTF-8）
        assert_eq!(applescript_escape("长沙 内网"), "长沙 内网");
    }

    #[test]
    fn shell_quote_simple_vs_complex() {
        assert_eq!(shell_quote("en0"), "en0");
        assert_eq!(shell_quote("0.0.0.0"), "0.0.0.0");
        assert_eq!(shell_quote("Wi-Fi"), "Wi-Fi");
        // 含空格 → 单引号
        assert_eq!(shell_quote("a b"), "'a b'");
        // 含单引号 → 转义
        assert_eq!(shell_quote("a'b"), "'a'\\''b'");
    }

    #[test]
    fn set_routes_builds_full_command() {
        // 间接验证：构造的命令字符串应含全部路由字段
        // （真正执行需要授权，这里只验证拼字符串逻辑，不调 set_routes）
        let routes = vec![
            Route::parse("10.0.0.0/8").unwrap(),
            Route::parse("172.16.0.0/12").unwrap(),
        ];
        let mut s = format!("networksetup -setadditionalroutes {}", shell_quote("Wi-Fi"));
        for r in &routes {
            let (d, m, gw) = r.to_set_args();
            s.push(' ');
            s.push_str(&shell_quote(&d));
            s.push(' ');
            s.push_str(&shell_quote(&m));
            s.push(' ');
            s.push_str(&shell_quote(&gw));
        }
        assert!(s.contains("10.0.0.0"));
        assert!(s.contains("255.0.0.0"));
        assert!(s.contains("172.16.0.0"));
        assert!(s.contains("255.240.0.0"));
        // 只有一个 service 字段
        assert_eq!(s.matches("Wi-Fi").count(), 1);
    }

    #[test]
    fn cmd_to_string_rendering() {
        let c = cmd!(networksetup -getadditionalroutes "My Service");
        let s = cmd_to_string(&c);
        assert!(s.starts_with("networksetup"));
        assert!(s.contains("My Service"));
    }
}
