//! 应用级错误类型。
//!
//! 所有 IO、命令执行、输出解析错误统一归一到 [`AppError`]，
//! UI 层捕获后展示在错误弹窗中，绝不 panic。

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

/// 应用错误。
///
/// 使用 `thiserror` 派生 `Display` + `From`，让 `?` 自动转换。
#[derive(Debug, Error)]
pub enum AppError {
    /// 执行外部命令失败（命令不存在、无权限等）。
    #[error("执行命令失败 `{cmd}`: {source}")]
    CommandSpawn {
        cmd: String,
        #[source]
        source: std::io::Error,
    },

    /// 命令执行了但以非零状态退出。
    #[error("命令退出码非零 `{cmd}` (code={code:?}):\n{stderr}")]
    CommandExit {
        cmd: String,
        code: Option<i32>,
        stderr: String,
    },

    /// 命令输出不是合法 UTF-8（理论上 networksetup 不会触发）。
    #[error("命令输出非 UTF-8: {0}")]
    NonUtf8(#[from] std::string::FromUtf8Error),

    /// networksetup 输出格式不符合预期。
    #[error("解析命令输出失败: {0}")]
    Parse(String),

    /// 用户输入不合法（CIDR 格式错误等）。
    #[error("无效输入: {0}")]
    InvalidInput(String),

    /// osascript 提权被用户取消或失败。
    #[error("提权失败: {0}")]
    Elevation(String),

    /// 兜底。
    #[error("{0}")]
    Other(String),
}

impl AppError {
    /// 构造一个"命令执行失败"错误，自动从 `Command` 提取可读字符串。
    pub fn spawn(cmd: &Command, source: std::io::Error) -> Self {
        Self::CommandSpawn {
            cmd: cmd_to_string(cmd),
            source,
        }
    }

    /// 构造一个"命令退出码非零"错误。
    pub fn exit(cmd: &Command, code: Option<i32>, stderr: impl Into<String>) -> Self {
        Self::CommandExit {
            cmd: cmd_to_string(cmd),
            code,
            stderr: stderr.into(),
        }
    }

    /// 简易 `Other` 包装。
    pub fn other(msg: impl Into<String>) -> Self {
        Self::Other(msg.into())
    }
}

/// 将 `Command` 渲染为形如 `networksetup -getadditionalroutes Wi-Fi` 的字符串，
/// 仅用于错误展示。
pub fn cmd_to_string(cmd: &Command) -> String {
    let prog = cmd.get_program().to_string_lossy();
    let args: Vec<String> = cmd
        .get_args()
        .map(|a| {
            let s = a.to_string_lossy().to_string();
            if s.contains(' ') || s.contains('"') {
                format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
            } else {
                s
            }
        })
        .collect();
    if args.is_empty() {
        prog.to_string()
    } else {
        format!("{} {}", prog, args.join(" "))
    }
}

pub type AppResult<T> = Result<T, AppError>;
