use crate::DbPool;
use crate::api::ClientVersion;
use crate::providers::{Provider, report_provider_error, requester_or_default};
use crate::status::*;
use crate::util::cache::VideoCache;
use crate::util::parse_abbreviated_number;
use crate::util::time::parse_time_to_seconds;
use crate::videos::{ServerOptions, VideoFormat, VideoItem};
use async_trait::async_trait;
use error_chain::error_chain;
use htmlentity::entity::{ICodedDataTrait, decode};
use scraper::{ElementRef, Html, Selector};
use serde_json::Value;
use std::process::Command;
use url::form_urlencoded::byte_serialize;

pub const CHANNEL_METADATA: crate::providers::ProviderChannelMetadata =
    crate::providers::ProviderChannelMetadata {
        group_id: "mainstream-tube",
        tags: &["mainstream", "mixed", "search"],
    };

error_chain! {
    foreign_links {
        Io(std::io::Error);
        HttpRequest(wreq::Error);
    }
}

#[derive(Debug, Clone)]
pub struct SpankbangProvider {
    url: String,
}

impl SpankbangProvider {
    pub fn new() -> Self {
        Self {
            url: "https://spankbang.com".to_string(),
        }
    }

    fn build_channel(&self, _clientversion: ClientVersion) -> Channel {
        Channel {
            id: "spankbang".to_string(),
            name: "SpankBang".to_string(),
            description: "Porn videos, trending searches, and featured scenes.".to_string(),
            premium: false,
            favicon: "https://www.google.com/s2/favicons?sz=64&domain=spankbang.com".to_string(),
            status: "active".to_string(),
            categories: vec![],
            options: vec![ChannelOption {
                id: "sort".to_string(),
                title: "Sort".to_string(),
                description: "Sort the videos".to_string(),
                systemImage: "list.number".to_string(),
                colorName: "blue".to_string(),
                options: vec![
                    FilterOption {
                        id: "trending".to_string(),
                        title: "Trending".to_string(),
                    },
                    FilterOption {
                        id: "upcoming".to_string(),
                        title: "Upcoming".to_string(),
                    },
                    FilterOption {
                        id: "new".to_string(),
                        title: "New".to_string(),
                    },
                    FilterOption {
                        id: "popular".to_string(),
                        title: "Popular".to_string(),
                    },
                    FilterOption {
                        id: "featured".to_string(),
                        title: "Featured".to_string(),
                    },
                ],
                multiSelect: false,
            }],
            nsfw: true,
            cacheDuration: Some(1800),
            ytdlpCommand: Some("yt-dlp --impersonate chrome-120".to_string())
        }
    }

