use crate::DbPool;
use crate::api::ClientVersion;
use crate::providers::Provider;
use crate::status::*;
use crate::util::cache::VideoCache;
use crate::util::discord::{format_error_chain, send_discord_error_report};
use crate::util::requester::Requester;
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 futures::future::join_all;
use htmlentity::entity::{ICodedDataTrait, decode};
use std::sync::{Arc, RwLock};
use std::{thread, vec};
use titlecase::Titlecase;
use url::Url;
use wreq::Version;

pub const CHANNEL_METADATA: crate::providers::ProviderChannelMetadata =
    crate::providers::ProviderChannelMetadata {
        group_id: "creator-leaks",
        tags: &["creator", "onlyfans", "amateur"],
    };

error_chain! {
    foreign_links {
        Io(std::io::Error);
        HttpRequest(wreq::Error);
        Json(serde_json::Error);
    }
    errors {
        Parse(msg: String) {
            description("parse error")
            display("parse error: {}", msg)
        }
    }
}

#[derive(Debug, Clone)]
pub struct PimpbunnyProvider {
    url: String,
    stars: Arc<RwLock<Vec<FilterOption>>>,
    categories: Arc<RwLock<Vec<FilterOption>>>,
}

impl PimpbunnyProvider {
    const FIREFOX_USER_AGENT: &'static str =
        "Mozilla/5.0 (X11; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0";
    const HTML_ACCEPT: &'static str =
        "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8";

    pub fn new() -> Self {
        let provider = Self {
            url: "https://pimpbunny.com".to_string(),
            stars: Arc::new(RwLock::new(vec![])),
            categories: Arc::new(RwLock::new(vec![])),
        };
        provider.spawn_initial_load();
        provider
    }

