use crate::DbPool;
use crate::api::ClientVersion;
use crate::providers::Provider;
use crate::status::*;
use crate::util::cache::VideoCache;
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;

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

#[derive(Debug, Clone)]
pub struct HqpornerProvider {
    url: String,
    stars: Arc<RwLock<Vec<FilterOption>>>,
    categories: Arc<RwLock<Vec<FilterOption>>>,
}
impl HqpornerProvider {
    pub fn new() -> Self {
        let provider = HqpornerProvider {
            url: "https://hqporner.com".to_string(),
            stars: Arc::new(RwLock::new(vec![])),
            categories: Arc::new(RwLock::new(vec![])),
        };
        provider.spawn_initial_load();
        provider
    }

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

        thread::spawn(move || {
            // Create a tiny runtime just for these async tasks
            let rt = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
                .expect("build tokio runtime");

            rt.block_on(async move {
                if let Err(e) = Self::load_stars(&url, stars).await {
                    eprintln!("load_stars failed: {e}");
                }
                if let Err(e) = Self::load_categories(&url, categories).await {
                    eprintln!("load_categories failed: {e}");
                }
            });
        });
    }

    async fn load_stars(base_url: &str, stars: Arc<RwLock<Vec<FilterOption>>>) -> Result<()> {
        let mut requester = Requester::new();
        let text = requester
            .get(format!("{}/girls", &base_url).as_str(), None)
            .await
            .unwrap();
        let stars_div = text
            .split("<span>Girls</span>")
            .collect::<Vec<&str>>().last().unwrap()
            .split("</ul>")
            .collect::<Vec<&str>>()[0];
        for stars_element in stars_div.split("<li ").collect::<Vec<&str>>()[1..].to_vec() {
            let star_id = stars_element.split("href=\"/actress/").collect::<Vec<&str>>()[1]
                .split("\"")
                .collect::<Vec<&str>>()[0]
                .to_string();
            let star_name = stars_element.split("<a ").collect::<Vec<&str>>()[1]
                .split(">").collect::<Vec<&str>>()[1]
                .split("<")
                .collect::<Vec<&str>>()[0]
                .to_string();
            Self::push_unique(
                &stars,
                FilterOption {
                    id: star_id,
                    title: star_name,
                },
            );
        }
        return Ok(());
    }

    async fn load_categories(base_url: &str, categories: Arc<RwLock<Vec<FilterOption>>>) -> Result<()> {
        let mut requester = Requester::new();
        let text = requester
            .get(format!("{}/categories", &base_url).as_str(), None)
            .await
            .unwrap();
        let categories_div = text
            .split("<span>Categories</span>")
            .collect::<Vec<&str>>().last().unwrap()
            .split("</ul>")
            .collect::<Vec<&str>>()[0];
        for categories_element in categories_div.split("<li ").collect::<Vec<&str>>()[1..].to_vec() {
            let category_id = categories_element.split("href=\"/category/").collect::<Vec<&str>>()[1]
                .split("\"")
                .collect::<Vec<&str>>()[0]
                .to_string();
            let category_name = categories_element.split("<a ").collect::<Vec<&str>>()[1]
                .split(">").collect::<Vec<&str>>()[1]
                .split("<")
                .collect::<Vec<&str>>()[0]
                .titlecase();
            Self::push_unique(
                &categories,
                FilterOption {
                    id: category_id,
                    title: category_name,
                },
            );
        }
        return Ok(());
    }

    fn build_channel(&self, clientversion: ClientVersion) -> Channel {
        let _ = clientversion;
        Channel {
            id: "hqporner".to_string(),
            name: "HQPorner".to_string(),
            description: "HD Porn Videos Tube".to_string(),
            premium: false,
            favicon: "https://www.google.com/s2/favicons?sz=64&domain=hqporner.com".to_string(),
            status: "active".to_string(),
            categories: self.categories.read().unwrap().iter().map(|c| c.title.clone()).collect(),
            options: vec![],
            nsfw: true,
            cacheDuration: None,
        }
    }

    // Push one item with minimal lock time and dedup by id
    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);
                // Optional: keep it sorted for nicer UX
                // vec.sort_by(|a,b| a.title.cmp(&b.title));
            }
        }
    }

    async fn get(
        &self,
        cache: VideoCache,
        page: u8,
        sort: &str,
        options: ServerOptions,
    ) -> Result<Vec<VideoItem>> {
        let _ = sort;
        let video_url = format!("{}/hdporn/{}", self.url, page);
        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 = options.requester.clone().unwrap();
        let text = requester.get(&video_url, None).await.unwrap();
        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!("{}/?q={}&p={}", self.url, search_string, page);

         if let Some(star) = self.stars.read().unwrap().iter().find(|s| s.title.to_ascii_lowercase() == search_string.to_ascii_lowercase()) {
            video_url = format!("{}/actress/{}/{}", self.url, star.id, page);
        }
        if let Some(cat) = self.categories.read().unwrap().iter().find(|c| c.title.to_ascii_lowercase() == search_string.to_ascii_lowercase()) {
            video_url = format!("{}/category/{}/{}", self.url, cat.id, page);
        }
        // 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 = options.requester.clone().unwrap();

        let text = requester.get(&video_url, None).await.unwrap();
        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 raw_videos = html.split("id=\"footer\"").collect::<Vec<&str>>()[0]
            .split("<section class=\"box features\">")
            .collect::<Vec<&str>>()[2]
            .split("<section class=\"box feature\">")
            .collect::<Vec<&str>>()[1..]
            .to_vec();

        let futures = raw_videos
            .into_iter()
            .map(|el| self.get_video_item(el.to_string(), requester.clone()));
        let results: Vec<Result<VideoItem>> = join_all(futures).await;
        let video_items: Vec<VideoItem> = results.into_iter().filter_map(Result::ok).collect();
        return video_items;
    }

    async fn get_video_item(
        &self,
        video_segment: String,
        mut requester: Requester,
    ) -> Result<VideoItem> {
        let video_url: String = format!(
            "{}{}",
            self.url,
            video_segment.split("<a href=\"").collect::<Vec<&str>>()[1]
                .split("\"")
                .collect::<Vec<&str>>()[0]
                .to_string()
        );
        let mut title = video_segment
            .split("<h3 class=\"meta-data-title\">")
            .collect::<Vec<&str>>()[1]
            .split(">")
            .collect::<Vec<&str>>()[1]
            .split("<")
            .collect::<Vec<&str>>()[0]
            .trim()
            .to_string();
        // html decode
        title = decode(title.as_bytes())
            .to_string()
            .unwrap_or(title)
            .titlecase();
        let id = video_url.split("/").collect::<Vec<&str>>()[4]
            .split(".")
            .collect::<Vec<&str>>()[0]
            .to_string();

        let thumb = format!(
            "https:{}",
            video_segment
                .split("onmouseleave='defaultImage(\"")
                .collect::<Vec<&str>>()[1]
                .split("\"")
                .collect::<Vec<&str>>()[0]
                .to_string()
        );
        let raw_duration = video_segment
            .split("<span class=\"icon fa-clock-o meta-data\">")
            .collect::<Vec<&str>>()[1]
            .split("s<")
            .collect::<Vec<&str>>()[0]
            .replace("m ", ":")
            .to_string();
        let duration = parse_time_to_seconds(raw_duration.as_str()).unwrap_or(0) as u32;

        let (tags, formats) = match self.extract_media(&video_url, &mut requester).await{
            Ok((t, f)) => (t, f),
            Err(_) => return Err(Error::from("Video media extraction failed")),
        };
        if formats.is_empty() {
            return Err(Error::from("No formats found for video"));
        }
        let video_item = VideoItem::new(
            id,
            title,
            video_url,
            "hqporner".to_string(),
            thumb,
            duration,
        )
        .formats(formats)
        .tags(tags);
        return Ok(video_item);
    }

    async fn extract_media(
        &self,
        video_page_url: &str,
        requester: &mut Requester,
    ) -> Result<(Vec<String>, Vec<VideoFormat>)> {
        let mut formats = vec![];
        let mut tags = vec![];
        let text = requester.get_raw_with_headers(&video_page_url, vec![("Referer".to_string(), "https://hqporner.com".to_string())])
            .await.unwrap().text().await.unwrap();
        if text.contains("Why do I see it?") {
            return Ok((tags, formats));
        }
        let stars_elements = text.split("icon fa-star-o").collect::<Vec<&str>>()[1]
            .split("</li>")
            .collect::<Vec<&str>>()[0]
            .split("href=\"/actress/")
            .collect::<Vec<&str>>()[1..]
            .to_vec();
        for star_el in stars_elements {
            let star_id = star_el.split("\"").collect::<Vec<&str>>()[0].to_string();
            let star_name = star_el.split("\">").collect::<Vec<&str>>()[1]
                .split("<")
                .collect::<Vec<&str>>()[0]
                .to_string();
            tags.push(star_name.clone());
            Self::push_unique(&self.stars, FilterOption {
                id: star_id,
                title: star_name.clone(),
            });
        }
        let categories_elements = text.split("This video belongs to the following categories").collect::<Vec<&str>>()[1]
            .split("</p>")
            .collect::<Vec<&str>>()[0]
            .split("href=\"/category/")
            .collect::<Vec<&str>>()[1..]
            .to_vec();
        for categories_el in categories_elements {
            let category_id = categories_el.split("\"").collect::<Vec<&str>>()[0].to_string();
            let category_name = categories_el.split("\">").collect::<Vec<&str>>()[1]
                .split("<")
                .collect::<Vec<&str>>()[0].titlecase();
            tags.push(category_name.clone());
            Self::push_unique(&self.categories, FilterOption {
                id: category_id,
                title: category_name.clone(),
            });
        }

        let video_url = format!(
            "https:{}",
            text.split("url: '/blocks/altplayer.php?i=")
                .collect::<Vec<&str>>()[1]
                .split("'")
                .collect::<Vec<&str>>()[0]
        );

        let text2 = requester
            .get_raw_with_headers(
                &video_url,
                vec![("Referer".to_string(), "https://hqporner.com/".to_string())],
            )
            .await
            .unwrap()
            .text()
            .await
            .unwrap();
        match text2.split("<video ").collect::<Vec<&str>>().len() > 2 {
            false => return Err(Error::from("No video element found")),
            true => (),
        }
        let video_element = text2.split("<video ").collect::<Vec<&str>>()[2]
            .split("</video>")
            .collect::<Vec<&str>>()[0];
        let sources = video_element.split("<source ").collect::<Vec<&str>>()[1..].to_vec();
        for source in sources {
            let title = source.split("title=\\\"").collect::<Vec<&str>>()[1]
                .split("\\\"")
                .collect::<Vec<&str>>()[0]
                .to_string();
            let quality = title.split(" ").collect::<Vec<&str>>()[0].to_string();
            let format = "mp4".to_string();
            let media_url = format!(
                "https:{}",
                source.split("src=\\\"").collect::<Vec<&str>>()[1]
                    .split("\\\"")
                    .collect::<Vec<&str>>()[0]
            );

            formats.push(
                VideoFormat::new(media_url, quality, format)
                    .format_id(title.clone())
                    .format_note(title.clone()),
            );
        }
        Ok((tags, formats))
    }
}

#[async_trait]
impl Provider for HqpornerProvider {
    async fn get_videos(
        &self,
        cache: VideoCache,
        pool: DbPool,
        sort: String,
        query: Option<String>,
        page: String,
        per_page: String,
        options: ServerOptions,
    ) -> Vec<VideoItem> {
        let _ = per_page;
        let _ = pool;
        let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
            Some(q) => {
                self.query(cache, page.parse::<u8>().unwrap_or(1), &q, options)
                    .await
            }
            None => {
                self.get(cache, page.parse::<u8>().unwrap_or(1), &sort, options)
                    .await
            }
        };
        match videos {
            Ok(v) => v,
            Err(e) => {
                println!("Error fetching videos: {}", e);
                vec![]
            }
        }
    }
    fn get_channel(&self, clientversion: ClientVersion) -> crate::status::Channel {
        self.build_channel(clientversion)
    }
}
