use crate::models::{LocalSingBoxConfig, SubscriptionCache};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::{
    env, fs,
    path::Path,
    process::Command,
    time::{SystemTime, UNIX_EPOCH},
};

pub const SINGBOX_ADAPTER_ID: &str = "singbox";
pub const SINGBOX_OUTPUT_FILE: &str = "sing-box-config.json";
pub const DEFAULT_MIXED_INBOUND_TAG: &str = "vpn-proxy-mixed-in";
pub const DEFAULT_VPN_OUTBOUND_TAG: &str = "vpn";
pub const DEFAULT_DIRECT_OUTBOUND_TAG: &str = "direct";
pub const DEFAULT_BLOCK_OUTBOUND_TAG: &str = "block";

const SUPPORTED_PROXY_TYPES: &[&str] = &["vless", "vmess", "trojan", "shadowsocks", "hysteria2"];

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SingBoxAdapter {
    log_level: String,
    inbound_tag: String,
    vpn_outbound_tag: String,
}

impl SingBoxAdapter {
    pub fn new(
        log_level: impl Into<String>,
        inbound_tag: impl Into<String>,
        vpn_outbound_tag: impl Into<String>,
    ) -> Self {
        Self {
            log_level: log_level.into(),
            inbound_tag: inbound_tag.into(),
            vpn_outbound_tag: vpn_outbound_tag.into(),
        }
    }

    pub fn generate_config<C>(
        &self,
        request: SingBoxGenerationRequest<'_>,
        checker: &C,
    ) -> Result<SingBoxGeneratedConfig, SingBoxConfigError>
    where
        C: SingBoxConfigChecker,
    {
        let selected_server_tag = request
            .config
            .selected_server_tag
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .ok_or_else(|| {
                SingBoxConfigError::new(
                    SingBoxConfigErrorKind::MissingSelectedServer,
                    "Сервер Local sing-box не выбран",
                )
            })?;
        let vpn_outbound = selected_outbound(
            &request.subscription_cache.config,
            selected_server_tag,
            &self.vpn_outbound_tag,
        )?;
        let generated_config = json!({
            "log": {
                "disabled": false,
                "level": self.log_level,
                "timestamp": true
            },
            "inbounds": [
                {
                    "type": "mixed",
                    "tag": self.inbound_tag,
                    "listen": request.config.listen_host,
                    "listen_port": request.config.listen_port,
                    "users": [],
                    "set_system_proxy": false
                }
            ],
            "outbounds": [
                vpn_outbound,
                { "type": "direct", "tag": DEFAULT_DIRECT_OUTBOUND_TAG },
                { "type": "block", "tag": DEFAULT_BLOCK_OUTBOUND_TAG }
            ],
            "route": {
                "rules": [
                    { "ip_is_private": true, "outbound": DEFAULT_DIRECT_OUTBOUND_TAG }
                ],
                "final": self.vpn_outbound_tag
            }
        });
        let contents = serde_json::to_string_pretty(&generated_config).map_err(|error| {
            SingBoxConfigError::new(
                SingBoxConfigErrorKind::Serialization,
                format!("Не удалось сериализовать конфиг sing-box: {error}"),
            )
        })?;
        let check = match request.binary_path {
            Some(binary_path) => Some(checker.check_config(binary_path, &contents)?),
            None => None,
        };

        Ok(SingBoxGeneratedConfig {
            adapter_id: SINGBOX_ADAPTER_ID.to_string(),
            output_file_name: SINGBOX_OUTPUT_FILE.to_string(),
            contents,
            selected_server_tag: selected_server_tag.to_string(),
            listen: request.config.listen_host.clone(),
            listen_port: request.config.listen_port,
            check,
        })
    }
}

impl Default for SingBoxAdapter {
    fn default() -> Self {
        Self::new("info", DEFAULT_MIXED_INBOUND_TAG, DEFAULT_VPN_OUTBOUND_TAG)
    }
}

#[derive(Debug, Clone, Copy)]
pub struct SingBoxGenerationRequest<'a> {
    pub config: &'a LocalSingBoxConfig,
    pub subscription_cache: &'a SubscriptionCache,
    pub binary_path: Option<&'a Path>,
}

