use base64::{Engine as _, engine::general_purpose::STANDARD};
use ntex::web::{self, HttpRequest};
use regex::Regex;
use serde::Deserialize;
use wreq::redirect::Policy;
use wreq_util::Emulation;

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

const API_BASE: &str = "https://manysex.com";
const BROWSER_UA: &str =
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";

#[derive(Debug, Deserialize)]
struct VideofileEntry {
    video_url: String,
    #[serde(default)]
    is_default: u8,
}

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

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

    /// Accepts the manysex.com/videomanysex.com detail page URL (host+path
    /// reconstructed from the `/proxy/melonstube/{endpoint}*` route) and
    /// validates the host against the known hard-site allowlist. Returns
    /// `None` rather than guessing for anything else.
    fn normalize_detail_url(endpoint: &str) -> Option<String> {
        let value = endpoint.trim().trim_start_matches('/');
        if value.is_empty() {
            return None;
        }
        let detail_url = if value.starts_with("http://") || value.starts_with("https://") {
            value.to_string()
        } else {
            format!("https://{value}")
        };
        let detail_url = detail_url.replacen("http://", "https://", 1);
        let parsed = url::Url::parse(&detail_url).ok()?;
        let host = parsed.host_str()?;
        if !(host == "manysex.com" || host == "videomanysex.com") {
            return None;
        }
        Some(detail_url)
    }

    fn extract_video_id(detail_url: &str) -> Option<String> {
        let regex = Regex::new(r"/video/(\d+)/").ok()?;
        Some(regex.captures(detail_url)?.get(1)?.as_str().to_string())
    }

    // manysex.com obfuscates the `get_file` path/query with a handful of
    // Cyrillic homoglyphs standing in for visually identical Latin letters,
    // before base64-encoding. Ported verbatim from vjav.rs's decode chain.
    fn decode_obfuscated_base64(value: &str) -> String {
        value
            .chars()
            .map(|character| match character {
                'А' => 'A',
                'В' => 'B',
                'Е' => 'E',
                'К' => 'K',
                'М' => 'M',
                'Н' => 'H',
                'О' => 'O',
                'Р' => 'P',
                'С' => 'C',
                'Т' => 'T',
                'Х' => 'X',
                'а' => 'a',
                'е' => 'e',
                'о' => 'o',
                'р' => 'p',
                'с' => 'c',
                'у' => 'y',
                'х' => 'x',
                'к' => 'k',
                'м' => 'm',
                'і' => 'i',
                'І' => 'I',
                _ => character,
            })
            .collect()
    }

    fn decode_base64ish(value: &str) -> Option<String> {
        let mut normalized = value.trim().replace('~', "=");
        while normalized.len() % 4 != 0 {
            normalized.push('=');
        }
        String::from_utf8(STANDARD.decode(normalized).ok()?).ok()
    }

    /// Decodes the obfuscated `video_url` field from `videofile.php` into the
    /// `get_file` path and query. Unlike vjav (whose decoded path is directly
    /// playable as an `.m3u8`), manysex.com's decoded path only resolves to a
    /// playable CDN URL after two additional redirect hops (see
    /// `resolve_stream_url`).
    fn decode_get_file_path(value: &str) -> Option<(String, String)> {
        let normalized = Self::decode_obfuscated_base64(value);
        let mut parts = normalized.splitn(2, ',');
        let path_part = parts.next()?;
        let query_part = parts.next()?;
        let path = Self::decode_base64ish(path_part)?;
        let query = Self::decode_base64ish(query_part)?;
        Some((path, query))
    }

    fn redirect_client() -> Option<wreq::Client> {
        wreq::Client::builder()
            .cert_verification(false)
            .emulation(Emulation::Chrome120)
            .redirect(Policy::default())
            .build()
            .ok()
    }

    fn no_redirect_client() -> Option<wreq::Client> {
        wreq::Client::builder()
            .cert_verification(false)
            .emulation(Emulation::Chrome120)
            .redirect(Policy::none())
            .build()
            .ok()
    }

    /// Resolves a manysex.com/videomanysex.com detail page URL to a playable
    /// CDN URL. Each hop is made manually with a per-hop Referer: the CDN's
    /// signed URL embeds a literal `referer=none,.manysex.com,.gstatic.com`
    /// allow-list, so blindly forwarding the same Referer to every hop (as
    /// automatic-redirect clients do) gets rejected with 403 at hop 2.
    /// Before returning, a ranged GET on the final URL verifies it is
    /// actually a video stream (not an ad/error page masquerading as one).
    async fn resolve_stream_url(detail_url: &str) -> Option<String> {
        let video_id = Self::extract_video_id(detail_url)?;

        let api_client = Self::redirect_client()?;
        let api_url = format!("{API_BASE}/api/videofile.php?video_id={video_id}&lifetime=8640000");
        let response = api_client
            .get(&api_url)
            .header("user-agent", BROWSER_UA)
            .header("referer", detail_url)
            .send()
            .await
            .ok()?;
        if !response.status().is_success() {
            return None;
        }
        let entries: Vec<VideofileEntry> = response.json().await.ok()?;
        let entry = entries
            .iter()
            .find(|e| e.is_default == 1)
            .or_else(|| entries.first())?;
        if entry.video_url.trim().is_empty() {
            return None;
        }

        let (path, query) = Self::decode_get_file_path(&entry.video_url)?;
        let separator = if path.contains('?') { "&" } else { "?" };
        let get_file_url = format!("{API_BASE}{path}{separator}{query}");

        let hop_client = Self::no_redirect_client()?;

        // Hop 1: the videomanysex.com/manysex.com page Referer is required here.
        let hop1 = hop_client
            .get(&get_file_url)
            .header("user-agent", BROWSER_UA)
            .header("referer", detail_url)
            .send()
            .await
            .ok()?;
        if !hop1.status().is_redirection() {
            return None;
        }
        let hop2_url = hop1.headers().get("location")?.to_str().ok()?.to_string();

        // Hop 2: the CDN allow-list rejects the videomanysex.com Referer used
        // above, so this hop must be sent with no Referer at all.
        let hop2 = hop_client
            .get(&hop2_url)
            .header("user-agent", BROWSER_UA)
            .send()
            .await
            .ok()?;
        if !hop2.status().is_redirection() {
            return None;
        }
        let final_url = hop2.headers().get("location")?.to_str().ok()?.to_string();

        // Anti-false-positive check: confirm the resolved URL actually
        // references the requested video id, and that a small ranged fetch
        // returns real video content rather than an ad/error page.
        if !final_url.contains(&video_id) {
            return None;
        }
        let verify = hop_client
            .get(&final_url)
            .header("user-agent", BROWSER_UA)
            .header("range", "bytes=0-65535")
            .send()
            .await
            .ok()?;
        if !(verify.status().as_u16() == 200 || verify.status().as_u16() == 206) {
            return None;
        }
        let content_type = verify
            .headers()
            .get("content-type")
            .and_then(|v| v.to_str().ok())
            .unwrap_or_default()
            .to_string();
        if !content_type.starts_with("video/") && !content_type.contains("octet-stream") {
            return None;
        }

        Some(final_url)
    }
}

/// Route handler for `/proxy/melonstube/{endpoint}*`.
///
/// Resolves a manysex.com/videomanysex.com detail page URL (melonstube's
/// out-link destination) to a playable CDN URL and returns a 302 redirect.
pub async fn serve(
    req: HttpRequest,
    _requester: web::types::State<Requester>,
) -> Result<impl web::Responder, web::Error> {
    let endpoint = req.match_info().query("endpoint").to_string();
    let query_string = req.query_string();
    let raw = if query_string.is_empty() {
        endpoint
    } else {
        format!("{endpoint}?{query_string}")
    };

    let Some(detail_url) = MelonstubeProxy::normalize_detail_url(&raw) else {
        return Ok(web::HttpResponse::BadRequest().finish());
    };

    match MelonstubeProxy::resolve_stream_url(&detail_url).await {
        Some(final_url) => Ok(web::HttpResponse::Found()
            .header("Location", final_url)
            .finish()),
        None => Ok(web::HttpResponse::BadGateway().finish()),
    }
}
