use aes::Aes256;
use base64::{Engine as _, engine::general_purpose::STANDARD as B64};
use cbc::cipher::{BlockDecryptMut, KeyIvInit, block_padding::Pkcs7};
use hmac::Hmac;
use ntex::web;
use pbkdf2::pbkdf2;
use sha2::Sha512;

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

const BASE_URL: &str = "https://www.thepornbunny.com";
const OKRU_API: &str = "https://api.ok.ru/fb.do?application_key=CBAFJIICABABABABA\
    &fields=video.url_tiny%2Cvideo.url_low%2Cvideo.url_high%2Cvideo.url_medium\
    %2Cvideo.url_quadhd%2Cvideo.url_mobile%2Cvideo.url_ultrahd%2Cvideo.url_fullhd%2C\
    &method=video.get&format=json&session_key=";

pub async fn redirect_to_media(
    req: web::HttpRequest,
    requester: web::types::State<Requester>,
) -> impl web::Responder {
    let slug = req
        .match_info()
        .query("slug")
        .trim_matches('/')
        .to_string();

    if slug.is_empty() {
        return web::HttpResponse::NotFound().finish();
    }

    if req.method() == ntex::http::Method::HEAD {
        return web::HttpResponse::Ok().finish();
    }

    let page_url = format!("{BASE_URL}/video/{slug}/");
    let mut client = requester.get_ref().clone();

    let html = match client.get(&page_url, None).await {
        Ok(h) => h,
        Err(_) => return web::HttpResponse::BadGateway().finish(),
    };

    let (enc_data, key, rnd) = match extract_generate_mp4_args(&html) {
        Some(args) => args,
        None => {
            // Fallback: try legacy get_stream approach
            return match extract_and_resolve_stream(&html).await {
                Some(url) => web::HttpResponse::Found().header("Location", url).finish(),
                None => web::HttpResponse::NotFound().finish(),
            };
        }
    };

    let session_key = match decrypt_session_key(&enc_data, &key) {
        Some(k) => k,
        None => return web::HttpResponse::BadGateway().finish(),
    };

    let api_url = format!("{OKRU_API}{session_key}&vids={rnd}");
    let json = match client.get(&api_url, None).await {
        Ok(j) => j,
        Err(_) => return web::HttpResponse::BadGateway().finish(),
    };

    match extract_best_okru_url(&json) {
        Some(url) => web::HttpResponse::Found().header("Location", url).finish(),
        None => web::HttpResponse::BadGateway().finish(),
    }
}

fn extract_generate_mp4_args(html: &str) -> Option<(String, String, String)> {
    let call_start = html.find("generate_mp4(")?;
    let after = &html[call_start + "generate_mp4(".len()..];
    let close = after.find(')')?;
    let args_str = &after[..close];

    let mut args = Vec::new();
    let mut in_quote = false;
    let mut quote_char = ' ';
    let mut current = String::new();
    let mut escape = false;

    for ch in args_str.chars() {
        if escape {
            current.push(ch);
            escape = false;
            continue;
        }
        if ch == '\\' {
            escape = true;
            continue;
        }
        if in_quote {
            if ch == quote_char {
                in_quote = false;
                args.push(current.clone());
                current.clear();
            } else {
                current.push(ch);
            }
        } else if ch == '\'' || ch == '"' {
            in_quote = true;
            quote_char = ch;
        }
    }

    if args.len() < 3 {
        return None;
    }

    Some((args[0].clone(), args[1].clone(), args[2].clone()))
}