impl<'a> SingBoxGenerationRequest<'a> {
    pub fn new(
        config: &'a LocalSingBoxConfig,
        subscription_cache: &'a SubscriptionCache,
        binary_path: Option<&'a Path>,
    ) -> Self {
        Self {
            config,
            subscription_cache,
            binary_path,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SingBoxGeneratedConfig {
    pub adapter_id: String,
    pub output_file_name: String,
    pub contents: String,
    pub selected_server_tag: String,
    pub listen: String,
    pub listen_port: u16,
    pub check: Option<SingBoxCheckResult>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SingBoxCheckResult {
    pub checked: bool,
    pub success: bool,
    pub message: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SingBoxConfigError {
    pub kind: SingBoxConfigErrorKind,
    pub message: String,
}

impl SingBoxConfigError {
    pub fn new(kind: SingBoxConfigErrorKind, message: impl Into<String>) -> Self {
        Self {
            kind,
            message: message.into(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SingBoxConfigErrorKind {
    MissingSelectedServer,
    MissingSelectedOutbound,
    UnsupportedSelectedOutbound,
    Serialization,
    CheckFailed,
}

pub trait SingBoxConfigChecker {
    fn check_config(
        &self,
        binary_path: &Path,
        config_json: &str,
    ) -> Result<SingBoxCheckResult, SingBoxConfigError>;
}

#[derive(Debug, Clone, Copy, Default)]
pub struct SingBoxCommandChecker;

impl SingBoxConfigChecker for SingBoxCommandChecker {
    fn check_config(
        &self,
        binary_path: &Path,
        config_json: &str,
    ) -> Result<SingBoxCheckResult, SingBoxConfigError> {
        let config_path = env::temp_dir().join(format!(
            "vpn-proxy-sing-box-{}-{}.json",
            std::process::id(),
            now_millis()
        ));

        fs::write(&config_path, config_json).map_err(|error| {
            SingBoxConfigError::new(
                SingBoxConfigErrorKind::CheckFailed,
                format!(
                    "Не удалось записать временный конфиг sing-box '{}': {error}",
                    config_path.display()
                ),
            )
        })?;

        let output = Command::new(binary_path)
            .arg("check")
            .arg("-c")
            .arg(&config_path)
            .output()
            .map_err(|error| {
                let _ = fs::remove_file(&config_path);
                SingBoxConfigError::new(
                    SingBoxConfigErrorKind::CheckFailed,
                    format!(
                        "Не удалось выполнить '{} check': {error}",
                        binary_path.display()
                    ),
                )
            })?;
        let _ = fs::remove_file(&config_path);

        let stdout = String::from_utf8_lossy(&output.stdout);
        let stderr = String::from_utf8_lossy(&output.stderr);
        let message = command_message(&stdout, &stderr);

        if !output.status.success() {
            return Err(SingBoxConfigError::new(
                SingBoxConfigErrorKind::CheckFailed,
                format!("Проверка sing-box не прошла: {message}"),
            ));
        }

        Ok(SingBoxCheckResult {
            checked: true,
            success: true,
            message: if message.is_empty() {
                "Проверка sing-box прошла успешно".to_string()
            } else {
                message
            },
        })
    }
}

fn selected_outbound(
    subscription_config: &Value,
    selected_server_tag: &str,
    vpn_outbound_tag: &str,
) -> Result<Value, SingBoxConfigError> {
    let outbounds = subscription_config
        .get("outbounds")
        .and_then(Value::as_array)
        .ok_or_else(|| {
            SingBoxConfigError::new(
                SingBoxConfigErrorKind::MissingSelectedOutbound,
                "В cache подписки нет outbounds",
            )
        })?;
    let outbound = outbounds
        .iter()
        .find(|outbound| {
            outbound
                .get("tag")
                .and_then(Value::as_str)
                .is_some_and(|tag| tag.trim() == selected_server_tag)
        })
        .ok_or_else(|| {
            SingBoxConfigError::new(
                SingBoxConfigErrorKind::MissingSelectedOutbound,
                format!("Outbound не найден: {selected_server_tag}"),
            )
        })?;
    let outbound_type = outbound
        .get("type")
        .and_then(Value::as_str)
        .unwrap_or_default();

    if !SUPPORTED_PROXY_TYPES.contains(&outbound_type) {
        return Err(SingBoxConfigError::new(
            SingBoxConfigErrorKind::UnsupportedSelectedOutbound,
            format!(
                "Outbound '{selected_server_tag}' имеет неподдерживаемый тип '{outbound_type}'"
            ),
        ));
    }

    let mut outbound = outbound.clone();
    let object = outbound.as_object_mut().ok_or_else(|| {
        SingBoxConfigError::new(
            SingBoxConfigErrorKind::UnsupportedSelectedOutbound,
            format!("Outbound '{selected_server_tag}' должен быть JSON-объектом"),
        )
    })?;
    object.insert(
        "tag".to_string(),
        Value::String(vpn_outbound_tag.to_string()),
    );
    if outbound_type == "vless" && !object.contains_key("packet_encoding") {
        object.insert(
            "packet_encoding".to_string(),
            Value::String("xudp".to_string()),
        );
    }

    Ok(outbound)
}

fn now_millis() -> u128 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_millis())
        .unwrap_or_default()
}

fn command_message(stdout: &str, stderr: &str) -> String {
    let stdout = stdout.trim();
    let stderr = stderr.trim();

    match (stdout.is_empty(), stderr.is_empty()) {
        (true, true) => String::new(),
        (false, true) => stdout.to_string(),
        (true, false) => stderr.to_string(),
        (false, false) => format!("{stdout}\n{stderr}"),
    }
}
