use crate::DbPool;
use crate::api::ClientVersion;
use crate::providers::{Provider, report_provider_error_background, requester_or_default};
use crate::status::*;
use crate::util::cache::VideoCache;
use crate::util::discord::send_discord_error_report;
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 std::fmt::Write;
use std::sync::{Arc, RwLock};
use std::vec;
use url::form_urlencoded::Serializer;

pub const CHANNEL_METADATA: crate::providers::ProviderChannelMetadata =
    crate::providers::ProviderChannelMetadata {
        group_id: "pmv-compilation",
        tags: &["pmv", "music", "compilation"],
    };

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

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

impl PmvhavenProvider {
    pub fn new() -> Self {
        Self {
            url: "https://pmvhaven.com".to_string(),
            stars: Arc::new(RwLock::new(vec![])),
            categories: Arc::new(RwLock::new(vec![])),
        }
    }

    fn encode_query_value(value: &str) -> String {
        let mut serializer = Serializer::new(String::new());
        serializer.append_pair("v", value);
        let encoded = serializer.finish();
        encoded.strip_prefix("v=").unwrap_or(&encoded).to_string()
    }

    fn build_channel(&self, clientversion: ClientVersion) -> Channel {
        let _ = clientversion;

        let categories = self
            .categories
            .read()
            .map(|g| g.clone())
            .unwrap_or_default();

        Channel {
            id: "pmvhaven".to_string(),
            name: "PMVHaven".to_string(),
            description: "Best PMV Videos".to_string(),
            premium: false,
            favicon: "https://www.google.com/s2/favicons?sz=64&domain=pmvhaven.com".to_string(),
            status: "active".to_string(),
            categories,
            options: vec![
                ChannelOption {
                    id: "sort".into(),
                    title: "Sort".into(),
                    description: "Sort the Videos".into(),
                    systemImage: "list.number".into(),
                    colorName: "blue".into(),
                    options: vec![
                        FilterOption {
                            id: "relevance".into(),
                            title: "Relevance".into(),
                        },
                        FilterOption {
                            id: "newest".into(),
                            title: "Newest".into(),
                        },
                        FilterOption {
                            id: "oldest".into(),
                            title: "Oldest".into(),
                        },
                        FilterOption {
                            id: "most viewed".into(),
                            title: "Most Viewed".into(),
                        },
                        FilterOption {
                            id: "most liked".into(),
                            title: "Most Liked".into(),
                        },
                        FilterOption {
                            id: "most disliked".into(),
                            title: "Most Disliked".into(),
                        },
                    ],
                    multiSelect: false,
                },
                ChannelOption {
                    id: "duration".into(),
                    title: "Duration".into(),
                    description: "Length of the Videos".into(),
                    systemImage: "timer".into(),
                    colorName: "green".into(),
                    options: vec![
                        FilterOption {
                            id: "any".into(),
                            title: "Any".into(),
                        },
                        FilterOption {
                            id: "<4 min".into(),
                            title: "<4 min".into(),
                        },
                        FilterOption {
                            id: "4-20 min".into(),
                            title: "4-20 min".into(),
                        },
                        FilterOption {
                            id: "20-60 min".into(),
                            title: "20-60 min".into(),
                        },
                        FilterOption {
                            id: ">1 hour".into(),
                            title: ">1 hour".into(),
                        },
                    ],
                    multiSelect: false,
                },
            ],
            nsfw: true,
            cacheDuration: None,
            ytdlpCommand: Some("yt-dlp --impersonate chrome-120".to_string())
        }
    }

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

    fn is_direct_media_url(url: &str) -> bool {
        let lower = url.to_ascii_lowercase();
        (lower.starts_with("http://") || lower.starts_with("https://"))
            && (lower.contains("/videos/") || lower.contains(".mp4") || lower.contains(".m3u8"))
    }

