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

use async_trait::async_trait;
use error_chain::error_chain;
use serde::Deserialize;

pub const CHANNEL_METADATA: crate::providers::ProviderChannelMetadata =
    crate::providers::ProviderChannelMetadata {
        group_id: "tiktok",
        tags: &["shorts", "pornhub", "vertical"],
    };

const BASE_URL: &str = "https://www.pornhub.com";
const CHANNEL_ID: &str = "ph-shorties";

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 PornhubShortiesProvider {
    url: String,
}

#[derive(Debug, Clone)]
enum Target {
    Home { sort: String },
    Search { query: String, sort: String },
}

/// Deserialized structure of each entry in the `JSON_SHORTIES` JS variable.
#[derive(Debug, Deserialize, Clone)]
struct ShortieItem {
    #[serde(rename = "vkey")]
    vkey: String,
    #[serde(rename = "videoTitle")]
    video_title: String,
    #[serde(rename = "linkUrl")]
    link_url: String,
    #[serde(rename = "imageUrl")]
    image_url: Option<String>,
    #[serde(rename = "likeNumber", default)]
    like_number: u64,
    #[serde(rename = "dislikeNumber", default)]
    dislike_number: u64,
    #[serde(rename = "name")]
    name: Option<String>,
    #[serde(rename = "profileUrl")]
    profile_url: Option<String>,
    #[serde(rename = "entityId")]
    entity_id: Option<u64>,
    #[serde(rename = "entityType")]
    entity_type: Option<String>,
    #[serde(rename = "trackingTimeWatched")]
    tracking_time_watched: Option<TrackingTimeWatched>,
    #[serde(rename = "pillsData", default)]
    pills_data: Vec<PillData>,
    #[serde(rename = "badges")]
    badges: Option<Badges>,
}

#[derive(Debug, Deserialize, Clone)]
struct TrackingTimeWatched {
    #[serde(rename = "video_duration", default)]
    video_duration: u32,
}

#[derive(Debug, Deserialize, Clone)]
struct PillData {
    #[serde(rename = "name")]
    name: String,
}

#[derive(Debug, Deserialize, Clone)]
struct Badges {
    #[serde(rename = "verified", default)]
    verified: bool,
}

impl PornhubShortiesProvider {
    pub fn new() -> Self {
        Self {
            url: BASE_URL.to_string(),
        }
    }

    fn build_target(sort: &str, query: Option<&str>) -> Target {
        let sort = Self::normalize_sort(sort).to_string();
        match query.map(str::trim).filter(|q| !q.is_empty()) {
            Some(q) => Target::Search {
                query: q.to_string(),
                sort,
            },
            None => Target::Home { sort },
        }
    }

