use crate::util::browser;
use crate::util::requester::Requester;
use ntex::web;
use regex::Regex;
use std::time::{SystemTime, UNIX_EPOCH};
use url::Url;

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

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

    fn normalize_detail_url(url: &str) -> Option<String> {
        let normalized = if url.starts_with("http://") || url.starts_with("https://") {
            url.to_string()
        } else {
            format!("https://{}", url.trim_start_matches('/'))
        };
        Self::is_allowed_detail_url(&normalized).then_some(normalized)
    }

    fn is_allowed_detail_url(url: &str) -> bool {
        let Ok(url) = Url::parse(url) else {
            return false;
        };
        if url.scheme() != "https" {
            return false;
        }
        let Some(host) = url.host_str() else {
            return false;
        };
        matches!(host, "pimpbunny.com" | "www.pimpbunny.com")
            && !url.path().starts_with("/contents/videos_screenshots/")
    }

    /// Generate 32-char numeric key from a pimpbunny license_code (e.g. '$576262819011919').
    ///
    /// Stage 1 of the KVS player URL decoder, extracted from kt_player.js:
    ///  - d_raw = license_code (keeps '$' at index 0 for the d[g+h] lookup)
    ///  - d_mod = strip '$', replace every '0' digit with '1'
    ///  - split d_mod at midpoint j = floor(len/2) to get k and l
    ///  - f_str = 4 * |k - l| as a string
    ///  - for g in 0..=j, h in 1..=4: n = d_raw[g+h] + f_str[g]; if n >= 10 then n -= 10
    fn generate_key(license_code: &str) -> Option<String> {
        let d_raw = license_code;
        // Strip '$' and replace '0' with '1'
        let d_mod: String = license_code
            .get(1..)?
            .chars()
            .map(|c| if c == '0' { '1' } else { c })
            .collect();

        let j = d_mod.len() / 2;
        let k: i64 = d_mod.get(..j + 1)?.parse().ok()?;
        let l: i64 = d_mod.get(j..)?.parse().ok()?;
        let f_str = (4 * (k - l).abs()).to_string();

        let threshold = 10i64;
        let d_bytes = d_raw.as_bytes();
        let f_bytes = f_str.as_bytes();

        let mut key = String::with_capacity(32);
        for g in 0..=j {
            for h in 1..=4usize {
                let d_digit = d_bytes.get(g + h).copied().unwrap_or(b'0') as i64 - b'0' as i64;
                let f_digit = f_bytes.get(g).copied().unwrap_or(b'0') as i64 - b'0' as i64;
                let mut n = d_digit + f_digit;
                if n >= threshold {
                    n -= threshold;
                }
                key.push((b'0' + n as u8) as char);
            }
        }
        Some(key)
    }

    /// Shuffle (permute) a 32-char hex hash using the key.
    ///
    /// Stage 2: for k from 31 down to 0, compute l = (k + sum of key[k..]) % 32, swap h[k] and h[l].
    fn shuffle_hash(hash: &str, key: &str) -> String {
        let mut h: Vec<char> = hash.chars().collect();
        let n = h.len();
        let key_bytes = key.as_bytes();
        for k in (0..n).rev() {
            let mut l = k as i64;
            for m in k..key_bytes.len() {
                l += (key_bytes[m] - b'0') as i64;
            }
            l %= n as i64;
            h.swap(k, l as usize);
        }
        h.into_iter().collect()
    }

    /// Decode a KVS-encoded 'function/N/https://…' video URL into a usable URL.
    ///
    /// Stage 3: strip the function/N/ prefix, shuffle the 32-char hash in segment 6,
    /// then append ?rnd=<epoch_ms>.
    fn decode_encoded_url(encoded: &str, license_code: &str) -> Option<String> {
        let after_prefix = encoded.strip_prefix("function/")?;
        // Split '0/https://host/path/hash_segment/…' into parts by '/'
        let parts: Vec<&str> = after_prefix.split('/').collect();
        // Layout: [0]version [1]'https:' [2]'' [3]host [4]'get_file' [5]segment_no [6]hash_segment …
        if parts.len() < 7 {
            return None;
        }
        let hash_segment = parts[6];
        if hash_segment.len() < 32 {
            return None;
        }

        let key = Self::generate_key(license_code)?;
        let shuffled = Self::shuffle_hash(&hash_segment[..32], &key);
        let new_hash_segment = format!("{}{}", shuffled, &hash_segment[32..]);

        let mut new_parts: Vec<String> = parts.iter().map(|s| s.to_string()).collect();
        new_parts[6] = new_hash_segment;
        new_parts.remove(0); // remove version number

        let rnd = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_millis();

        let url = new_parts.join("/");
        Some(format!("{}?rnd={}", url, rnd))
    }

    /// Extract the license_code from the KVS player config in the page HTML.
    /// The license_code property holds a 16-char string starting with '$'.
    fn extract_license_code(html: &str) -> Option<String> {
        let re = Regex::new(r"license_code:\s*'(\$[0-9]{15})'").ok()?;
        Some(re.captures(html)?[1].to_string())
    }

    /// Extract all 'function/…' video URLs from the KVS player config in the page HTML.
    /// Returns them in document order (lowest to highest quality for pimpbunny).
    fn extract_video_urls(html: &str) -> Vec<String> {
        let Ok(re) = Regex::new(r"video(?:_alt)?_url\d*:\s*'(function/[^']+)'") else {
            return vec![];
        };
        re.captures_iter(html)
            .map(|cap| cap[1].to_string())
            .collect()
    }

    /// GET the decoded URL without following redirects and return the Location header
    /// if the server responds with a 302, or None if it responds with anything else
    /// (including 403 "access denied").
    ///
    /// The PHPSESSID cookie — acquired when we fetched the detail page — must be
    /// forwarded; the server won't issue the 302 without it.
    async fn follow_302(url: &str, requester: &Requester) -> Option<String> {
        let client = wreq::Client::builder()
            .redirect(wreq::redirect::Policy::none())
            .build()
            .ok()?;
        let mut req = client.get(url);
        if let Some(cookie) = requester.cookie_header_for_url(url) {
            req = req.header("Cookie", cookie);
        }
        let resp = req.send().await.ok()?;
        if resp.status().as_u16() != 302 {
            return None;
        }
        resp.headers()
            .get("location")
            .and_then(|v| v.to_str().ok())
            .map(|s| s.to_string())
    }

    /// Try to decode the video URL using the KVS algorithm extracted from kt_player.js.
    /// Returns the CDN redirect target (Location from the 302) on success, None otherwise.
    async fn try_decode(detail_url: &str, requester: &mut Requester) -> Option<String> {
        let html = requester.get(detail_url, None).await.ok()?;

        let license_code = Self::extract_license_code(&html)?;
        let encoded_urls = Self::extract_video_urls(&html);
        if encoded_urls.is_empty() {
            return None;
        }

        // Attempt highest quality first (last URL in the config = highest non-redirect quality).
        for encoded_url in encoded_urls.iter().rev() {
            let Some(decoded) = Self::decode_encoded_url(encoded_url, &license_code) else {
                continue;
            };
            if let Some(location) = Self::follow_302(&decoded, requester).await {
                return Some(location);
            }
        }

        None
    }
}