    fn pick_downloadable_media_url(&self, video: &serde_json::Value) -> Option<String> {
        let video_url = video
            .get("videoUrl")
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .trim();
        if Self::is_direct_media_url(video_url) {
            return Some(video_url.replace(' ', "%20"));
        }

        // Fallback: derive direct media URL from object key.
        let key = video
            .get("key")
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .trim_matches('/');
        if !key.is_empty() {
            let rebuilt = format!("https://video.pmvhaven.com/{key}");
            if Self::is_direct_media_url(&rebuilt) {
                return Some(rebuilt.replace(' ', "%20"));
            }
        }

        // Search API returns hlsMasterPlaylistUrl instead of videoUrl/key.
        let hls_url = video
            .get("hlsMasterPlaylistUrl")
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .trim();
        if Self::is_direct_media_url(hls_url) {
            return Some(hls_url.to_string());
        }

        None
    }

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

        let sort = match options.sort.as_deref() {
            Some("newest") => "&sort=-uploadDate",
            Some("oldest") => "&sort=uploadDate",
            Some("most viewed") => "&sort=-views",
            Some("most liked") => "&sort=-likes",
            Some("most disliked") => "&sort=-dislikes",
            _ => "",
        };

        let duration = match options.duration.as_deref() {
            Some("<4 min") => "&durationMax=240",
            Some("4-20 min") => "&durationMin=240&durationMax=1200",
            Some("20-60 min") => "&durationMin=1200&durationMax=3600",
            Some(">1 hour") => "&durationMin=3600",
            _ => "",
        };

        let encoded_search = Self::encode_query_value(&search);

        let mut extra_filters = String::new();
        if let Ok(stars) = self.stars.read() {
            if let Some(star) = stars.iter().find(|s| s.eq_ignore_ascii_case(&search)) {
                let encoded_star = Self::encode_query_value(star);
                extra_filters.push_str(&format!("&stars={encoded_star}"));
            }
        }

        if let Ok(cats) = self.categories.read() {
            if let Some(cat) = cats.iter().find(|c| c.eq_ignore_ascii_case(&search)) {
                let encoded_cat = Self::encode_query_value(cat);
                extra_filters.push_str(&format!("&tagMode=OR&tags={encoded_cat}&expandTags=false"));
            }
        }

        let mut urls = vec![];
        if search.is_empty() {
            urls.push(format!(
                "{}/api/videos?limit=100&page={page}{duration}{sort}{extra_filters}",
                self.url
            ));
        } else {
            urls.push(format!(
                "{}/api/videos/search?limit=100&page={page}{duration}{sort}{extra_filters}&q={encoded_search}",
                self.url
            ));
            urls.push(format!(
                "{}/api/videos/search?limit=100&page={page}{duration}{sort}{extra_filters}&query={encoded_search}",
                self.url
            ));
        }

        let mut requester = requester_or_default(&options, "pmvhaven", "query");
        for url in urls {
            if let Some((time, items)) = cache.get(&url) {
                if time.elapsed().unwrap_or_default().as_secs() < 300 {
                    return Ok(items.clone());
                }
            }

            let text = match requester.get(&url, None).await {
                Ok(text) => text,
                Err(err) => {
                    report_provider_error_background(
                        "pmvhaven",
                        "get.request",
                        &format!("url={url}; error={err}"),
                    );
                    continue;
                }
            };

            let json: serde_json::Value = match serde_json::from_str(&text) {
                Ok(json) => json,
                Err(err) => {
                    report_provider_error_background(
                        "pmvhaven",
                        "parse.json",
                        &format!("url={url}; error={err}"),
                    );
                    continue;
                }
            };
            let items = self.get_video_items_from_json(json).await;

            if !items.is_empty() {
                cache.remove(&url);
                cache.insert(url, items.clone());
                return Ok(items);
            }
        }