    fn normalize_sort(sort: &str) -> &'static str {
        match sort.trim().to_ascii_lowercase().as_str() {
            "trending" => "trending",
            "popular" | "mv" | "mostviewed" => "mostviewed",
            "top_rated" | "tr" | "toprated" => "top_rated",
            "hottest" | "ht" => "hottest",
            _ => "new",
        }
    }

    fn build_url(base: &str, target: &Target, page: u8) -> String {
        match target {
            Target::Home { sort } => {
                if sort == "new" {
                    if page <= 1 {
                        format!("{base}/shorties")
                    } else {
                        format!("{base}/shorties?page={page}")
                    }
                } else {
                    if page <= 1 {
                        format!("{base}/shorties?sort={sort}")
                    } else {
                        format!("{base}/shorties?sort={sort}&page={page}")
                    }
                }
            }
            Target::Search { query, sort } => {
                let encoded = query.replace(' ', "+");
                if sort == "new" {
                    if page <= 1 {
                        format!("{base}/shorties?search={encoded}")
                    } else {
                        format!("{base}/shorties?search={encoded}&page={page}")
                    }
                } else {
                    if page <= 1 {
                        format!("{base}/shorties?search={encoded}&sort={sort}")
                    } else {
                        format!("{base}/shorties?search={encoded}&sort={sort}&page={page}")
                    }
                }
            }
        }
    }

    fn extract_json_shorties(html: &str) -> Result<Vec<ShortieItem>> {
        // The page embeds: JSON_SHORTIES = insertAfterNthPosition([{...}, ...], AD_POSITION, ...)
        // We locate the array by finding the '[' after JSON_SHORTIES and matching brackets.
        let marker = "JSON_SHORTIES";
        let start = html.find(marker).ok_or_else(|| {
            Error::from(ErrorKind::Parse(
                "JSON_SHORTIES marker not found in HTML".to_string(),
            ))
        })?;

        let arr_start = html[start..].find('[').ok_or_else(|| {
            Error::from(ErrorKind::Parse(
                "JSON_SHORTIES array open bracket not found".to_string(),
            ))
        })? + start;

        // Walk the HTML to find the matching closing bracket.
        let bytes = html.as_bytes();
        let mut depth: i32 = 0;
        let mut in_string = false;
        let mut escape_next = false;
        let mut arr_end = arr_start;

        for (offset, &b) in bytes[arr_start..].iter().enumerate() {
            if escape_next {
                escape_next = false;
                continue;
            }
            if b == b'\\' && in_string {
                escape_next = true;
                continue;
            }
            if b == b'"' {
                in_string = !in_string;
                continue;
            }
            if in_string {
                continue;
            }
            match b {
                b'[' => depth += 1,
                b']' => {
                    depth -= 1;
                    if depth == 0 {
                        arr_end = arr_start + offset;
                        break;
                    }
                }
                _ => {}
            }
        }

        if depth != 0 {
            return Err(
                ErrorKind::Parse("JSON_SHORTIES array bracket mismatch".to_string()).into(),
            );
        }

        let raw = &html[arr_start..=arr_end];
        let items: Vec<ShortieItem> = serde_json::from_str(raw).map_err(|e| {
            Error::from(ErrorKind::Parse(format!("JSON_SHORTIES parse error: {e}")))
        })?;

        Ok(items)
    }

    fn item_to_video(item: ShortieItem) -> Option<VideoItem> {
        let id = item.vkey;
        if id.is_empty() {
            return None;
        }
        let title = item.video_title.trim().to_string();
        if title.is_empty() {
            return None;
        }
        let url = item.link_url.trim().to_string();
        if url.is_empty() || !url.contains("/view_video.php") {
            return None;
        }

        let thumb = item
            .image_url
            .unwrap_or_default()
            .replace("\\/", "/")
            .trim()
            .to_string();

        let duration = item
            .tracking_time_watched
            .map(|t| t.video_duration)
            .unwrap_or(0);

        let mut video = VideoItem::new(
            id,
            title,
            url,
            CHANNEL_ID.to_string(),
            thumb,
            duration,
        );

        // Rating from likes / (likes + dislikes)
        let total = item.like_number + item.dislike_number;
        if total > 0 {
            video.rating = Some((item.like_number as f32 / total as f32) * 100.0);
        }

        // Uploader
        if let Some(name) = item.name.filter(|n| !n.is_empty()) {
            video.uploader = Some(name.clone());
            if let Some(profile_url) = item
                .profile_url
                .map(|u| u.replace("\\/", "/"))
                .filter(|u| !u.is_empty())
            {
                video.uploaderUrl = Some(profile_url.clone());
                // Build namespaced uploader ID from entity_type + entity_id
                if let (Some(et), Some(eid)) = (item.entity_type.as_deref(), item.entity_id) {
                    let kind = match et {
                        "Mpp" | "Model" => "model",
                        "Channel" => "channels",
                        "Pornstar" => "pornstar",
                        _ => "model",
                    };
                    video.uploaderId = Some(format!("{CHANNEL_ID}:{kind}:{eid}"));
                }
            }
        }

        // Tags from pillsData
        let tags: Vec<String> = item.pills_data.into_iter().map(|p| p.name).collect();
        if !tags.is_empty() {
            video.tags = Some(tags);
        }

        // Verified badge
        video.verified = item.badges.map(|b| b.verified).filter(|&v| v);

        Some(video)
    }

    fn build_channel(&self, _clientversion: ClientVersion) -> Channel {
        Channel {
            id: CHANNEL_ID.to_string(),
            name: "PH Shorties".to_string(),
            description: "Pornhub Shorties — vertical short-form porn clips.".to_string(),
            premium: false,
            favicon: "https://www.google.com/s2/favicons?sz=64&domain=pornhub.com".to_string(),
            status: "active".to_string(),
            categories: vec![],
            options: vec![ChannelOption {
                id: "sort".to_string(),
                title: "Sort".to_string(),
                description: "Browse Pornhub Shorties by sort order.".to_string(),
                systemImage: "list.number".to_string(),
                colorName: "orange".to_string(),
                options: vec![
                    FilterOption {
                        id: "new".to_string(),
                        title: "New".to_string(),
                    },
                    FilterOption {
                        id: "trending".to_string(),
                        title: "Trending".to_string(),
                    },
                    FilterOption {
                        id: "mostviewed".to_string(),
                        title: "Most Viewed".to_string(),
                    },
                    FilterOption {
                        id: "top_rated".to_string(),
                        title: "Top Rated".to_string(),
                    },
                    FilterOption {
                        id: "hottest".to_string(),
                        title: "Hottest".to_string(),
                    },
                ],
                multiSelect: false,
            }],
            nsfw: true,
            cacheDuration: Some(1800),
            ytdlpCommand: Some("yt-dlp --impersonate chrome-120".to_string())
        }
    }

    async fn fetch_videos(
        &self,
        cache: VideoCache,
        page: u8,
        sort: &str,
        query: Option<&str>,
        options: ServerOptions,
    ) -> Result<Vec<VideoItem>> {
        let target = Self::build_target(sort, query);
        let fetch_url = Self::build_url(&self.url, &target, page);

        // Cache hit
        let old_items = match cache.get(&fetch_url) {
            Some((time, items)) if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 => {
                return Ok(items.clone());
            }
            Some((_, items)) => items.clone(),
            None => vec![],
        };

        let mut requester = requester_or_default(&options, CHANNEL_ID, "fetch_videos");

        let text = match requester
            .get_with_headers(
                &fetch_url,
                vec![
                    ("Referer".to_string(), format!("{}/shorties", self.url)),
                    (
                        "Accept".to_string(),
                        "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
                            .to_string(),
                    ),
                    ("Accept-Language".to_string(), "en-US,en;q=0.9".to_string()),
                ],
                None,
            )
            .await
        {
            Ok(text) => text,
            Err(error) => {
                report_provider_error(
                    CHANNEL_ID,
                    "fetch_videos.request",
                    &format!("url={fetch_url}; error={error}"),
                )
                .await;
                return Ok(old_items);
            }
        };

        let shorties = match Self::extract_json_shorties(&text) {
            Ok(items) => items,
            Err(error) => {
                report_provider_error(
                    CHANNEL_ID,
                    "fetch_videos.parse",
                    &format!("url={fetch_url}; error={error}"),
                )
                .await;
                return Ok(old_items);
            }
        };

        let items: Vec<VideoItem> = shorties
            .into_iter()
            .filter_map(Self::item_to_video)
            .collect();

        if items.is_empty() {
            return Ok(old_items);
        }

        cache.remove(&fetch_url);
        cache.insert(fetch_url, items.clone());
        Ok(items)
    }
}