    fn build_channel(&self, clientversion: ClientVersion) -> Channel {
        let _ = clientversion;
        Channel {
            id: "pimpbunny".to_string(),
            name: "Pimpbunny".to_string(),
            description: "Watch Porn!".to_string(),
            premium: false,
            favicon: "https://www.google.com/s2/favicons?sz=64&domain=pimpbunny.com".to_string(),
            status: "active".to_string(),
            categories: self
                .categories
                .read()
                .map(|categories| categories.iter().map(|c| c.title.clone()).collect())
                .unwrap_or_else(|e| {
                    crate::providers::report_provider_error_background(
                        "pimpbunny",
                        "build_channel.categories_read",
                        &e.to_string(),
                    );
                    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: "featured".into(),
                        title: "Featured".into(),
                    },
                    FilterOption {
                        id: "most recent".into(),
                        title: "Most Recent".into(),
                    },
                    FilterOption {
                        id: "most viewed".into(),
                        title: "Most Viewed".into(),
                    },
                    FilterOption {
                        id: "best rated".into(),
                        title: "Best Rated".into(),
                    },
                ],
                multiSelect: false,
            }],
            nsfw: true,
            cacheDuration: None,
        }
    }

    fn spawn_initial_load(&self) {
        let url = self.url.clone();
        let stars = Arc::clone(&self.stars);
        let categories = Arc::clone(&self.categories);

        thread::spawn(async move || {
            let rt = match tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
            {
                Ok(rt) => rt,
                Err(e) => {
                    eprintln!("tokio runtime failed: {e}");
                    send_discord_error_report(
                        e.to_string(),
                        Some(format_error_chain(&e)),
                        Some("Pimpbunny Provider"),
                        Some("Failed to create tokio runtime"),
                        file!(),
                        line!(),
                        module_path!(),
                    )
                    .await;
                    return;
                }
            };

            rt.block_on(async {
                if let Err(e) = Self::load_stars(&url, Arc::clone(&stars)).await {
                    eprintln!("load_stars failed: {e}");
                    send_discord_error_report(
                        e.to_string(),
                        Some(format_error_chain(&e)),
                        Some("Pimpbunny Provider"),
                        Some("Failed to load stars during initial load"),
                        file!(),
                        line!(),
                        module_path!(),
                    )
                    .await;
                }
                if let Err(e) = Self::load_categories(&url, Arc::clone(&categories)).await {
                    eprintln!("load_categories failed: {e}");
                    send_discord_error_report(
                        e.to_string(),
                        Some(format_error_chain(&e)),
                        Some("Pimpbunny Provider"),
                        Some("Failed to load categories during initial load"),
                        file!(),
                        line!(),
                        module_path!(),
                    )
                    .await;
                }
            });
        });
    }

    fn push_unique(target: &Arc<RwLock<Vec<FilterOption>>>, item: FilterOption) {
        if let Ok(mut vec) = target.write() {
            if !vec.iter().any(|x| x.id == item.id) {
                vec.push(item);
            }
        }
    }

    fn is_allowed_thumb_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;
        };
        matches!(host, "pimpbunny.com" | "www.pimpbunny.com")
            && url.path().starts_with("/contents/videos_screenshots/")
    }

    fn proxied_thumb(&self, options: &ServerOptions, thumb: &str) -> String {
        if thumb.is_empty() || !Self::is_allowed_thumb_url(thumb) {
            return thumb.to_string();
        }

        crate::providers::build_proxy_url(
            options,
            "pimpbunny-thumb",
            &crate::providers::strip_url_scheme(thumb),
        )
    }

    fn root_referer(&self) -> String {
        format!("{}/", self.url.trim_end_matches('/'))
    }

    fn root_headers(&self) -> Vec<(String, String)> {
        Self::html_headers_with_referer(&self.root_referer())
    }

    fn html_headers_with_referer(referer: &str) -> Vec<(String, String)> {
        vec![
            ("Referer".to_string(), referer.to_string()),
            (
                "User-Agent".to_string(),
                Self::FIREFOX_USER_AGENT.to_string(),
            ),
            ("Accept".to_string(), Self::HTML_ACCEPT.to_string()),
            ("Accept-Language".to_string(), "en-US,en;q=0.9".to_string()),
        ]
    }

    fn headers_with_cookies(
        &self,
        requester: &Requester,
        request_url: &str,
        referer: &str,
    ) -> Vec<(String, String)> {
        let mut headers = Self::html_headers_with_referer(referer);
        if let Some(cookie) = requester.cookie_header_for_url(request_url) {
            headers.push(("Cookie".to_string(), cookie));
        }
        headers
    }

    async fn warm_root_session(&self, requester: &mut Requester) {
        let root_url = self.root_referer();
        let _ = requester
            .get_with_headers(&root_url, self.root_headers(), Some(Version::HTTP_11))
            .await;
    }

    async fn warm_root_session_for_base(base: &str, requester: &mut Requester) {
        let root_url = format!("{}/", base.trim_end_matches('/'));
        let _ = requester
            .get_with_headers(
                &root_url,
                Self::html_headers_with_referer(&root_url),
                Some(Version::HTTP_11),
            )
            .await;
    }

    async fn load_stars(base: &str, stars: Arc<RwLock<Vec<FilterOption>>>) -> Result<()> {
        let mut requester = Requester::new();
        Self::warm_root_session_for_base(base, &mut requester).await;
        let request_url = format!("{base}/onlyfans-models/?models_per_page=20");
        let headers = {
            let root_url = format!("{}/", base.trim_end_matches('/'));
            let mut headers = Self::html_headers_with_referer(&root_url);
            if let Some(cookie) = requester.cookie_header_for_url(&request_url) {
                headers.push(("Cookie".to_string(), cookie));
            }
            headers
        };
        let text = requester
            .get_with_headers(
                &request_url,
                headers,
                Some(Version::HTTP_2),
            )
            .await
            .map_err(|e| Error::from(format!("{}", e)))?;

        let block = text
            .split("vt_list_models_with_advertising_custom_models_list_items")
            .last()
            .ok_or_else(|| ErrorKind::Parse("missing stars block".into()))?
            .split("pb-page-description")
            .next()
            .unwrap_or("");

        for el in block.split("<div class=\"col\">").skip(1) {
            if el.contains("pb-promoted-link")
                || !el.contains("href=\"https://pimpbunny.com/onlyfans-models/")
            {
                continue;
            }

            let id = el
                .split("href=\"https://pimpbunny.com/onlyfans-models/")
                .nth(1)
                .and_then(|s| s.split("/\"").next())
                .ok_or_else(|| ErrorKind::Parse(format!("star id: {el}").into()))?
                .to_string();

            let title = el
                .split("ui-card-title")
                .nth(1)
                .and_then(|s| s.split('<').next())
                .ok_or_else(|| ErrorKind::Parse(format!("star title: {el}").into()))?
                .to_string();

            Self::push_unique(&stars, FilterOption { id, title });
        }
        Ok(())
    }

    async fn load_categories(base: &str, cats: Arc<RwLock<Vec<FilterOption>>>) -> Result<()> {
        let mut requester = Requester::new();
        Self::warm_root_session_for_base(base, &mut requester).await;
        let request_url = format!("{base}/categories/?items_per_page=120");
        let headers = {
            let root_url = format!("{}/", base.trim_end_matches('/'));
            let mut headers = Self::html_headers_with_referer(&root_url);
            if let Some(cookie) = requester.cookie_header_for_url(&request_url) {
                headers.push(("Cookie".to_string(), cookie));
            }
            headers
        };
        let text = requester
            .get_with_headers(
                &request_url,
                headers,
                Some(Version::HTTP_2),
            )
            .await
            .map_err(|e| Error::from(format!("{}", e)))?;

        let block = text
            .split("list_categories_categories_list_items")
            .last()
            .ok_or_else(|| ErrorKind::Parse("categories block".into()))?
            .split("pb-pagination-wrapper")
            .next()
            .unwrap_or("");

        for el in block.split("<div class=\"col\">").skip(1) {
            let id = el
                .split("href=\"https://pimpbunny.com/categories/")
                .nth(1)
                .and_then(|s| s.split("/\"").next())
                .ok_or_else(|| ErrorKind::Parse(format!("category id: {el}").into()))?
                .to_string();

            let title = el
                .split("ui-heading-h3")
                .nth(1)
                .and_then(|s| s.split('<').next())
                .ok_or_else(|| ErrorKind::Parse(format!("category title: {el}").into()))?
                .titlecase();

            Self::push_unique(&cats, FilterOption { id, title });
        }
        Ok(())
    }

    async fn get(
        &self,
        cache: VideoCache,
        page: u8,
        sort: &str,
        options: ServerOptions,
    ) -> Result<Vec<VideoItem>> {
        let sort_string = match sort {
            "best rated" => "&sort_by=rating",
            "most viewed" => "&sort_by=video_viewed",
            _ => "&sort_by=post_date",
        };
        let video_url = format!(
            "{}/videos/{}/?videos_per_page=32{}",
            self.url, page, sort_string
        );
        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());
                } else {
                    items.clone()
                }
            }
            None => {
                vec![]
            }
        };
        let mut requester =
            crate::providers::requester_or_default(&options, module_path!(), "missing_requester");
        self.warm_root_session(&mut requester).await;
        let headers = self.headers_with_cookies(&requester, &video_url, &self.root_referer());
        let text = match requester
            .get_with_headers(&video_url, headers, Some(Version::HTTP_11))
            .await
        {
            Ok(text) => text,
            Err(e) => {
                crate::providers::report_provider_error(
                    "pimpbunny",
                    "get.request",
                    &format!("url={video_url}; error={e}"),
                )
                .await;
                return Ok(old_items);
            }
        };
        let video_items: Vec<VideoItem> = self
            .get_video_items_from_html(text.clone(), &mut requester)
            .await;
        if !video_items.is_empty() {
            cache.remove(&video_url);
            cache.insert(video_url.clone(), video_items.clone());
        } else {
            return Ok(old_items);
        }
        Ok(video_items)
    }

    async fn query(
        &self,
        cache: VideoCache,
        page: u8,
        query: &str,
        options: ServerOptions,
    ) -> Result<Vec<VideoItem>> {
        let search_string = query.trim().to_string();

        let mut video_url = format!(
            "{}/search/{}/?mode=async&function=get_block&block_id=list_videos_videos_list_search_result&videos_per_page=32&from_videos={}",
            self.url,
            search_string.replace(" ", "-"),
            page
        );

        let sort_string = match options.sort.as_deref().unwrap_or("") {
            "best rated" => "&sort_by=rating",
            "most viewed" => "&sort_by=video_viewed",
            _ => "&sort_by=post_date",
        };
        if let Ok(stars) = self.stars.read() {
            if let Some(star) = stars
                .iter()
                .find(|s| s.title.to_ascii_lowercase() == search_string.to_ascii_lowercase())
            {
                video_url = format!(
                    "{}/onlyfans-models/{}/{}/?videos_per_page=20{}",
                    self.url, star.id, page, sort_string
                );
            }
        } else {
            crate::providers::report_provider_error_background(
                "pimpbunny",
                "query.stars_read",
                "failed to lock stars",
            );
        }
        if let Ok(categories) = self.categories.read() {
            if let Some(cat) = categories
                .iter()
                .find(|c| c.title.to_ascii_lowercase() == search_string.to_ascii_lowercase())
            {
                video_url = format!(
                    "{}/categories/{}/{}/?videos_per_page=20{}",
                    self.url, cat.id, page, sort_string
                );
            }
        } else {
            crate::providers::report_provider_error_background(
                "pimpbunny",
                "query.categories_read",
                "failed to lock categories",
            );
        }
        // Check our Video Cache. If the result is younger than 1 hour, we return it.
        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());
                } else {
                    let _ = cache.check().await;
                    return Ok(items.clone());
                }
            }
            None => {
                vec![]
            }
        };

        let mut requester =
            crate::providers::requester_or_default(&options, module_path!(), "missing_requester");
        self.warm_root_session(&mut requester).await;
        println!("Fetching URL: {}", video_url);
        let headers = self.headers_with_cookies(&requester, &video_url, &self.root_referer());
        let text = match requester
            .get_with_headers(&video_url, headers, Some(Version::HTTP_2))
            .await
        {
            Ok(text) => text,
            Err(e) => {
                crate::providers::report_provider_error(
                    "pimpbunny",
                    "query.request",
                    &format!("url={video_url}; error={e}"),
                )
                .await;
                return Ok(old_items);
            }
        };
        let video_items: Vec<VideoItem> = self
            .get_video_items_from_html(text.clone(), &mut requester)
            .await;
        if !video_items.is_empty() {
            cache.remove(&video_url);
            cache.insert(video_url.clone(), video_items.clone());
        } else {
            return Ok(old_items);
        }
        Ok(video_items)
    }

    async fn get_video_items_from_html(
        &self,
        html: String,
        requester: &mut Requester,
    ) -> Vec<VideoItem> {
        if html.is_empty() || html.contains("404 Not Found") {
            return vec![];
        }

        let block = match html
            .split("-pagination-wrapper")
            .next()
            .and_then(|s| s.split("videos_videos_list").nth(2))
        {
            Some(b) => b,
            None => return vec![],
        };

        let futures = block
            .split("<div class=\"col\">")
            .skip(1)
            .map(|el| self.get_video_item(el.to_string(), requester.clone()));

        join_all(futures)
            .await
            .into_iter()
            .filter_map(Result::ok)
            .collect()
    }

    async fn get_video_item(&self, seg: String, mut requester: Requester) -> Result<VideoItem> {
        let video_url = seg
            .split(" href=\"")
            .nth(1)
            .and_then(|s| s.split('"').next())
            .ok_or_else(|| ErrorKind::Parse("video url".into()))?
            .to_string();

        let mut title = seg
            .split("card-title")
            .nth(1)
            .and_then(|s| s.split('>').nth(1))
            .and_then(|s| s.split('<').next())
            .ok_or_else(|| ErrorKind::Parse("video title".into()))?
            .trim()
            .to_string();
        title = decode(title.as_bytes())
            .to_string()
            .unwrap_or(title)
            .titlecase();

        let id = video_url
            .split('/')
            .nth(4)
            .and_then(|s| s.split('.').next())
            .ok_or_else(|| ErrorKind::Parse("video id".into()))?
            .to_string();

        let thumb_block = seg
            .split("card-thumbnail")
            .nth(1)
            .ok_or_else(|| ErrorKind::Parse("thumb block".into()))?;

        let mut thumb = thumb_block
            .split("src=\"")
            .nth(1)
            .and_then(|s| s.split('"').next())
            .unwrap_or("")
            .to_string();

        if thumb.contains("data:image/jpg;base64") {
            thumb = thumb_block
                .split("data-original=\"")
                .nth(1)
                .and_then(|s| s.split('"').next())
                .unwrap_or("")
                .to_string();
        }

        let preview = thumb_block
            .split("data-preview=\"")
            .nth(1)
            .and_then(|s| s.split('"').next())
            .unwrap_or("")
            .to_string();

        let (tags, formats, views, duration) =
            self.extract_media(&video_url, &mut requester).await?;

        Ok(
            VideoItem::new(id, title, video_url, "pimpbunny".into(), thumb, duration)
                .formats(formats)
                .tags(tags)
                .preview(preview)
                .views(views),
        )
    }

    async fn extract_media(
        &self,
        url: &str,
        requester: &mut Requester,
    ) -> Result<(Vec<String>, Vec<VideoFormat>, u32, u32)> {
        self.warm_root_session(requester).await;
        let headers = self.headers_with_cookies(requester, url, &self.root_referer());
        let text = requester
            .get_with_headers(url, headers, Some(Version::HTTP_2))
            .await
            .map_err(|e| Error::from(format!("{}", e)))?;

        let json_str = text
            .split("application/ld+json\">")
            .nth(1)
            .and_then(|s| s.split("</script>").next())
            .ok_or_else(|| ErrorKind::Parse("ld+json".into()))?;

        let json: serde_json::Value = serde_json::from_str(json_str)?;

        let video_url = json["contentUrl"].as_str().unwrap_or("").to_string();
        let quality = video_url
            .split('_')
            .last()
            .and_then(|s| s.split('.').next())
            .unwrap_or("")
            .to_string();

        let views = json["interactionStatistic"]
            .as_array()
            .and_then(|a| a.first())
            .and_then(|v| v["userInteractionCount"].as_str())
            .and_then(|v| v.parse().ok())
            .unwrap_or(0);

        let duration = json["duration"]
            .as_str()
            .map(|d| parse_time_to_seconds(&d.replace(['P', 'T', 'H', 'M', 'S'], "")).unwrap_or(0))
            .unwrap_or(0) as u32;

        let mut format = VideoFormat::new(video_url.clone(), quality, "video/mp4".into())
            .http_header("Referer".to_string(), url.to_string())
            .http_header(
                "User-Agent".to_string(),
                Self::FIREFOX_USER_AGENT.to_string(),
            );
        if let Some(cookie) = requester.cookie_header_for_url(&video_url) {
            format.add_http_header("Cookie".to_string(), cookie);
        }

        Ok((vec![], vec![format], views, duration))
    }
}

