use crate::models::{decode_percent_encoded_utf8, SubscriptionCache, SubscriptionServer};
use base64::{engine::general_purpose, Engine};
use serde_json::{json, Map, Value};
use std::time::{SystemTime, UNIX_EPOCH};
use url::Url;

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

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

impl SubscriptionError {
    fn new(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
        }
    }
}

impl std::fmt::Display for SubscriptionError {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter.write_str(&self.message)
    }
}

impl std::error::Error for SubscriptionError {}

#[derive(Debug, Clone, PartialEq)]
pub struct ParsedSubscription {
    pub config: Value,
    pub servers: Vec<SubscriptionServer>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubscriptionFetchIdentity {
    pub device_hwid: Option<String>,
    pub app_name: String,
    pub user_agent: String,
    pub device_os: String,
    pub device_os_version: Option<String>,
    pub device_model: String,
}

impl SubscriptionFetchIdentity {
    pub fn with_device_hwid(device_hwid: Option<&str>) -> Self {
        Self {
            device_hwid: device_hwid
                .map(str::trim)
                .filter(|value| !value.is_empty())
                .map(str::to_string),
            ..Self::default()
        }
    }

    pub fn request_headers_without_device_hwid(&self) -> Vec<(&'static str, String)> {
        let mut headers = vec![
            ("User-Agent", self.user_agent.clone()),
            ("X-App-Name", self.app_name.clone()),
            ("X-Device-OS", self.device_os.clone()),
            ("X-Device-Model", self.device_model.clone()),
        ];

        if let Some(device_os_version) = self
            .device_os_version
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
        {
            let header_value = sanitize_header_value(device_os_version);
            if !header_value.is_empty() {
                headers.push(("X-Device-OS-Version", header_value.clone()));
                headers.push(("X-Ver-OS", header_value));
            }
        }

        headers
    }
}

impl Default for SubscriptionFetchIdentity {
    fn default() -> Self {
        let device_os = std::env::consts::OS.to_string();

        Self {
            device_hwid: None,
            app_name: DEFAULT_APP_NAME.to_string(),
            user_agent: format!("{DEFAULT_APP_NAME}/{device_os}"),
            device_os,
            device_os_version: detect_device_os_version(),
            device_model: DEFAULT_APP_NAME.to_string(),
        }
    }
}

pub fn parse_subscription_body(body: &str) -> Result<ParsedSubscription, SubscriptionError> {
    let config = match serde_json::from_str::<Value>(body) {
        Ok(value) => value,
        Err(_) => parse_link_subscription(body)?,
    };
    let servers = servers_from_config(&config)?;

    Ok(ParsedSubscription { config, servers })
}

pub fn parse_user_info(header_value: Option<&str>) -> Map<String, Value> {
    let mut result = Map::new();
    let Some(header_value) = header_value else {
        return result;
    };

    for part in header_value.split(';') {
        let Some((key, value)) = part.trim().split_once('=') else {
            continue;
        };
        let key = key.trim();
        if key.is_empty() {
            continue;
        }
        if let Ok(parsed) = value.trim().parse::<i64>() {
            result.insert(key.to_string(), Value::Number(parsed.into()));
        }
    }

    result
}

pub fn fetch_subscription(url: &str) -> Result<SubscriptionCache, SubscriptionError> {
    fetch_subscription_with_identity(url, &SubscriptionFetchIdentity::default())
}

pub fn fetch_subscription_with_identity(
    url: &str,
    identity: &SubscriptionFetchIdentity,
) -> Result<SubscriptionCache, SubscriptionError> {
    let parsed_url =
        Url::parse(url).map_err(|_| SubscriptionError::new("Invalid subscription URL"))?;
    if !matches!(parsed_url.scheme(), "http" | "https") {
        return Err(SubscriptionError::new(
            "Subscription URL must use http or https",
        ));
    }

    let mut request = reqwest::blocking::Client::new().get(parsed_url);

    for (name, value) in identity.request_headers_without_device_hwid() {
        request = request.header(name, value);
    }

    if let Some(device_hwid) = identity
        .device_hwid
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        request = request.header("x-hwid", device_hwid);
    }

