use base64::{Engine as _, engine::general_purpose};
use ntex::web;
use std::time::Duration;

use crate::util::requester::Requester;

fn ssut51(arg: &str) -> u32 {
    arg.chars()
        .filter(|c| c.is_ascii_digit())
        .map(|c| c.to_digit(10).unwrap())
        .sum()
}

fn boo(sum1: u32, sum2: u32) -> String {
    let raw = format!("{}-{}-{}", sum1, "sxyprn.com", sum2);
    let encoded = general_purpose::STANDARD.encode(raw);
    encoded
        .replace('+', "-")
        .replace('/', "_")
        .replace('=', ".")
}

/// Extracts all CDN path values from the data-vnfo JSON attribute.
pub(crate) fn extract_all_cdn_paths(html: &str) -> Vec<String> {
    let json_str = match html.split("data-vnfo='").nth(1) {
        Some(s) => s.split('\'').next().unwrap_or(""),
        None => return vec![],
    };

    if let Ok(serde_json::Value::Object(map)) = serde_json::from_str(json_str) {
        let paths: Vec<String> = map
            .values()
            .filter_map(|v| v.as_str().map(|s| s.replace('\\', "")))
            .filter(|s| !s.is_empty())
            .collect();
        if !paths.is_empty() {
            return paths;
        }
    }

    // Fallback: original single-value extraction
    let first = json_str
        .split("\":\"")
        .nth(1)
        .and_then(|s| s.split("\"}").next())
        .map(|s| s.replace('\\', ""))
        .unwrap_or_default();
    if first.is_empty() {
        vec![]
    } else {
        vec![first]
    }
}

/// Applies the sxyprn segment transformation to produce the pre-redirect CDN URL.
pub(crate) fn transform_cdn_path(path: &str) -> Option<String> {
    let mut tmp: Vec<String> = path.split('/').map(|s| s.to_string()).collect();
    if tmp.len() < 8 {
        return None;
    }
    let s6 = ssut51(&tmp[6]);
    let s7 = ssut51(&tmp[7]);
    tmp[1] = format!("{}8/{}", tmp[1], boo(s6, s7));
    tmp[5] = format!("{}", tmp[5].parse::<u32>().unwrap_or(0).saturating_sub(s6 + s7));
    Some(format!("https://sxyprn.com{}", tmp.join("/")))
}

/// Races all candidate CDN URLs concurrently via blocking curl HEAD requests.
/// Returns the final CDN URL from the first candidate that successfully redirects.
async fn race_cdn_urls(candidate_urls: Vec<String>) -> String {
    if candidate_urls.is_empty() {
        return String::new();
    }

    let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(candidate_urls.len());

    for cdn_url in candidate_urls {
        let tx = tx.clone();
        tokio::spawn(async move {
            let result = tokio::task::spawn_blocking(move || {
                crate::util::get_redirect_location(&cdn_url)
                    .ok()
                    .flatten()
                    .map(|loc| format!("https:{}", loc))
            })
            .await
            .ok()
            .flatten();

            if let Some(url) = result {
                let _ = tx.send(url).await;
            }
        });
    }
    drop(tx);

    tokio::time::timeout(Duration::from_secs(15), rx.recv())
        .await
        .ok()
        .flatten()
        .unwrap_or_default()
}

/// Resolves all candidate CDN mirror URLs concurrently, collecting every
/// mirror that redirects successfully within the timeout window. Unlike
/// `race_cdn_urls`, this does not stop at the first success -- the mirrors
/// are redundant copies of the same stream, and callers that want to serve
/// directly-playable format URLs need as many working ones as possible.
pub(crate) async fn resolve_all_cdn_urls(candidate_urls: Vec<String>) -> Vec<String> {
    if candidate_urls.is_empty() {
        return vec![];
    }

    let handles: Vec<_> = candidate_urls
        .into_iter()
        .map(|cdn_url| {
            tokio::spawn(async move {
                tokio::task::spawn_blocking(move || {
                    crate::util::get_redirect_location(&cdn_url)
                        .ok()
                        .flatten()
                        .map(|loc| format!("https:{}", loc))
                })
                .await
                .ok()
                .flatten()
            })
        })
        .collect();

    let deadline = tokio::time::Instant::now() + Duration::from_secs(15);
    let mut resolved = Vec::new();
    for handle in handles {
        if let Ok(Ok(Some(url))) = tokio::time::timeout_at(deadline, handle).await {
            if !resolved.contains(&url) {
                resolved.push(url);
            }
        }
    }
    resolved
}

/// Fetches the sxyprn detail page for `slug` and resolves every mirror CDN
/// URL it advertises. Used by the provider to eagerly populate `formats`
/// for app clients, instead of the lazy single-mirror `/proxy/sxyprn/...`
/// redirect used by other clients.
pub(crate) async fn resolve_all_media_urls(
    requester: &mut Requester,
    base_url: &str,
    slug: &str,
) -> Vec<String> {
    let full_url = format!("{}/post/{}", base_url, slug);
    let text = requester.get(&full_url, None).await.unwrap_or_default();
    if text.is_empty() {
        return vec![];
    }

    let candidate_urls: Vec<String> = extract_all_cdn_paths(&text)
        .iter()
        .filter_map(|p| transform_cdn_path(p))
        .collect();

    resolve_all_cdn_urls(candidate_urls).await
}

#[derive(Debug, Clone)]
pub struct SxyprnProxy {}

impl SxyprnProxy {
    pub fn new() -> Self {
        SxyprnProxy {}
    }

    pub async fn get_video_url(
        &self,
        url: String,
        requester: web::types::State<Requester>,
    ) -> String {
        if let Some(encoded) = url.strip_prefix("race/") {
            return self.resolve_race(encoded).await;
        }

        let mut requester = requester.get_ref().clone();
        let full_url = format!("https://sxyprn.com/{}", url);
        let text = requester.get(&full_url, None).await.unwrap_or_default();
        if text.is_empty() {
            return String::new();
        }

        let cdn_paths = extract_all_cdn_paths(&text);
        let candidate_urls: Vec<String> = cdn_paths
            .iter()
            .filter_map(|p| transform_cdn_path(p))
            .collect();

        if candidate_urls.is_empty() {
            return String::new();
        }

        let joined = candidate_urls.join("|");
        let encoded = general_purpose::URL_SAFE_NO_PAD.encode(joined.as_bytes());
        format!("/proxy/sxyprn/race/{}", encoded)
    }

    async fn resolve_race(&self, encoded: &str) -> String {
        let bytes = match general_purpose::URL_SAFE_NO_PAD.decode(encoded.as_bytes()) {
            Ok(b) => b,
            Err(_) => return String::new(),
        };
        let decoded = match String::from_utf8(bytes) {
            Ok(s) => s,
            Err(_) => return String::new(),
        };

        let urls: Vec<String> = decoded
            .split('|')
            .filter(|s| !s.is_empty())
            .map(|s| s.to_string())
            .collect();

        race_cdn_urls(urls).await
    }
}