#[async_trait]
impl Provider for PimpbunnyProvider {
    async fn get_videos(
        &self,
        cache: VideoCache,
        _pool: DbPool,
        sort: String,
        query: Option<String>,
        page: String,
        _per_page: String,
        options: ServerOptions,
    ) -> Vec<VideoItem> {
        let page = page.parse::<u8>().unwrap_or(1);
        let thumb_options = options.clone();

        let res = match query {
            Some(q) => self.to_owned().query(cache, page, &q, options).await,
            None => self.get(cache, page, &sort, options).await,
        };

        res.unwrap_or_else(|e| {
            eprintln!("pimpbunny error: {e}");
            vec![]
        })
        .into_iter()
        .map(|mut item| {
            if !item.thumb.is_empty() {
                item.thumb = self.proxied_thumb(&thumb_options, &item.thumb);
            }
            item
        })
        .collect()
    }

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

#[cfg(test)]
mod tests {
    use super::PimpbunnyProvider;
    use crate::videos::ServerOptions;

    #[test]
    fn rewrites_allowed_thumbs_to_proxy_urls() {
        let provider = PimpbunnyProvider::new();
        let options = ServerOptions {
            featured: None,
            category: None,
            sites: None,
            filter: None,
            language: None,
            public_url_base: Some("https://example.com".to_string()),
            requester: None,
            network: None,
            stars: None,
            categories: None,
            duration: None,
            sort: None,
            sexuality: None,
        };

        let proxied = provider.proxied_thumb(
            &options,
            "https://pimpbunny.com/contents/videos_screenshots/517000/517329/800x450/1.jpg",
        );

        assert_eq!(
            proxied,
            "https://example.com/proxy/pimpbunny-thumb/pimpbunny.com/contents/videos_screenshots/517000/517329/800x450/1.jpg"
        );
    }
}