#[async_trait]
impl Provider for PornhubShortiesProvider {
    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::<u8>().unwrap_or(1);

        match self
            .fetch_videos(cache, page, &sort, query.as_deref(), options)
            .await
        {
            Ok(items) => items,
            Err(error) => {
                report_provider_error(CHANNEL_ID, "get_videos", &error.to_string()).await;
                vec![]
            }
        }
    }

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

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

    #[test]
    fn parses_json_shorties_from_html() {
        let html = r#"
            var JSON_SHORTIES = insertAfterNthPosition([
                {
                    "videoId": 123456,
                    "vkey": "abcdef1234567",
                    "videoTitle": "Test Short Video",
                    "favoriteInfo": "1K",
                    "likeInfo": "2K",
                    "likeNumber": 2000,
                    "dislikeNumber": 100,
                    "isHD": true,
                    "linkUrl": "https:\/\/www.pornhub.com\/view_video.php?viewkey=abcdef1234567",
                    "shortieUrl": "https:\/\/www.pornhub.com\/shorties\/abcdef1234567",
                    "embedUrl": "",
                    "imageUrl": "https:\/\/example.com\/thumb.jpg",
                    "mediaPriority": "hls",
                    "mediaDefinitions": [],
                    "isFavorite": false,
                    "isReported": false,
                    "isSubscribed": false,
                    "userVoteDetail": -1,
                    "trackingTimeWatched": {"video_duration": 45},
                    "pillsData": [{"name": "Amateur", "slug": "amateur", "type": "category"}],
                    "badges": {"verified": true, "premium": false, "award": false},
                    "name": "TestUser",
                    "profileUrl": "https:\/\/www.pornhub.com\/model\/testuser",
                    "entityType": "Mpp",
                    "entityId": 9876543
                }
            ], AD_POSITION, {});
        "#;

        let items = PornhubShortiesProvider::extract_json_shorties(html)
            .expect("should parse JSON_SHORTIES");
        assert_eq!(items.len(), 1);
        assert_eq!(items[0].vkey, "abcdef1234567");
        assert_eq!(items[0].video_title, "Test Short Video");
        assert_eq!(
            items[0].link_url,
            "https://www.pornhub.com/view_video.php?viewkey=abcdef1234567"
        );
        assert_eq!(items[0].like_number, 2000);
        assert_eq!(items[0].dislike_number, 100);
        assert_eq!(
            items[0]
                .tracking_time_watched
                .as_ref()
                .map(|t| t.video_duration),
            Some(45)
        );
        assert_eq!(items[0].pills_data[0].name, "Amateur");
        assert_eq!(items[0].name.as_deref(), Some("TestUser"));

        let video = PornhubShortiesProvider::item_to_video(items[0].clone())
            .expect("should convert to VideoItem");
        assert_eq!(video.id, "abcdef1234567");
        assert_eq!(video.duration, 45);
        assert!(video.rating.is_some());
        assert!((video.rating.unwrap() - 95.23).abs() < 0.1);
        assert_eq!(video.uploader.as_deref(), Some("TestUser"));
        assert!(video.tags.as_ref().is_some_and(|t| t.contains(&"Amateur".to_string())));
    }

    #[test]
    fn build_url_home_new() {
        let url = PornhubShortiesProvider::build_url(
            BASE_URL,
            &Target::Home {
                sort: "new".to_string(),
            },
            1,
        );
        assert_eq!(url, "https://www.pornhub.com/shorties");

        let url2 = PornhubShortiesProvider::build_url(
            BASE_URL,
            &Target::Home {
                sort: "new".to_string(),
            },
            2,
        );
        assert_eq!(url2, "https://www.pornhub.com/shorties?page=2");
    }

    #[test]
    fn build_url_search() {
        let url = PornhubShortiesProvider::build_url(
            BASE_URL,
            &Target::Search {
                query: "teen".to_string(),
                sort: "new".to_string(),
            },
            1,
        );
        assert_eq!(url, "https://www.pornhub.com/shorties?search=teen");
    }

    #[test]
    fn normalize_sort_variants() {
        assert_eq!(PornhubShortiesProvider::normalize_sort("new"), "new");
        assert_eq!(PornhubShortiesProvider::normalize_sort("trending"), "trending");
        assert_eq!(PornhubShortiesProvider::normalize_sort("popular"), "mostviewed");
        assert_eq!(PornhubShortiesProvider::normalize_sort("unknown"), "new");
    }
}
