use std::sync::Arc;

use ntex::web;
use regex::{Captures, Regex};
use url::Url;
use wreq::cookie::Jar;
use wreq::redirect::Policy;
use wreq_util::Emulation;

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

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

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

    /// Convert any doodstream URL variant to the embed-player URL (`/e/{id}`).
    /// Handles `/d/` (download page) and pass-through for already-correct `/e/` paths.
    /// Returns `None` only if the host is not an allowed doodstream host.
    pub(crate) fn normalize_embed_url(url: &str) -> Option<String> {
        let parsed = Url::parse(url).ok()?;
        let host = parsed.host_str()?;
        if !Self::is_allowed_host(host) {
            return None;
        }
        let path = parsed.path();
        // Replace /d/ with /e/; everything else stays as-is
        let new_path = if let Some(id) = path.strip_prefix("/d/") {
            format!("/e/{id}")
        } else {
            path.to_string()
        };
        Some(format!("https://{host}{new_path}"))
    }

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

        // is_allowed_detail_url checks host + path prefix (/t/, /e/, /d/)
        if !Self::is_allowed_detail_url(&url) {
            return None;
        }

        // Normalise /d/ (download page) to /e/ (embed player)
        Some(Self::normalize_embed_url(&url).unwrap_or(url))
    }

    fn is_allowed_host(host: &str) -> bool {
        matches!(
            host,
            "turboplayers.xyz"
                | "www.turboplayers.xyz"
                | "trailerhg.xyz"
                | "www.trailerhg.xyz"
                | "streamhg.com"
                | "www.streamhg.com"
                | "doodstream.com"
                | "www.doodstream.com"
        )
    }

    fn is_allowed_detail_url(url: &str) -> bool {
        let Some(url) = Url::parse(url).ok() else {
            return false;
        };
        if url.scheme() != "https" {
            return false;
        }
        let Some(host) = url.host_str() else {
            return false;
        };
        if !Self::is_allowed_host(host) {
            return false;
        }

        url.path().starts_with("/t/")
            || url.path().starts_with("/e/")
            || url.path().starts_with("/d/")
    }

    fn request_origin(detail_url: &str) -> Option<String> {
        let parsed = Url::parse(detail_url).ok()?;
        let host = parsed.host_str()?;
        Some(format!("{}://{}", parsed.scheme(), host))
    }

    fn request_headers(detail_url: &str) -> Vec<(String, String)> {
        let origin = Self::request_origin(detail_url)
            .unwrap_or_else(|| "https://turboplayers.xyz".to_string());
        vec![
            (
                "Referer".to_string(),
                format!("{}/", origin.trim_end_matches('/')),
            ),
            ("Origin".to_string(), origin),
            (
                "Accept".to_string(),
                "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8".to_string(),
            ),
            ("Accept-Language".to_string(), "en-US,en;q=0.9".to_string()),
            ("Sec-Fetch-Site".to_string(), "same-origin".to_string()),
        ]
    }

    fn regex(pattern: &str) -> Option<Regex> {
        Regex::new(pattern).ok()
    }

    fn decode_base36(token: &str) -> Option<usize> {
        usize::from_str_radix(token, 36).ok()
    }

    fn sanitize_media_url(url: &str) -> String {
        url.trim()
            .trim_end_matches('\\')
            .trim_end_matches('"')
            .trim_end_matches('\'')
            .to_string()
    }

    fn extract_literal_url(text: &str) -> Option<String> {
        let direct_patterns = [
            r#"urlPlay\s*=\s*'(?P<url>https?://[^']+)'"#,
            r#"data-hash\s*=\s*"(?P<url>https?://[^"]+)""#,
            r#""(?P<url>https?://[^"]+\.(?:m3u8|mp4)(?:\?[^"]*)?)""#,
            r#"'(?P<url>https?://[^']+\.(?:m3u8|mp4)(?:\?[^']*)?)'"#,
        ];

        for pattern in direct_patterns {
            let Some(regex) = Self::regex(pattern) else {
                continue;
            };
            if let Some(url) = regex
                .captures(text)
                .and_then(|captures| captures.name("url"))
                .map(|value| Self::sanitize_media_url(value.as_str()))
            {
                return Some(url);
            }
        }

        None
    }

    fn extract_packed_eval_args(text: &str) -> Option<(String, usize, usize, Vec<String>)> {
        let regex = Self::regex(
            r#"eval\(function\(p,a,c,k,e,d\)\{.*?\}\('(?P<payload>(?:\\'|\\\\|[^'])*)',(?P<radix>\d+),(?P<count>\d+),'(?P<symbols>(?:\\'|\\\\|[^'])*)'\.split\('\|'\)"#,
        )?;
        let captures = regex.captures(text)?;
        let payload = Self::decode_js_single_quoted(captures.name("payload")?.as_str());
        let radix = captures.name("radix")?.as_str().parse::<usize>().ok()?;
        let count = captures.name("count")?.as_str().parse::<usize>().ok()?;
        let symbols = Self::decode_js_single_quoted(captures.name("symbols")?.as_str());
        let parts = symbols.split('|').map(|value| value.to_string()).collect();
        Some((payload, radix, count, parts))
    }

    fn decode_js_single_quoted(value: &str) -> String {
        let mut result = String::with_capacity(value.len());
        let mut chars = value.chars();

        while let Some(ch) = chars.next() {
            if ch != '\\' {
                result.push(ch);
                continue;
            }

            match chars.next() {
                Some('\\') => result.push('\\'),
                Some('\'') => result.push('\''),
                Some('"') => result.push('"'),
                Some('n') => result.push('\n'),
                Some('r') => result.push('\r'),
                Some('t') => result.push('\t'),
                Some(other) => {
                    result.push('\\');
                    result.push(other);
                }
                None => result.push('\\'),
            }
        }

        result
    }

    fn unpack_packer(text: &str) -> Option<String> {
        let (mut payload, radix, count, symbols) = Self::extract_packed_eval_args(text)?;
        if radix != 36 {
            return None;
        }

        let token_regex = Self::regex(r"\b[0-9a-z]+\b")?;
        payload = token_regex
            .replace_all(&payload, |captures: &Captures| {
                let token = captures
                    .get(0)
                    .map(|value| value.as_str())
                    .unwrap_or_default();
                let Some(index) = Self::decode_base36(token) else {
                    return token.to_string();
                };
                if index >= count {
                    return token.to_string();
                }
                let replacement = symbols.get(index).map(|value| value.as_str()).unwrap_or("");
                if replacement.is_empty() {
                    token.to_string()
                } else {
                    replacement.to_string()
                }
            })
            .to_string();

        Some(payload)
    }

    fn collect_media_candidates(text: &str) -> Vec<String> {
        let Some(regex) = Self::regex(r#"https?://[^\s"'<>]+?\.(?:m3u8|mp4|txt)(?:\?[^\s"'<>]*)?"#)
        else {
            return vec![];
        };

        let mut urls = regex
            .find_iter(text)
            .map(|value| Self::sanitize_media_url(value.as_str()))
            .filter(|url| url.starts_with("https://"))
            .collect::<Vec<_>>();

        urls.sort_by_key(|url| {
            if url.contains(".m3u8") {
                0
            } else if url.contains(".mp4") {
                1
            } else {
                2
            }
        });
        urls.dedup();
        urls
    }

    fn extract_stream_url(text: &str) -> Option<String> {
        if let Some(url) = Self::extract_literal_url(text) {
            return Some(url);
        }

        let unpacked = Self::unpack_packer(text)?;
        Self::collect_media_candidates(&unpacked)
            .into_iter()
            .next()
            .or_else(|| Self::extract_literal_url(&unpacked))
    }

    fn extract_pass_md5_url(text: &str, detail_url: &str) -> Option<String> {
        let decoded = text.replace("\\/", "/");
        let absolute_regex = Self::regex(r#"https?://[^\s"'<>]+/pass_md5/[^\s"'<>]+"#)?;
        if let Some(url) = absolute_regex
            .find(&decoded)
            .map(|value| value.as_str().to_string())
        {
            return Some(url);
        }

        let relative_regex = Self::regex(r#"/pass_md5/[^\s"'<>]+"#)?;
        let relative = relative_regex.find(&decoded)?.as_str();
        let origin = Self::request_origin(detail_url)?;
        Some(format!("{origin}{relative}"))
    }

    fn random_alphanumeric(len: usize) -> String {
        let chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        let mut seed = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .subsec_nanos() as u64;
        (0..len)
            .map(|_| {
                seed = seed
                    .wrapping_mul(6364136223846793005)
                    .wrapping_add(1442695040888963407);
                chars[(seed >> 33) as usize % chars.len()] as char
            })
            .collect()
    }

    fn compose_pass_md5_media_url(pass_md5_url: &str, response_body: &str) -> Option<String> {
        let raw = response_body
            .trim()
            .trim_matches('"')
            .trim_matches('\'')
            .replace("\\/", "/");
        if raw.is_empty() {
            return None;
        }

        let mut media_url = if raw.starts_with("https://") || raw.starts_with("http://") {
            raw
        } else if let Some(rest) = raw.strip_prefix("//") {
            format!("https://{rest}")
        } else {
            let parsed = Url::parse(pass_md5_url).ok()?;
            let host = parsed.host_str()?;
            format!("{}://{}{}", parsed.scheme(), host, raw)
        };

        let parsed = Url::parse(pass_md5_url).ok()?;

        if let Some(query) = parsed.query().map(str::to_string) {
            // Old format: token and expiry are in the pass_md5 query string
            if !query.is_empty() && !media_url.contains("token=") {
                let separator = if media_url.contains('?') { '&' } else { '?' };
                media_url.push(separator);
                media_url.push_str(&query);
            }
        } else {
            // New doodstream format: token is the last path segment, base URL needs
            // a random 10-char suffix and expiry appended (mirrors makePlay() in the player JS)
            let token = parsed.path_segments()?.last()?.to_string();
            if !token.is_empty() {
                let rand = Self::random_alphanumeric(10);
                let expiry = std::time::SystemTime::now()
                    .duration_since(std::time::UNIX_EPOCH)
                    .unwrap_or_default()
                    .as_millis();
                media_url.push_str(&rand);
                media_url.push_str(&format!("?token={token}&expiry={expiry}"));
            }
        }

        Some(Self::sanitize_media_url(&media_url))
    }

    async fn resolve_stream_from_pass_md5(
        page_url: &str,
        html: &str,
        requester: &mut Requester,
    ) -> Option<String> {
        let pass_md5_url = Self::extract_pass_md5_url(html, page_url).or_else(|| {
            Self::unpack_packer(html)
                .and_then(|unpacked| Self::extract_pass_md5_url(&unpacked, page_url))
        })?;

        let headers = vec![
            ("Referer".to_string(), page_url.to_string()),
            ("X-Requested-With".to_string(), "XMLHttpRequest".to_string()),
            ("Accept".to_string(), "*/*".to_string()),
        ];
        let response = requester
            .get_with_headers(&pass_md5_url, headers, None)
            .await
            .ok()?;
        Self::compose_pass_md5_media_url(&pass_md5_url, &response)
    }
}

impl DoodstreamProxy {
    // Cloudflare on playmogo.com (where doodstream.com redirects) requires a Chrome TLS
    // fingerprint. Firefox136 (the default Requester emulation) gets 403. Chrome120 bypasses it.
    fn build_chrome_client() -> Option<wreq::Client> {
        let jar = Arc::new(Jar::default());
        wreq::Client::builder()
            .cert_verification(false)
            .emulation(Emulation::Chrome120)
            .cookie_provider(jar)
            .redirect(Policy::default())
            .build()
            .ok()
    }

    pub(crate) async fn try_chrome_extraction(detail_url: &str) -> Option<String> {
        let client = Self::build_chrome_client()?;

        // No version override — let Chrome120 emulation negotiate HTTP/2 via ALPN,
        // which Cloudflare requires for Chrome fingerprints (HTTP/1.1 gets 403)
        let mut request = client.get(detail_url);
        for (key, value) in Self::request_headers(detail_url) {
            request = request.header(key, value);
        }
        let response = request.send().await.ok()?;
        if !response.status().is_success() {
            return None;
        }
        let effective_url = response.url().to_string();
        let html = response.text().await.ok()?;

        if let Some(url) = Self::extract_stream_url(&html) {
            return Some(url);
        }

        let pass_md5_url = Self::extract_pass_md5_url(&html, &effective_url).or_else(|| {
            Self::unpack_packer(&html)
                .and_then(|unpacked| Self::extract_pass_md5_url(&unpacked, &effective_url))
        })?;

        let pm_response = client
            .get(&pass_md5_url)
            .header("Referer", &effective_url)
            .header("X-Requested-With", "XMLHttpRequest")
            .header("Accept", "*/*")
            .send()
            .await
            .ok()?;

        if !pm_response.status().is_success() {
            return None;
        }
        let pm_body = pm_response.text().await.ok()?;
        Self::compose_pass_md5_media_url(&pass_md5_url, &pm_body)
    }
}

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

        // Chrome120 emulation bypasses Cloudflare on playmogo.com (doodstream's redirect target)
        if let Some(cdn_url) = Self::try_chrome_extraction(&detail_url).await {
            return cdn_url;
        }

        // Fallback: standard Requester (Firefox136 + FlareSolverr)
        let mut requester = requester.get_ref().clone();
        let (html, effective_url) = match requester
            .get_raw_with_headers(&detail_url, Self::request_headers(&detail_url))
            .await
        {
            Ok(response) if response.status().is_success() => {
                let effective_url = response.url().to_string();
                match response.text().await {
                    Ok(text) => (text, effective_url),
                    Err(_) => return String::new(),
                }
            }
            _ => {
                let html = match requester
                    .get_with_headers(&detail_url, Self::request_headers(&detail_url), None)
                    .await
                {
                    Ok(text) => text,
                    Err(_) => return String::new(),
                };
                (html, detail_url.clone())
            }
        };

        if let Some(url) = Self::extract_stream_url(&html) {
            return url;
        }

        if let Some(url) =
            Self::resolve_stream_from_pass_md5(&effective_url, &html, &mut requester).await
        {
            return url;
        }

        String::new()
    }
}

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

    #[test]
    fn allows_only_known_doodstream_hosts() {
        assert!(DoodstreamProxy::is_allowed_detail_url(
            "https://turboplayers.xyz/t/69bdfb21cc640"
        ));
        assert!(DoodstreamProxy::is_allowed_detail_url(
            "https://trailerhg.xyz/e/ttdc7a6qpskt"
        ));
        assert!(DoodstreamProxy::is_allowed_detail_url(
            "https://doodstream.com/e/31xp1rqt975g"
        ));
        assert!(!DoodstreamProxy::is_allowed_detail_url(
            "http://turboplayers.xyz/t/69bdfb21cc640"
        ));
        assert!(!DoodstreamProxy::is_allowed_detail_url(
            "https://example.com/t/69bdfb21cc640"
        ));
    }

    #[test]
    fn extracts_clear_hls_url_from_turboplayers_layout() {
        let html = r#"
            <div id="video_player" data-hash="https://cdn4.turboviplay.com/data1/69bdfa8ce1f4d/69bdfa8ce1f4d.m3u8"></div>
            <script>
                var urlPlay = 'https://cdn4.turboviplay.com/data1/69bdfa8ce1f4d/69bdfa8ce1f4d.m3u8';
            </script>
        "#;

        assert_eq!(
            DoodstreamProxy::extract_stream_url(html).as_deref(),
            Some("https://cdn4.turboviplay.com/data1/69bdfa8ce1f4d/69bdfa8ce1f4d.m3u8")
        );
    }

    #[test]
    fn unpacks_streamhg_style_player_config() {
        let html = r#"
            <script type='text/javascript'>
            eval(function(p,a,c,k,e,d){while(c--)if(k[c])p=p.replace(new RegExp('\\b'+c.toString(a)+'\\b','g'),k[c]);return p}('0 1={\"2\":\"https://cdn.example/master.m3u8?t=1\",\"3\":\"https://cdn.example/master.txt\"};4(\"5\").6({7:[{8:1.2,9:\"a\"}]});',36,11,'var|links|hls2|hls3|jwplayer|vplayer|setup|sources|file|type|hls'.split('|')))
            </script>
        "#;

        assert_eq!(
            DoodstreamProxy::extract_stream_url(html).as_deref(),
            Some("https://cdn.example/master.m3u8?t=1")
        );
    }

    #[test]
    fn composes_media_url_from_pass_md5_response() {
        let pass_md5_url =
            "https://trailerhg.xyz/pass_md5/abc123/def456?token=t0k3n&expiry=1775000000";
        let body = "https://g4vsrqvtrj.pinebrookproductionlab.shop/1ghkpx2e8jnal/hls3/01/08534/syyzvotfnhaa_l/master.txt";
        assert_eq!(
            DoodstreamProxy::compose_pass_md5_media_url(pass_md5_url, body).as_deref(),
            Some(
                "https://g4vsrqvtrj.pinebrookproductionlab.shop/1ghkpx2e8jnal/hls3/01/08534/syyzvotfnhaa_l/master.txt?token=t0k3n&expiry=1775000000"
            )
        );
    }

    #[test]
    fn composes_media_url_from_pass_md5_response_new_format() {
        // New doodstream format: token in path, no query string, base URL needs
        // random suffix + ?token=TOKEN&expiry=TIMESTAMP appended
        let pass_md5_url =
            "https://playmogo.com/pass_md5/263443276-hash/wyr3joknzwbzdhufty55banc";
        let body =
            "https://mx273o.cloudatacdn.com/u5kj6mn5xpa3sdgge7d24z/vgz4woi6uq~";
        let result = DoodstreamProxy::compose_pass_md5_media_url(pass_md5_url, body)
            .expect("should produce a URL");
        assert!(result.starts_with(body), "must start with base URL");
        assert!(
            result.contains("?token=wyr3joknzwbzdhufty55banc&expiry="),
            "must contain token and expiry"
        );
        // random suffix is 10 chars between base URL and '?'
        let suffix_start = body.len();
        let query_start = result.find('?').unwrap();
        assert_eq!(query_start - suffix_start, 10, "random suffix must be 10 chars");
    }

    #[test]
    fn extracts_relative_pass_md5_url() {
        let html = r#"
            <script>
                var file = "/pass_md5/abc123/def456?token=t0k3n&expiry=1775000000";
            </script>
        "#;
        assert_eq!(
            DoodstreamProxy::extract_pass_md5_url(html, "https://trailerhg.xyz/e/ttdc7a6qpskt")
                .as_deref(),
            Some("https://trailerhg.xyz/pass_md5/abc123/def456?token=t0k3n&expiry=1775000000")
        );
    }
}