        Ok(vec![])
    }

    async fn get_video_items_from_json(&self, json: serde_json::Value) -> Vec<VideoItem> {
        let mut items = vec![];

        if !json
            .get("success")
            .and_then(|v| v.as_bool())
            .unwrap_or(false)
        {
            return items;
        }

        let videos = json
            .get("data")
            .and_then(|v| v.as_array())
            .or_else(|| json.get("videos").and_then(|v| v.as_array()))
            .cloned()
            .unwrap_or_default();

        for video in videos {
            let title = decode(
                video
                    .get("title")
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .as_bytes(),
            )
            .to_string()
            .unwrap_or_default();

            let id = video
                .get("_id")
                .and_then(|v| v.as_str())
                .unwrap_or(&title)
                .to_string();

            let video_url = match self.pick_downloadable_media_url(&video) {
                Some(url) => url,
                None => {
                    continue;
                }
            };
            let thumb = video
                .get("thumbnailUrl")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string();
            let preview = video
                .get("previewUrl")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string();

            let views = video.get("views").and_then(|v| v.as_u64()).unwrap_or(0);
            let duration = parse_time_to_seconds(
                video
                    .get("duration")
                    .and_then(|v| v.as_str())
                    .unwrap_or("0"),
            )
            .unwrap_or(0);

            let tags = video
                .get("tags")
                .and_then(|v| v.as_array())
                .cloned()
                .unwrap_or_default();
            let stars = video
                .get("starsTags")
                .and_then(|v| v.as_array())
                .cloned()
                .unwrap_or_default();
            for t in tags.iter() {
                if let Some(s) = t.as_str() {
                    let decoded = decode(s.as_bytes()).to_string().unwrap_or_default();
                    Self::push_unique(&self.categories, decoded.clone());
                }
            }
            for t in stars.iter() {
                if let Some(s) = t.as_str() {
                    let decoded = decode(s.as_bytes()).to_string().unwrap_or_default();
                    Self::push_unique(&self.stars, decoded.clone());
                }
            }

            let format_type = if video_url.to_ascii_lowercase().contains(".m3u8") {
                "m3u8".to_string()
            } else {
                "mp4".to_string()
            };
            items.push(
                VideoItem::new(
                    id,
                    title,
                    video_url.clone(),
                    "pmvhaven".into(),
                    thumb,
                    duration as u32,
                )
                .views(views as u32)
                .formats(vec![VideoFormat::new(
                    video_url,
                    "1080".to_string(),
                    format_type,
                )])
                .preview(preview),
            );
        }

        items
    }
}

#[cfg(test)]
mod tests {
    use super::PmvhavenProvider;
    use serde_json::json;

    #[tokio::test]
    async fn parses_videos_from_videos_key() {
        let provider = PmvhavenProvider::new();
        let payload = json!({
            "success": true,
            "videos": [{
                "_id": "abc123",
                "title": "Sample Title",
                "videoUrl": "https://video.pmvhaven.com/videos/sample.mp4",
                "thumbnailUrl": "https://video.pmvhaven.com/thumbnails/sample.webp",
                "previewUrl": "https://video.pmvhaven.com/previews/sample.mp4",
                "views": 42,
                "duration": "2:11",
                "tags": [],
                "starsTags": []
            }]
        });

        let items = provider.get_video_items_from_json(payload).await;
        assert_eq!(items.len(), 1);
    }

    #[tokio::test]
    async fn parses_videos_from_data_key() {
        let provider = PmvhavenProvider::new();
        let payload = json!({
            "success": true,
            "data": [{
                "_id": "abc123",
                "title": "Sample Title",
                "videoUrl": "https://video.pmvhaven.com/videos/sample.mp4",
                "thumbnailUrl": "https://video.pmvhaven.com/thumbnails/sample.webp",
                "previewUrl": "https://video.pmvhaven.com/previews/sample.mp4",
                "views": 42,
                "duration": "2:11",
                "tags": [],
                "starsTags": []
            }]
        });

        let items = provider.get_video_items_from_json(payload).await;
        assert_eq!(items.len(), 1);
    }
}

#[async_trait]
impl Provider for PmvhavenProvider {
    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();

        match self.query(cache, page, &query, options).await {
            Ok(v) => v,
            Err(e) => {
                eprintln!("pmvhaven error: {e}");
                let mut chain_str = String::new();
                for (i, cause) in e.iter().enumerate() {
                    let _ = writeln!(chain_str, "{}. {}", i + 1, cause);
                }
                send_discord_error_report(
                    e.to_string(),
                    Some(chain_str),
                    Some("PMVHaven Provider"),
                    Some("Failed to load videos from PMVHaven"),
                    file!(),
                    line!(),
                    module_path!(),
                )
                .await;
                vec![]
            }
        }
    }

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