fn decrypt_session_key(enc_data_b64: &str, key: &str) -> Option<String> {
    let decoded = B64.decode(enc_data_b64).ok()?;
    let obj: serde_json::Value = serde_json::from_slice(&decoded).ok()?;

    let ciphertext_b64 = obj["ciphertext"].as_str()?;
    let iv_hex = obj["iv"].as_str()?;
    let salt_hex = obj["salt"].as_str()?;
    let iterations = obj["iterations"].as_u64().unwrap_or(999) as u32;

    let ciphertext = B64.decode(ciphertext_b64).ok()?;
    let iv = hex::decode(iv_hex).ok()?;
    let salt = hex::decode(salt_hex).ok()?;

    if iv.len() != 16 || ciphertext.len() % 16 != 0 {
        return None;
    }

    let mut derived_key = [0u8; 32];
    pbkdf2::<Hmac<Sha512>>(key.as_bytes(), &salt, iterations, &mut derived_key).ok()?;

    let iv_arr: [u8; 16] = iv.try_into().ok()?;
    let key_arr: [u8; 32] = derived_key;

    type Aes256CbcDec = cbc::Decryptor<Aes256>;
    let mut buf = ciphertext.to_vec();
    let plaintext = Aes256CbcDec::new(&key_arr.into(), &iv_arr.into())
        .decrypt_padded_mut::<Pkcs7>(&mut buf)
        .ok()?;

    std::str::from_utf8(plaintext).ok().map(|s| s.to_string())
}

fn extract_best_okru_url(json: &str) -> Option<String> {
    let data: serde_json::Value = serde_json::from_str(json).ok()?;
    let videos = data["videos"].as_array()?;

    // Pick the video object with the most quality levels (likely the main one)
    let video = videos.iter().max_by_key(|v| {
        v.as_object().map(|o| o.len()).unwrap_or(0)
    })?;

    for quality in &["url_fullhd", "url_quadhd", "url_high", "url_medium", "url_low", "url_mobile", "url_tiny"] {
        if let Some(url) = video[quality].as_str() {
            if !url.is_empty() {
                return Some(url.to_string());
            }
        }
    }
    None
}

// Fallback for videos that still use the old get_stream mechanism
async fn extract_and_resolve_stream(html: &str) -> Option<String> {
    let stream_url = extract_best_stream_url(html)?;
    resolve_cdn_url(&stream_url).await
}

fn extract_best_stream_url(html: &str) -> Option<String> {
    for key in &["video_alt_url2", "video_alt_url", "video_url"] {
        if let Some(url) = extract_flashvar(html, key) {
            return Some(url);
        }
    }
    None
}

fn extract_flashvar(html: &str, key: &str) -> Option<String> {
    let pattern = format!("{key}:");
    let pos = html.find(&pattern)?;
    let rest = &html[pos + pattern.len()..];
    let q1 = rest.find('\'')?;
    let value_start = &rest[q1 + 1..];
    let q2 = value_start.find('\'')?;
    let value = &value_start[..q2];
    if value.starts_with("https://") {
        Some(value.to_string())
    } else {
        None
    }
}

async fn resolve_cdn_url(stream_url: &str) -> Option<String> {
    let client = wreq::Client::builder()
        .redirect(wreq::redirect::Policy::none())
        .build()
        .ok()?;

    let resp = client
        .get(stream_url)
        .header("Referer", BASE_URL)
        .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())
}

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

    #[test]
    fn parses_generate_mp4_args() {
        let html = r#"generate_mp4('eyJABC', 'key123', 'vid1,vid2', '10010686');"#;
        let (data, key, rnd) = extract_generate_mp4_args(html).unwrap();
        assert_eq!(data, "eyJABC");
        assert_eq!(key, "key123");
        assert_eq!(rnd, "vid1,vid2");
    }

    #[test]
    fn extracts_best_okru_url_fullhd_first() {
        let json = r#"{"videos":[{"url_mobile":"http://mobile.example","url_fullhd":"http://fullhd.example","url_high":"http://high.example"}]}"#;
        let url = extract_best_okru_url(json).unwrap();
        assert_eq!(url, "http://fullhd.example");
    }

    #[test]
    fn extracts_1080p_first() {
        let html = r#"
            var flashvars = {
                video_url: 'https://www.thepornbunny.com/get_stream/1418-480.mp4?md5=abc&timestamp=123',
                video_alt_url: 'https://www.thepornbunny.com/get_stream/1418-720.mp4?md5=abc&timestamp=123',
                video_alt_url2: 'https://www.thepornbunny.com/get_stream/1418-1080.mp4?md5=abc&timestamp=123',
            };
        "#;
        let url = extract_best_stream_url(html).expect("should extract");
        assert!(url.contains("1080"), "expected 1080 url, got: {url}");
    }
}