    fn normalize_get_sort(sort: &str) -> &'static str {
        match sort {
            "upcoming" => "upcoming",
            "new" => "new",
            "popular" => "popular",
            _ => "trending",
        }
    }

    fn normalize_query_sort(sort: &str) -> &'static str {
        match sort {
            "new" => "new",
            "popular" => "popular",
            "featured" => "featured",
            _ => "trending",
        }
    }

    fn encode_search_query(query: &str) -> String {
        query
            .split_whitespace()
            .map(|part| byte_serialize(part.as_bytes()).collect::<String>())
            .collect::<Vec<_>>()
            .join("+")
    }

    fn build_get_url(&self, page: u32, sort: &str) -> String {
        match Self::normalize_get_sort(sort) {
            "upcoming" => {
                if page > 1 {
                    format!("{}/upcoming/{page}/", self.url)
                } else {
                    format!("{}/upcoming/", self.url)
                }
            }
            "new" => {
                if page > 1 {
                    format!("{}/new_videos/{page}/", self.url)
                } else {
                    format!("{}/new_videos/", self.url)
                }
            }
            "popular" => {
                if page > 1 {
                    format!("{}/most_popular/{page}/?p=w", self.url)
                } else {
                    format!("{}/most_popular/?p=w", self.url)
                }
            }
            _ => {
                if page > 1 {
                    format!("{}/trending_videos/{page}/", self.url)
                } else {
                    format!("{}/trending_videos/", self.url)
                }
            }
        }
    }

    fn request_headers(&self) -> Vec<(String, String)> {
        vec![
            (
                "accept".to_string(),
                "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8"
                    .to_string(),
            ),
            ("accept-language".to_string(), "en-US,en;q=0.6".to_string()),
            ("cache-control".to_string(), "no-cache".to_string()),
            ("pragma".to_string(), "no-cache".to_string()),
            ("priority".to_string(), "u=0, i".to_string()),
            (
                "sec-ch-ua".to_string(),
                r#""Chromium";v="146", "Not-A.Brand";v="24", "Brave";v="146""#.to_string(),
            ),
            ("sec-ch-ua-mobile".to_string(), "?0".to_string()),
            ("sec-ch-ua-platform".to_string(), "\"Linux\"".to_string()),
            ("sec-fetch-dest".to_string(), "document".to_string()),
            ("sec-fetch-mode".to_string(), "navigate".to_string()),
            ("sec-fetch-site".to_string(), "none".to_string()),
            ("sec-fetch-user".to_string(), "?1".to_string()),
            ("sec-gpc".to_string(), "1".to_string()),
            (
                "upgrade-insecure-requests".to_string(),
                "1".to_string(),
            ),
            (
                "user-agent".to_string(),
                "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
                    .to_string(),
            ),
            ("Referer".to_string(), format!("{}/", self.url)),
        ]
    }

    fn is_cloudflare_block(text: &str) -> bool {
        let lowercase = text.to_ascii_lowercase();
        lowercase.contains("attention required")
            || lowercase.contains("you have been blocked")
            || lowercase.contains("cloudflare ray id")
    }

    fn fetch_items_with_curl_cffi(&self, page_url: &str, proxy_base_url: &str) -> Vec<VideoItem> {
        crate::flow_debug!(
            "trace={} spankbang curl_cffi fetch start url={}",
            "none",
            crate::util::flow_debug::preview(page_url, 120)
        );
        let output = match Command::new("python3")
            .arg("-c")
            .arg(
                r#"from curl_cffi import requests
from bs4 import BeautifulSoup
import json
import sys

url = sys.argv[1]
r = requests.get(url, impersonate='chrome124', timeout=45, headers={'Referer': 'https://spankbang.com/'})
if r.status_code >= 400:
    raise SystemExit(2)

soup = BeautifulSoup(r.text, 'html.parser')
cards = soup.select('[data-testid="video-list"] [data-testid="video-item"]')
if not cards:
    cards = soup.select('[data-testid="video-item"]')

items = []
for card in cards:
    vid = (card.get('data-id') or '').strip()
    link = card.select_one('a[href*="/video/"]')
    if not vid or link is None:
        continue
    href = (link.get('href') or '').strip()
    if not href:
        continue
    img = card.select_one('picture img, img')
    title_anchor = card.select_one('p a[title], a[title]')
    duration = card.select_one('[data-testid="video-item-length"]')
    views = card.select_one('[data-testid="views"]')
    uploader = card.select_one('[data-testid="video-info-with-badge"] a[data-testid="title"]')
    preview = card.select_one('video source[data-src]')
    items.append({
        'id': vid,
        'href': href,
        'title': (title_anchor.get('title') if title_anchor else '') or (img.get('alt') if img else ''),
        'thumb': ((img.get('src') if img else '') or (img.get('data-src') if img else '') or '').strip(),
        'preview': (preview.get('data-src') if preview else '') or '',
        'duration': duration.get_text(' ', strip=True) if duration else '',
        'views': views.get_text(' ', strip=True) if views else '',
        'uploader': uploader.get_text(' ', strip=True) if uploader else '',
        'uploader_href': (uploader.get('href') if uploader else '') or '',
    })

sys.stdout.write(json.dumps(items))
"#,
            )
            .arg(page_url)
            .output()
        {
            Ok(output) if output.status.success() => output,
            Ok(output) => {
                let stderr = String::from_utf8_lossy(&output.stderr);
                crate::providers::report_provider_error_background(
                    "spankbang",
                    "curl_cffi.fetch.status",
                    &format!(
                        "url={page_url}; status={}; stderr={}",
                        output.status,
                        crate::util::flow_debug::preview(&stderr, 300)
                    ),
                );
                return vec![];
            }
            Err(e) => {
                crate::providers::report_provider_error_background(
                    "spankbang",
                    "curl_cffi.fetch.spawn",
                    &format!("url={page_url}; error={e}"),
                );
                return vec![];
            }
        };

        let payload = String::from_utf8(output.stdout).ok();
        if payload.as_deref().unwrap_or("").trim().is_empty() {
            crate::providers::report_provider_error_background(
                "spankbang",
                "curl_cffi.fetch.empty",
                &format!("url={page_url}"),
            );
            return vec![];
        }
        crate::flow_debug!(
            "trace={} spankbang curl_cffi fetch ok url={} bytes={}",
            "none",
            crate::util::flow_debug::preview(page_url, 120),
            payload.as_deref().unwrap_or("").len()
        );

        let items_json: Value = match serde_json::from_str(payload.as_deref().unwrap_or("")) {
            Ok(value) => value,
            Err(e) => {
                crate::providers::report_provider_error_background(
                    "spankbang",
                    "curl_cffi.parse.json",
                    &format!("url={page_url}; error={e}"),
                );
                return vec![];
            }
        };
        let Some(entries) = items_json.as_array() else {
            return vec![];
        };

        let mut items = Vec::new();
        for entry in entries {
            let id = entry
                .get("id")
                .and_then(|value| value.as_str())
                .unwrap_or("")
                .trim()
                .to_string();
            let href = entry
                .get("href")
                .and_then(|value| value.as_str())
                .unwrap_or("")
                .trim()
                .to_string();
            if id.is_empty() || href.is_empty() {
                continue;
            }
            let detail_url = self.normalize_url(&href);
            let title = entry
                .get("title")
                .and_then(|value| value.as_str())
                .map(Self::decode_html)
                .unwrap_or_default();
            if title.is_empty() {
                continue;
            }
            let thumb = entry
                .get("thumb")
                .and_then(|value| value.as_str())
                .map(|value| self.normalize_url(value))
                .unwrap_or_default();
            let preview = entry
                .get("preview")
                .and_then(|value| value.as_str())
                .map(|value| self.normalize_url(value))
                .unwrap_or_default();
            let duration = entry
                .get("duration")
                .and_then(|value| value.as_str())
                .map(Self::parse_duration)
                .unwrap_or(0);
            let views = entry
                .get("views")
                .and_then(|value| value.as_str())
                .and_then(parse_abbreviated_number);

            let mut item = VideoItem::new(
                id,
                title,
                self.proxy_url(proxy_base_url, &href),
                "spankbang".to_string(),
                thumb,
                duration,
            );
            if let Some(views) = views {
                item = item.views(views);
            }
            if !preview.is_empty() {
                let mut format = VideoFormat::new(
                    preview.clone(),
                    "preview".to_string(),
                    "video/mp4".to_string(),
                );
                format.add_http_header("Referer".to_string(), detail_url.clone());
                item = item.preview(preview).formats(vec![format]);
            }
            let uploader = entry
                .get("uploader")
                .and_then(|value| value.as_str())
                .map(Self::decode_html)
                .unwrap_or_default();
            if !uploader.is_empty() {
                item = item.uploader(uploader);
            }
            let uploader_href = entry
                .get("uploader_href")
                .and_then(|value| value.as_str())
                .unwrap_or("")
                .trim();
            if !uploader_href.is_empty() {
                let uploader_url = self.normalize_url(uploader_href);
                if !uploader_url.is_empty() {
                    item = item.uploader_url(uploader_url);
                }
            }
            items.push(item);
        }

        if items.is_empty() {
            crate::providers::report_provider_error_background(
                "spankbang",
                "curl_cffi.parse.empty",
                &format!("url={page_url}"),
            );
            return vec![];
        }
        crate::flow_debug!(
            "trace={} spankbang curl_cffi parsed url={} items={}",
            "none",
            crate::util::flow_debug::preview(page_url, 120),
            items.len()
        );

        items
    }

    fn build_query_url(&self, query: &str, page: u32, sort: &str) -> String {
        let encoded_query = Self::encode_search_query(query);
        let mut url = if page > 1 {
            format!("{}/s/{encoded_query}/{page}/", self.url)
        } else {
            format!("{}/s/{encoded_query}/", self.url)
        };

        match Self::normalize_query_sort(sort) {
            "new" => url.push_str("?o=new"),
            "popular" => url.push_str("?o=popular"),
            "featured" => url.push_str("?o=featured"),
            _ => {}
        }

        url
    }

    fn normalize_url(&self, url: &str) -> String {
        if url.is_empty() {
            return String::new();
        }
        if url.starts_with("http://") || url.starts_with("https://") {
            return url.to_string();
        }
        if url.starts_with("//") {
            return format!("https:{url}");
        }
        if url.starts_with('/') {
            return format!("{}{}", self.url, url);
        }
        format!("{}/{}", self.url, url.trim_start_matches("./"))
    }

    fn proxy_url(&self, proxy_base_url: &str, url: &str) -> String {
        let path = url
            .strip_prefix(&self.url)
            .unwrap_or(url)
            .trim_start_matches('/');
        if proxy_base_url.is_empty() {
            return format!("/proxy/spankbang/{path}");
        }
        format!(
            "{}/proxy/spankbang/{path}",
            proxy_base_url.trim_end_matches('/')
        )
    }

    fn decode_html(text: &str) -> String {
        decode(text.as_bytes())
            .to_string()
            .unwrap_or_else(|_| text.to_string())
    }

    fn collapse_whitespace(text: &str) -> String {
        text.split_whitespace().collect::<Vec<_>>().join(" ")
    }

    fn text_of(element: &ElementRef<'_>) -> String {
        Self::collapse_whitespace(&element.text().collect::<Vec<_>>().join(" "))
    }

    fn parse_duration(text: &str) -> u32 {
        let raw = Self::collapse_whitespace(text);
        if raw.is_empty() {
            return 0;
        }

        if raw.contains(':') {
            return parse_time_to_seconds(&raw)
                .and_then(|seconds| u32::try_from(seconds).ok())
                .unwrap_or(0);
        }

        let mut total = 0;
        let mut digits = String::new();

        for ch in raw.chars() {
            if ch.is_ascii_digit() {
                digits.push(ch);
                continue;
            }

            if digits.is_empty() {
                continue;
            }

            let value = digits.parse::<u32>().unwrap_or(0);
            match ch.to_ascii_lowercase() {
                'h' => total += value * 3600,
                'm' => total += value * 60,
                's' => total += value,
                _ => {}
            }
            digits.clear();
        }

        if total == 0 && !digits.is_empty() {
            digits.parse::<u32>().unwrap_or(0)
        } else {
            total
        }
    }

    fn parse_rating(text: &str) -> Option<f32> {
        let cleaned = Self::collapse_whitespace(text)
            .trim_end_matches('%')
            .trim()
            .to_string();
        if cleaned.is_empty() || cleaned == "-" {
            return None;
        }
        cleaned.parse::<f32>().ok()
    }

    fn parse_card(
        &self,
        card: ElementRef<'_>,
        video_link_selector: &Selector,
        title_selector: &Selector,
        thumb_selector: &Selector,
        preview_selector: &Selector,
        length_selector: &Selector,
        views_selector: &Selector,
        rating_selector: &Selector,
        meta_link_selector: &Selector,
        proxy_base_url: &str,
    ) -> Option<VideoItem> {
        let card_html = card.html();
        let card_text = Self::collapse_whitespace(&card.text().collect::<Vec<_>>().join(" "));
        if card_html.contains("SpankBang Gold") || card_text.contains("SpankBang Gold") {
            return None;
        }

        let id = card.value().attr("data-id")?.to_string();
        let href = card
            .select(video_link_selector)
            .find_map(|link| link.value().attr("href"))
            .map(ToString::to_string)?;
        let detail_url = self.normalize_url(&href);
        let thumb = card
            .select(thumb_selector)
            .find_map(|img| img.value().attr("src"))
            .map(|src| self.normalize_url(src))
            .unwrap_or_default();
        let preview = card
            .select(preview_selector)
            .find_map(|source| source.value().attr("data-src"))
            .map(|src| self.normalize_url(src));
        let duration = card
            .select(length_selector)
            .next()
            .map(|element| Self::parse_duration(&Self::text_of(&element)))
            .unwrap_or(0);
        let views = card
            .select(views_selector)
            .next()
            .and_then(|element| parse_abbreviated_number(&Self::text_of(&element)));
        let rating = card
            .select(rating_selector)
            .next()
            .and_then(|element| Self::parse_rating(&Self::text_of(&element)));
        let title = card
            .select(title_selector)
            .next()
            .and_then(|link| link.value().attr("title"))
            .map(Self::decode_html)
            .unwrap_or_else(|| {
                card.select(thumb_selector)
                    .next()
                    .and_then(|img| img.value().attr("alt"))
                    .map(Self::decode_html)
                    .unwrap_or_default()
            });

        if title.is_empty() {
            return None;
        }

        let mut item = VideoItem::new(
            id,
            title,
            self.proxy_url(proxy_base_url, &href),
            "spankbang".to_string(),
            thumb,
            duration,
        );

        if let Some(views) = views {
            item = item.views(views);
        }
        if let Some(rating) = rating {
            item = item.rating(rating);
        }
        if let Some(preview) = preview {
            let mut format = VideoFormat::new(
                preview.clone(),
                "preview".to_string(),
                "video/mp4".to_string(),
            );
            format.add_http_header("Referer".to_string(), detail_url.clone());
            item = item.preview(preview).formats(vec![format]);
        }

        if let Some(meta_link) = card.select(meta_link_selector).next() {
            let uploader = Self::decode_html(&Self::text_of(&meta_link));
            if !uploader.is_empty() {
                item = item.uploader(uploader);
            }
            if let Some(meta_href) = meta_link.value().attr("href") {
                let uploader_url = self.normalize_url(meta_href);
                if !uploader_url.is_empty() {
                    item = item.uploader_url(uploader_url);
                }
            }
        }

        Some(item)
    }

    fn get_video_items_from_html(&self, html: String, proxy_base_url: &str) -> Vec<VideoItem> {
        let document = Html::parse_document(&html);
        let video_list_selector = Selector::parse(r#"[data-testid="video-list"]"#).unwrap();
        let card_selector = Selector::parse(r#"[data-testid="video-item"]"#).unwrap();
        let video_link_selector = Selector::parse(r#"a[href*="/video/"]"#).unwrap();
        let title_selector = Selector::parse(r#"a[title]"#).unwrap();
        let thumb_selector = Selector::parse("picture img, img").unwrap();
        let preview_selector = Selector::parse(r#"source[data-src]"#).unwrap();
        let length_selector = Selector::parse(r#"[data-testid="video-item-length"]"#).unwrap();
        let views_selector = Selector::parse(r#"[data-testid="views"]"#).unwrap();
        let rating_selector = Selector::parse(r#"[data-testid="rates"]"#).unwrap();
        let meta_link_selector =
            Selector::parse(r#"[data-testid="video-info-with-badge"] a[data-testid="title"]"#)
                .unwrap();

        let mut items = Vec::new();
        let roots = document.select(&video_list_selector).collect::<Vec<_>>();
        let cards = if let Some(root) = roots.last() {
            root.select(&card_selector).collect::<Vec<_>>()
        } else {
            document.select(&card_selector).collect::<Vec<_>>()
        };

        for card in cards {
            if let Some(item) = self.parse_card(
                card,
                &video_link_selector,
                &title_selector,
                &thumb_selector,
                &preview_selector,
                &length_selector,
                &views_selector,
                &rating_selector,
                &meta_link_selector,
                proxy_base_url,
            ) {
                items.push(item);
            }
        }

        items
    }

    async fn get(
        &self,
        cache: VideoCache,
        page: u32,
        sort: &str,
        options: ServerOptions,
    ) -> Result<Vec<VideoItem>> {
        let video_url = self.build_get_url(page, sort);
        let old_items = match cache.get(&video_url) {
            Some((time, items)) => {
                if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
                    return Ok(items.clone());
                }
                items.clone()
            }
            None => vec![],
        };
        let mut requester =
            requester_or_default(&options, "spankbang", "spankbang.get.missing_requester");
        let text = match requester
            .get_with_headers(&video_url, self.request_headers(), None)
            .await
        {
            Ok(text) => text,
            Err(e) => {
                report_provider_error(
                    "spankbang",
                    "get.request",
                    &format!("url={video_url}; error={e}"),
                )
                .await;
                let proxy_base_url = options.public_url_base.as_deref().unwrap_or_default();
                let curl_cffi_items = self.fetch_items_with_curl_cffi(&video_url, proxy_base_url);
                if !curl_cffi_items.is_empty() {
                    cache.remove(&video_url);
                    cache.insert(video_url.clone(), curl_cffi_items.clone());
                    return Ok(curl_cffi_items);
                }
                return Ok(old_items);
            }
        };

        if text.trim().is_empty() {
            report_provider_error(
                "spankbang",
                "get.empty_response",
                &format!("url={video_url}"),
            )
            .await;
            let proxy_base_url = options.public_url_base.as_deref().unwrap_or_default();
            let curl_cffi_items = self.fetch_items_with_curl_cffi(&video_url, proxy_base_url);
            if !curl_cffi_items.is_empty() {
                cache.remove(&video_url);
                cache.insert(video_url.clone(), curl_cffi_items.clone());
                return Ok(curl_cffi_items);
            }
            return Ok(old_items);
        }

        if Self::is_cloudflare_block(&text) {
            report_provider_error(
                "spankbang",
                "get.cloudflare_block",
                &format!("url={video_url}"),
            )
            .await;
            let proxy_base_url = options.public_url_base.as_deref().unwrap_or_default();
            let curl_cffi_items = self.fetch_items_with_curl_cffi(&video_url, proxy_base_url);
            if !curl_cffi_items.is_empty() {
                cache.remove(&video_url);
                cache.insert(video_url.clone(), curl_cffi_items.clone());
                return Ok(curl_cffi_items);
            }
            return Ok(old_items);
        }

        let looks_like_html = text.to_ascii_lowercase().contains("<html");
        let proxy_base_url = options.public_url_base.as_deref().unwrap_or_default();
        let video_items = self.get_video_items_from_html(text, proxy_base_url);
        if !video_items.is_empty() {
            cache.remove(&video_url);
            cache.insert(video_url.clone(), video_items.clone());
            return Ok(video_items);
        }

        report_provider_error(
            "spankbang",
            "get.parse_empty",
            &format!("url={video_url}; looks_like_html={looks_like_html}"),
        )
        .await;
        let curl_cffi_items = self.fetch_items_with_curl_cffi(&video_url, proxy_base_url);
        if !curl_cffi_items.is_empty() {
            cache.remove(&video_url);
            cache.insert(video_url.clone(), curl_cffi_items.clone());
            return Ok(curl_cffi_items);
        }
        Ok(old_items)
    }

    async fn query(
        &self,
        cache: VideoCache,
        page: u32,
        query: &str,
        sort: &str,
        options: ServerOptions,
    ) -> Result<Vec<VideoItem>> {
        let video_url = self.build_query_url(query, page, sort);
        let old_items = match cache.get(&video_url) {
            Some((time, items)) => {
                if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
                    return Ok(items.clone());
                }
                items.clone()
            }
            None => vec![],
        };
        let mut requester =
            requester_or_default(&options, "spankbang", "spankbang.query.missing_requester");
        let text = match requester
            .get_with_headers(&video_url, self.request_headers(), None)
            .await
        {
            Ok(text) => text,
            Err(e) => {
                report_provider_error(
                    "spankbang",
                    "query.request",
                    &format!("url={video_url}; error={e}"),
                )
                .await;
                let proxy_base_url = options.public_url_base.as_deref().unwrap_or_default();
                let curl_cffi_items = self.fetch_items_with_curl_cffi(&video_url, proxy_base_url);
                if !curl_cffi_items.is_empty() {
                    cache.remove(&video_url);
                    cache.insert(video_url.clone(), curl_cffi_items.clone());
                    return Ok(curl_cffi_items);
                }
                return Ok(old_items);
            }
        };

        if text.trim().is_empty() {
            report_provider_error(
                "spankbang",
                "query.empty_response",
                &format!("url={video_url}"),
            )
            .await;
            let proxy_base_url = options.public_url_base.as_deref().unwrap_or_default();
            let curl_cffi_items = self.fetch_items_with_curl_cffi(&video_url, proxy_base_url);
            if !curl_cffi_items.is_empty() {
                cache.remove(&video_url);
                cache.insert(video_url.clone(), curl_cffi_items.clone());
                return Ok(curl_cffi_items);
            }
            return Ok(old_items);
        }

        if Self::is_cloudflare_block(&text) {
            report_provider_error(
                "spankbang",
                "query.cloudflare_block",
                &format!("url={video_url}"),
            )
            .await;
            let proxy_base_url = options.public_url_base.as_deref().unwrap_or_default();
            let curl_cffi_items = self.fetch_items_with_curl_cffi(&video_url, proxy_base_url);
            if !curl_cffi_items.is_empty() {
                cache.remove(&video_url);
                cache.insert(video_url.clone(), curl_cffi_items.clone());
                return Ok(curl_cffi_items);
            }
            return Ok(old_items);
        }

        let looks_like_html = text.to_ascii_lowercase().contains("<html");
        let proxy_base_url = options.public_url_base.as_deref().unwrap_or_default();
        let video_items = self.get_video_items_from_html(text, proxy_base_url);
        if !video_items.is_empty() {
            cache.remove(&video_url);
            cache.insert(video_url.clone(), video_items.clone());
            return Ok(video_items);
        }

        report_provider_error(
            "spankbang",
            "query.parse_empty",
            &format!("url={video_url}; looks_like_html={looks_like_html}"),
        )
        .await;
        let curl_cffi_items = self.fetch_items_with_curl_cffi(&video_url, proxy_base_url);
        if !curl_cffi_items.is_empty() {
            cache.remove(&video_url);
            cache.insert(video_url.clone(), curl_cffi_items.clone());
            return Ok(curl_cffi_items);
        }
        Ok(old_items)
    }
}

#[async_trait]
impl Provider for SpankbangProvider {
    async fn get_videos(
        &self,
        cache: VideoCache,
        pool: DbPool,
        sort: String,
        query: Option<String>,
        page: String,
        per_page: String,
        options: ServerOptions,
    ) -> Vec<VideoItem> {
        let _ = pool;
        let _ = per_page;
        let page = page.parse::<u32>().unwrap_or(1);

        let videos = match query {
            Some(query) if !query.trim().is_empty() => {
                self.query(cache, page, &query, &sort, options).await
            }
            _ => self.get(cache, page, &sort, options).await,
        };

        match videos {
            Ok(videos) => videos,
            Err(e) => {
                report_provider_error(
                    "spankbang",
                    "get_videos",
                    &format!("page={page}; error={e}"),
                )
                .await;
                vec![]
            }
        }
    }

    fn get_channel(&self, clientversion: ClientVersion) -> Option<Channel> {
        Some(self.build_channel(clientversion))
    }
}

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

    #[test]
    fn builds_top_level_urls() {
        let provider = SpankbangProvider::new();
        assert_eq!(
            provider.build_get_url(1, "trending"),
            "https://spankbang.com/trending_videos/"
        );
        assert_eq!(
            provider.build_get_url(2, "upcoming"),
            "https://spankbang.com/upcoming/2/"
        );
        assert_eq!(
            provider.build_get_url(2, "new"),
            "https://spankbang.com/new_videos/2/"
        );
        assert_eq!(
            provider.build_get_url(2, "popular"),
            "https://spankbang.com/most_popular/2/?p=w"
        );
        assert_eq!(
            provider.build_get_url(1, "featured"),
            "https://spankbang.com/trending_videos/"
        );
    }

    #[test]
    fn builds_search_urls_with_exact_sort_shape() {
        let provider = SpankbangProvider::new();
        assert_eq!(
            provider.build_query_url("adriana chechik", 1, "trending"),
            "https://spankbang.com/s/adriana+chechik/"
        );
        assert_eq!(
            provider.build_query_url("adriana chechik", 2, "new"),
            "https://spankbang.com/s/adriana+chechik/2/?o=new"
        );
        assert_eq!(
            provider.build_query_url("adriana chechik", 2, "popular"),
            "https://spankbang.com/s/adriana+chechik/2/?o=popular"
        );
        assert_eq!(
            provider.build_query_url("adriana chechik", 2, "featured"),
            "https://spankbang.com/s/adriana+chechik/2/?o=featured"
        );
        assert_eq!(
            provider.build_query_url("無修正", 1, "trending"),
            "https://spankbang.com/s/%E7%84%A1%E4%BF%AE%E6%AD%A3/"
        );
        assert_eq!(
            provider.request_headers(),
            vec![
                (
                    "accept".to_string(),
                    "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8"
                        .to_string(),
                ),
                ("accept-language".to_string(), "en-US,en;q=0.6".to_string()),
                ("cache-control".to_string(), "no-cache".to_string()),
                ("pragma".to_string(), "no-cache".to_string()),
                ("priority".to_string(), "u=0, i".to_string()),
                (
                    "sec-ch-ua".to_string(),
                    r#""Chromium";v="146", "Not-A.Brand";v="24", "Brave";v="146""#.to_string(),
                ),
                ("sec-ch-ua-mobile".to_string(), "?0".to_string()),
                ("sec-ch-ua-platform".to_string(), "\"Linux\"".to_string()),
                ("sec-fetch-dest".to_string(), "document".to_string()),
                ("sec-fetch-mode".to_string(), "navigate".to_string()),
                ("sec-fetch-site".to_string(), "none".to_string()),
                ("sec-fetch-user".to_string(), "?1".to_string()),
                ("sec-gpc".to_string(), "1".to_string()),
                (
                    "upgrade-insecure-requests".to_string(),
                    "1".to_string(),
                ),
                (
                    "user-agent".to_string(),
                    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
                        .to_string(),
                ),
                ("Referer".to_string(), "https://spankbang.com/".to_string()),
            ]
        );
    }

    #[test]
    fn parses_cards_and_rewrites_to_proxy_url() {
        let provider = SpankbangProvider::new();
        let html = r#"
            <div data-testid="video-item" data-id="6597754" class="js-video-item z-0 flex flex-col">
                <a href="/3xeuy/video/adriana+s+fleshlight+insertion" class="relative mb-1 overflow-hidden rounded bg-neutral-900">
                    <picture>
                        <img
                            src="https://tbi.sb-cd.com/t/6597754/6/5/w:300/t6-enh/adriana-s-fleshlight-insertion.jpg"
                            alt="Adriana&#39;s Fleshlight Insertion"
                        />
                    </picture>
                    <video>
                        <source data-src="https://tbv.sb-cd.com/t/6597754/6/5/td.mp4" type="video/mp4" />
                    </video>
                    <div data-testid="video-item-length">17m</div>
                </a>
                <div data-testid="video-info-with-badge">
                    <div class="flex justify-between">
                        <a data-testid="title" href="/76/pornstar/adriana+chechik/">
                            <span>Adriana Chechik</span>
                        </a>
                        <span data-testid="views"><span></span><span>35K</span></span>
                        <span data-testid="rates"><span></span><span>96%</span></span>
                    </div>
                    <p>
                        <a href="/3xeuy/video/adriana+s+fleshlight+insertion" title="Adriana&#39;s Fleshlight Insertion">
                            <span>Adriana&#39;s Fleshlight Insertion</span>
                        </a>
                    </p>
                </div>
            </div>
        "#;

        let items = provider.get_video_items_from_html(html.to_string(), "https://example.com");
        assert_eq!(items.len(), 1);
        assert_eq!(items[0].id, "6597754");
        assert_eq!(items[0].title, "Adriana's Fleshlight Insertion");
        assert_eq!(
            items[0].url,
            "https://example.com/proxy/spankbang/3xeuy/video/adriana+s+fleshlight+insertion"
        );
        assert_eq!(
            items[0].thumb,
            "https://tbi.sb-cd.com/t/6597754/6/5/w:300/t6-enh/adriana-s-fleshlight-insertion.jpg"
        );
        assert_eq!(
            items[0].preview,
            Some("https://tbv.sb-cd.com/t/6597754/6/5/td.mp4".to_string())
        );
        assert_eq!(items[0].duration, 1020);
        assert_eq!(items[0].views, Some(35_000));
        assert_eq!(items[0].rating, Some(96.0));
        assert_eq!(items[0].uploader, Some("Adriana Chechik".to_string()));
        assert_eq!(
            items[0].uploaderUrl,
            Some("https://spankbang.com/76/pornstar/adriana+chechik/".to_string())
        );
    }

    #[test]
    fn skips_spankbang_gold_cards() {
        let provider = SpankbangProvider::new();
        let html = r#"
            <div data-testid="video-item" data-id="1">
                <a href="/gold/video/locked">
                    <picture>
                        <img src="https://example.com/gold.jpg" alt="Gold video" />
                    </picture>
                    <div>SpankBang Gold</div>
                    <div data-testid="video-item-length">10m</div>
                </a>
                <div data-testid="video-info-with-badge">
                    <span data-testid="views"><span>1K</span></span>
                    <p><a href="/gold/video/locked" title="Gold video"><span>Gold video</span></a></p>
                </div>
            </div>
            <div data-testid="video-item" data-id="2">
                <a href="/free/video/open">
                    <picture>
                        <img src="https://example.com/free.jpg" alt="Free video" />
                    </picture>
                    <div data-testid="video-item-length">5m</div>
                </a>
                <div data-testid="video-info-with-badge">
                    <span data-testid="views"><span>2K</span></span>
                    <p><a href="/free/video/open" title="Free video"><span>Free video</span></a></p>
                </div>
            </div>
        "#;

        let items = provider.get_video_items_from_html(html.to_string(), "https://example.com");
        assert_eq!(items.len(), 1);
        assert_eq!(items[0].id, "2");
        assert_eq!(items[0].title, "Free video");
    }

    #[test]
    fn prefers_primary_video_list_over_header_dropdown_cards() {
        let provider = SpankbangProvider::new();
        let html = r#"
            <div data-testid="video-list">
                <div data-testid="video-item" data-id="111">
                    <a href="/wrong/video/header-card">
                        <picture><img src="https://example.com/wrong.jpg" alt="Wrong header card" /></picture>
                        <div data-testid="video-item-length">5m</div>
                    </a>
                    <div data-testid="video-info-with-badge">
                        <span data-testid="views"><span>1K</span></span>
                        <p><a href="/wrong/video/header-card" title="Wrong header card"><span>Wrong header card</span></a></p>
                    </div>
                </div>
            </div>
            <div data-testid="video-list">
                <div data-testid="video-item" data-id="222">
                    <a href="/right/video/adriana+chechik">
                        <picture><img src="https://example.com/right.jpg" alt="Right result" /></picture>
                        <div data-testid="video-item-length">17m</div>
                    </a>
                    <div data-testid="video-info-with-badge">
                        <span data-testid="views"><span>35K</span></span>
                        <span data-testid="rates"><span>96%</span></span>
                        <p><a href="/right/video/adriana+chechik" title="Right result"><span>Right result</span></a></p>
                    </div>
                </div>
            </div>
        "#;

        let items = provider.get_video_items_from_html(html.to_string(), "https://example.com");
        assert_eq!(items.len(), 1);
        assert_eq!(items[0].id, "222");
        assert_eq!(items[0].title, "Right result");
    }
}
