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::videos::{ServerOptions, VideoItem};

use async_trait::async_trait;
use error_chain::error_chain;
use std::sync::{Arc, RwLock};
use std::vec;
use wreq::Version;

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

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 XfreeProvider {
    url: String,
    categories: Arc<RwLock<Vec<FilterOption>>>,
}

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

    fn build_channel(&self, clientversion: ClientVersion) -> Channel {
        let _ = clientversion;
        Channel {
            id: "xfree".to_string(),
            name: "XFree".to_string(),
            description: "Reels & Nudes!".to_string(),
            premium: false,
            favicon: "https://www.google.com/s2/favicons?sz=64&domain=xfree.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(
                        "xfree",
                        "build_channel.categories_read",
                        &e.to_string(),
                    );
                    vec![]
                }),
            options: vec![ChannelOption {
                id: "sexuality".to_string(),
                title: "Sexuality".to_string(),
                description: "Sexuality of the Videos".to_string(),
                systemImage: "heart".to_string(),
                colorName: "red".to_string(),
                multiSelect: false,
                options: vec![
                    FilterOption {
                        id: "1".to_string(),
                        title: "Straight".to_string(),
                    },
                    FilterOption {
                        id: "2".to_string(),
                        title: "Gay".to_string(),
                    },
                    FilterOption {
                        id: "3".to_string(),
                        title: "Trans".to_string(),
                    },
                ],
            }],
            nsfw: true,
            cacheDuration: None,
            ytdlpCommand: Some("yt-dlp --impersonate chrome-120".to_string())
        }
    }

    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);
            }
        }
    }

    async fn query(
        &self,
        cache: VideoCache,
        page: u8,
        query: &str,
        options: ServerOptions,
        pool: DbPool,
    ) -> Result<Vec<VideoItem>> {
        let query = if query.is_empty() { "null" } else { query };
        let sexuality = match options.clone().sexuality {
            Some(s) if !s.is_empty() => s,
            _ => "1".to_string(),
        };
        let video_url = format!(
            "{}/api/2/search?search={}&lgbt={}&limit=30&offset={}",
            self.url,
            query.replace(" ", "%20"),
            sexuality,
            (page as u32 - 1) * 30
        );
        // 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 * 60 * 24 {
                    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");
        // let _ = requester.get("https://www.xfree.com/", Some(Version::HTTP_2)).await;
        let text = match requester
            .get_with_headers(
                &video_url,
                vec![
                    ("Apiversion".to_string(), "1.0".to_string()),
                    (
                        "Accept".to_string(),
                        "application/json text/plain */*".to_string(),
                    ),
                    ("Referer".to_string(), "https://www.xfree.com/".to_string()),
                ],
                Some(Version::HTTP_2),
            )
            .await
        {
            Ok(text) => text,
            Err(e) => {
                crate::providers::report_provider_error(
                    "xfree",
                    "query.request",
                    &format!("url={video_url}; error={e}"),
                )
                .await;
                return Ok(old_items);
            }
        };
        let video_items: Vec<VideoItem> = self
            .get_video_items_from_json(text.clone(), &mut requester, pool)
            .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)
    }

    /// Front-page feed used when there is no search query. This mirrors the
    /// site's homepage, which dispatches `getAutoPop` against
    /// `/api/post/?t=popular&nsfhp=true&limit=30&offset=N&lgbt=X` instead of the
    /// `/api/2/search` endpoint. (`popular` is the real feed type the homepage
    /// loads first; `posts` is only a Vuex store key and 404s as a `t=` value.)
    /// The response body is the post array directly, not `body.posts`.
    async fn front_page(
        &self,
        cache: VideoCache,
        page: u8,
        options: ServerOptions,
        pool: DbPool,
    ) -> Result<Vec<VideoItem>> {
        let sexuality = match options.clone().sexuality {
            Some(s) if !s.is_empty() => s,
            _ => "1".to_string(),
        };
        let offset = (page as u32 - 1) * 30;
        let video_url = format!(
            "{}/api/post/?t=popular&nsfhp=true&limit=30{}&lgbt={}",
            self.url,
            if page > 1 {
                format!("&offset={offset}")
            } else {
                String::new()
            },
            sexuality,
        );
        // 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 * 60 * 24 {
                    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");
        let text = match requester
            .get_with_headers(
                &video_url,
                vec![
                    ("Apiversion".to_string(), "1.0".to_string()),
                    (
                        "Accept".to_string(),
                        "application/json text/plain */*".to_string(),
                    ),
                    ("Referer".to_string(), "https://www.xfree.com/".to_string()),
                ],
                Some(Version::HTTP_2),
            )
            .await
        {
            Ok(text) => text,
            Err(e) => {
                crate::providers::report_provider_error(
                    "xfree",
                    "front_page.request",
                    &format!("url={video_url}; error={e}"),
                )
                .await;
                return Ok(old_items);
            }
        };
        let video_items: Vec<VideoItem> = self
            .get_video_items_from_json(text.clone(), &mut requester, pool)
            .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_json(
        &self,
        html: String,
        _requester: &mut Requester,
        _pool: DbPool,
    ) -> Vec<VideoItem> {
        let mut items: Vec<VideoItem> = Vec::new();
        let json_result = serde_json::from_str::<serde_json::Value>(&html);
        let json = match json_result {
            Ok(json) => json,
            Err(e) => {
                eprintln!("Failed to parse JSON: {e}");
                crate::providers::report_provider_error(
                    "xfree",
                    "get_video_items_from_json.parse",
                    &format!("Failed to parse JSON: {e}"),
                )
                .await;
                return vec![];
            }
        };

        // The search endpoint returns `{ body: { posts: [...] } }`, while the
        // front-page feed (`/api/post/`) returns `{ body: [...] }` directly.
        // Mirror the site's own logic (`body.posts ? body.posts : body`).
        let empty: Vec<serde_json::Value> = vec![];
        let body = json.get("body");
        let posts = body
            .and_then(|v| v.get("posts"))
            .and_then(|p| p.as_array())
            .or_else(|| body.and_then(|v| v.as_array()))
            .unwrap_or(&empty);
        for post in posts {
            let id = post
                .get("media")
                .and_then(|v| v.get("name"))
                .and_then(|v| v.as_str())
                .unwrap_or_default();
            let title = post
                .get("title")
                .and_then(|v| v.as_str())
                .unwrap_or_default()
                .to_string();
            let video_url = format!(
                "https://cdn.xfree.com/xfree-prod/{}/{}/{}/{}/full.mp4",
                id.chars().nth(0).unwrap_or('0'),
                id.chars().nth(1).unwrap_or('0'),
                id.chars().nth(2).unwrap_or('0'),
                id
            );

            let listsuffix = post
                .get("media")
                .and_then(|v| v.get("listingSuffix"))
                .and_then(|v| v.as_i64())
                .unwrap_or_default();
            let thumb = format!(
                "https://thumbs.xfree.com/listing/medium/{}_{}.webp",
                id, listsuffix
            );
            let views = post.get("viewCount").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
            let preview = format!(
                "https://cdn.xfree.com/xfree-prod/{}/{}/{}/{}/listing7.mp4",
                id.chars().nth(0).unwrap_or('0'),
                id.chars().nth(1).unwrap_or('0'),
                id.chars().nth(2).unwrap_or('0'),
                id
            );
            let duration = post
                .get("media")
                .and_then(|v| v.get("duration"))
                .and_then(|v| v.as_f64())
                .unwrap_or_default() as u32;
            let tags = post
                .get("tags")
                .and_then(|v| v.as_array())
                .unwrap_or(&vec![])
                .iter()
                .filter_map(|t| t.get("tag").and_then(|n| n.as_str()).map(|s| s.to_string()))
                .collect::<Vec<String>>();
            for tag in tags.iter() {
                Self::push_unique(
                    &self.categories,
                    FilterOption {
                        id: tag.clone(),
                        title: tag.clone(),
                    },
                );
            }
            let uploader = post
                .get("user")
                .and_then(|v| v.get("displayName"))
                .and_then(|v| v.as_str())
                .unwrap_or_default()
                .to_string();
            let upload_date = post
                .get("publishedDate")
                .and_then(|v| v.as_str())
                .unwrap_or_default()
                .to_string();
            let uploaded_at = chrono::DateTime::parse_from_rfc3339(&upload_date)
                .map(|dt| dt.timestamp() as u64)
                .unwrap_or(0);
            let aspect_ration = post
                .get("media")
                .and_then(|v| v.get("aspectRatio"))
                .and_then(|v| v.as_str())
                .unwrap_or_default()
                .to_string()
                .parse::<f32>()
                .unwrap_or(0.5625);
            let video_item = VideoItem::new(
                id.to_string(),
                title,
                video_url,
                "xfree".to_string(),
                thumb,
                duration,
            )
            .views(views)
            .preview(preview)
            .tags(tags)
            .uploader(uploader)
            .uploaded_at(uploaded_at)
            .aspect_ratio(aspect_ration);
            items.push(video_item);
        }
        return items;
    }
}

#[async_trait]
impl Provider for XfreeProvider {
    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 query = query.unwrap_or_default();
        let res = if query.trim().is_empty() {
            // Empty query => front page feed, not the search endpoint.
            self.to_owned().front_page(cache, page, options, pool).await
        } else {
            self.to_owned()
                .query(cache, page, &query, options, pool)
                .await
        };

        res.unwrap_or_else(|e| {
            eprintln!("xfree error: {e}");
            vec![]
        })
    }

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