    let response = request
        .send()
        .map_err(|error| SubscriptionError::new(format!("Subscription request failed: {error}")))?;

    let status = response.status();
    if !status.is_success() {
        return Err(SubscriptionError::new(format!(
            "Subscription request failed: HTTP {}",
            status.as_u16()
        )));
    }

    let user_info = parse_user_info(
        response
            .headers()
            .get("subscription-userinfo")
            .and_then(|value| value.to_str().ok()),
    );
    let body = response.text().map_err(|error| {
        SubscriptionError::new(format!("Subscription body read failed: {error}"))
    })?;
    let parsed = parse_subscription_body(&body)?;

    Ok(SubscriptionCache {
        config: parsed.config,
        servers: parsed.servers,
        user_info,
        fetched_at: now_timestamp(),
    })
}

fn parse_link_subscription(body: &str) -> Result<Value, SubscriptionError> {
    let decoded = maybe_decode_base64(body);
    let links = decoded
        .lines()
        .map(str::trim)
        .filter(|line| line.starts_with("vless://"))
        .collect::<Vec<_>>();

    if links.is_empty() {
        return Err(SubscriptionError::new(
            "Subscription does not contain JSON config or VLESS links",
        ));
    }

    let outbounds = links
        .into_iter()
        .map(parse_vless_url)
        .collect::<Result<Vec<_>, _>>()?;

    Ok(json!({ "outbounds": outbounds }))
}

fn parse_vless_url(raw_url: &str) -> Result<Value, SubscriptionError> {
    if !raw_url.starts_with("vless://") {
        return Err(SubscriptionError::new("VLESS URL must start with vless://"));
    }

    let parsed = Url::parse(raw_url).map_err(|_| SubscriptionError::new("Invalid VLESS URL"))?;
    let tag = parsed
        .fragment()
        .map(decode_percent_encoded_utf8)
        .unwrap_or_else(|| "vless-out".to_string());
    let uuid = parsed.username().trim().to_string();
    let server = parsed.host_str().map(str::to_string).unwrap_or_default();
    let server_port = parsed.port_or_known_default().unwrap_or(443);
    let public_key = query_value(&parsed, "pbk").unwrap_or_default();
    let short_id = query_value(&parsed, "sid").unwrap_or_default();
    let server_name = query_value(&parsed, "sni").unwrap_or_else(|| server.clone());
    let fingerprint = query_value(&parsed, "fp").unwrap_or_else(|| "chrome".to_string());
    let flow = query_value(&parsed, "flow").unwrap_or_default();

    if uuid.is_empty() || server.is_empty() {
        return Err(SubscriptionError::new(
            "VLESS URL misses uuid, host or port",
        ));
    }

    if public_key.is_empty() || short_id.is_empty() {
        return Err(SubscriptionError::new(
            "VLESS REALITY parameters pbk and sid are required",
        ));
    }

    Ok(json!({
        "type": "vless",
        "tag": tag,
        "server": server,
        "server_port": server_port,
        "uuid": uuid,
        "flow": flow,
        "tls": {
            "enabled": true,
            "server_name": server_name,
            "utls": {
                "enabled": true,
                "fingerprint": fingerprint
            },
            "reality": {
                "enabled": true,
                "public_key": public_key,
                "short_id": short_id
            }
        },
        "packet_encoding": "xudp"
    }))
}

fn servers_from_config(config: &Value) -> Result<Vec<SubscriptionServer>, SubscriptionError> {
    let servers = config
        .get("outbounds")
        .and_then(Value::as_array)
        .into_iter()
        .flatten()
        .filter_map(server_from_outbound)
        .collect::<Vec<_>>();

    if servers.is_empty() {
        return Err(SubscriptionError::new(
            "No supported proxy outbounds found in subscription",
        ));
    }

    Ok(servers)
}