impl crate::proxies::Proxy for PimpbunnyProxy {
    async fn get_video_url(
        &self,
        url: String,
        requester: web::types::State<crate::util::requester::Requester>,
    ) -> String {
        let Some(detail_url) = Self::normalize_detail_url(&url) else {
            return String::new();
        };

        let mut req = requester.get_ref().clone();

        if let Some(video_url) = Self::try_decode(&detail_url, &mut req).await {
            return video_url;
        }

        // Fall back to browser-based extraction when the decoder fails or the decoded
        // URL is rejected (access denied).
        browser::wait_for_src(&detail_url, "//video[@src]")
            .await
            .unwrap_or_default()
    }
}

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

    #[test]
    fn test_normalize_adds_https_scheme() {
        let url =
            PimpbunnyProxy::normalize_detail_url("pimpbunny.com/videos/some-video/").unwrap();
        assert!(url.starts_with("https://"));
    }

    #[test]
    fn test_normalize_passes_through_valid_https() {
        let url = "https://pimpbunny.com/videos/some-video/";
        assert_eq!(
            PimpbunnyProxy::normalize_detail_url(url).unwrap(),
            url
        );
    }

    #[test]
    fn test_normalize_rejects_screenshots_path() {
        let url =
            "https://pimpbunny.com/contents/videos_screenshots/473000/473894/preview.jpg";
        assert!(PimpbunnyProxy::normalize_detail_url(url).is_none());
    }

    #[test]
    fn test_normalize_rejects_other_host() {
        assert!(PimpbunnyProxy::normalize_detail_url("https://evil.com/videos/x/").is_none());
    }

    #[test]
    fn test_normalize_rejects_http() {
        assert!(PimpbunnyProxy::normalize_detail_url(
            "http://pimpbunny.com/videos/some-video/"
        )
        .is_none());
    }

    #[test]
    fn test_www_subdomain_is_allowed() {
        let url = "https://www.pimpbunny.com/videos/some-video/";
        assert!(PimpbunnyProxy::normalize_detail_url(url).is_some());
    }

    #[test]
    fn test_generate_key() {
        let key = PimpbunnyProxy::generate_key("$576262819011919").unwrap();
        assert_eq!(key, "68732171060626281736958625345345");
        assert_eq!(key.len(), 32);
    }

    #[test]
    fn test_shuffle_hash() {
        let key = "68732171060626281736958625345345";
        let input = "7e4df9f504c7ec5b02b0101ed28edfa4";
        let output = PimpbunnyProxy::shuffle_hash(input, key);
        assert_eq!(output, "fde01fe04b42c7e72d0d0a8c95b1e45f");
    }

    #[test]
    fn test_decode_encoded_url() {
        let encoded = "function/0/https://pimpbunny.com/get_file/40/7e4df9f504c7ec5b02b0101ed28edfa4c687186454/564000/564161/564161_720p.mp4/";
        let license = "$576262819011919";
        let decoded = PimpbunnyProxy::decode_encoded_url(encoded, license).unwrap();
        assert!(decoded.starts_with(
            "https://pimpbunny.com/get_file/40/fde01fe04b42c7e72d0d0a8c95b1e45fc687186454/564000/564161/564161_720p.mp4/"
        ));
        assert!(decoded.contains("?rnd="));
    }

    #[test]
    fn test_extract_license_code() {
        let html = r#"license_code: '$576262819011919', lrc: '77028724',"#;
        assert_eq!(
            PimpbunnyProxy::extract_license_code(html).unwrap(),
            "$576262819011919"
        );
    }

    #[test]
    fn test_extract_video_urls() {
        let html = r#"
            video_url: 'function/0/https://pimpbunny.com/get_file/34/abc/530000/530112/530112_pb_360p.mp4/',
            video_alt_url: 'function/0/https://pimpbunny.com/get_file/34/def/530000/530112/530112_pb_480p.mp4/',
            video_alt_url2: 'function/0/https://pimpbunny.com/get_file/34/ghi/530000/530112/530112_pb_720p.mp4/',
            video_alt_url5: 'https://pimpbunny.com/?upgrade=true',
        "#;
        let urls = PimpbunnyProxy::extract_video_urls(html);
        assert_eq!(urls.len(), 3);
        assert!(urls[0].starts_with("function/0/"));
        assert!(urls[2].contains("720p"));
    }

    #[tokio::test]
    async fn test_live_extract_charlotte_sins() {
        let url = "https://pimpbunny.com/videos/charlotte-sins-gets-drilled-by-jax-slayher/";
        let src = browser::wait_for_src(url, "//video[@src]").await;
        println!("video src: {:?}", src);
        assert!(src.is_some(), "expected a video URL but got None");
        let src = src.unwrap();
        assert!(src.starts_with("http"), "expected http URL, got: {src}");
    }
}