fn server_from_outbound(outbound: &Value) -> Option<SubscriptionServer> {
    let server_type = outbound.get("type")?.as_str()?.to_string();
    if !SUPPORTED_PROXY_TYPES.contains(&server_type.as_str()) {
        return None;
    }

    let server = outbound
        .get("server")
        .and_then(Value::as_str)
        .unwrap_or("unknown")
        .to_string();
    let server_port = outbound
        .get("server_port")
        .and_then(Value::as_u64)
        .and_then(|value| u16::try_from(value).ok())
        .unwrap_or(443);
    let tag = outbound
        .get("tag")
        .and_then(Value::as_str)
        .map(str::to_string)
        .unwrap_or_else(|| format!("{server_type}-{server}"));

    Some(SubscriptionServer {
        tag,
        server_type,
        server,
        server_port,
    })
}

fn maybe_decode_base64(content: &str) -> String {
    let compact = content.split_whitespace().collect::<String>();
    if compact.is_empty()
        || !compact
            .chars()
            .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '+' | '/' | '=' | '-' | '_'))
    {
        return content.to_string();
    }

    for engine in [general_purpose::STANDARD, general_purpose::URL_SAFE] {
        if let Ok(decoded) = engine.decode(compact.as_bytes()) {
            if let Ok(decoded) = String::from_utf8(decoded) {
                if decoded.contains("vless://") || decoded.contains('{') {
                    return decoded;
                }
            }
        }
    }

    content.to_string()
}

fn query_value(url: &Url, key: &str) -> Option<String> {
    url.query_pairs()
        .find(|(name, _)| name == key)
        .map(|(_, value)| value.into_owned())
}

fn sanitize_header_value(value: &str) -> String {
    value
        .chars()
        .filter(|ch| ch.is_ascii_graphic() || *ch == ' ')
        .collect::<String>()
}

fn detect_device_os_version() -> Option<String> {
    #[cfg(windows)]
    {
        windows_device_os_version()
    }

    #[cfg(not(windows))]
    {
        None
    }
}

#[cfg(windows)]
fn windows_device_os_version() -> Option<String> {
    use winreg::{enums::HKEY_LOCAL_MACHINE, RegKey};

    let current_version = RegKey::predef(HKEY_LOCAL_MACHINE)
        .open_subkey("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion")
        .ok()?;

    let product_name = current_version
        .get_value::<String, _>("ProductName")
        .ok()
        .map(|value| normalize_windows_product_name(&value, &current_version))
        .filter(|value| !value.trim().is_empty());
    let display_version = current_version
        .get_value::<String, _>("DisplayVersion")
        .ok()
        .or_else(|| current_version.get_value::<String, _>("ReleaseId").ok())
        .filter(|value| !value.trim().is_empty());
    let build = current_version
        .get_value::<String, _>("CurrentBuildNumber")
        .ok()
        .or_else(|| current_version.get_value::<String, _>("CurrentBuild").ok())
        .filter(|value| !value.trim().is_empty());
    let ubr = current_version.get_value::<u32, _>("UBR").ok();
    let build = match (build, ubr) {
        (Some(build), Some(ubr)) => Some(format!("{build}.{ubr}")),
        (build, _) => build,
    };

    let mut parts = Vec::new();
    if let Some(product_name) = product_name {
        parts.push(product_name);
    }
    if let Some(display_version) = display_version {
        parts.push(display_version);
    }
    if let Some(build) = build {
        parts.push(format!("build {build}"));
    }

    let version = parts.join(" | ");
    (!version.is_empty()).then_some(version)
}

#[cfg(windows)]
fn normalize_windows_product_name(value: &str, current_version: &winreg::RegKey) -> String {
    let trimmed = value.trim();
    let build_number = current_version
        .get_value::<String, _>("CurrentBuildNumber")
        .ok()
        .or_else(|| current_version.get_value::<String, _>("CurrentBuild").ok())
        .and_then(|value| value.parse::<u32>().ok())
        .unwrap_or_default();

    if build_number >= 22000 && trimmed.starts_with("Windows 10") {
        return trimmed.replacen("Windows 10", "Windows 11", 1);
    }

    trimmed.to_string()
}

fn now_timestamp() -> String {
    let seconds = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_secs())
        .unwrap_or(0);
    format!("unix:{seconds}")
}
