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::util::time::parse_time_to_seconds;
use crate::videos::{ServerOptions, VideoFormat, VideoItem};
use async_trait::async_trait;
use error_chain::error_chain;
use scraper::{Html, Selector};

pub const CHANNEL_METADATA: crate::providers::ProviderChannelMetadata =
    crate::providers::ProviderChannelMetadata {
        group_id: "mainstream-tube",
        tags: &["full length", "hd", "free", "mainstream"],
    };

const BASE_URL: &str = "https://fullporner.com";
const MEDIA_HOST: &str = "https://xiaoshenke.net";
const CHANNEL_ID: &str = "fullporner";
const DEFAULT_PER_PAGE: usize = 32;
const BROWSER_UA: &str =
    "Mozilla/5.0 (X11; Linux x86_64; rv:146.0) Gecko/20100101 Firefox/146.0";

error_chain! {
    foreign_links {
        Io(std::io::Error);
        Url(url::ParseError);
    }
    errors {
        Parse(msg: String) {
            description("parse error")
            display("parse error: {}", msg)
        }
    }
}

#[derive(Debug, Clone)]
pub struct FullpornerProvider;

#[derive(Debug, Clone)]
enum Target {
    Latest { page: u32 },
    Search { query: String, page: u32 },
    Category { slug: String, page: u32 },
    Pornstar { slug: String, page: u32 },
}

#[derive(Debug, Clone)]
struct CardStub {
    slug: String,
    title: String,
    thumb: String,
    thumb_id: Option<String>,
    duration: u32,
    uploaded_at: Option<u64>,
}

impl FullpornerProvider {
    pub fn new() -> Self {
        Self
    }

    fn build_channel(&self, _clientversion: ClientVersion) -> Channel {
        Channel {
            id: CHANNEL_ID.to_string(),
            name: "FullPorner".to_string(),
            description: "Full-length free HD porn videos. Latest uploads, categories, and pornstar browsing.".to_string(),
            premium: false,
            favicon: "https://www.google.com/s2/favicons?sz=64&domain=fullporner.com".to_string(),
            status: "active".to_string(),
            categories: vec![],
            options: vec![
                ChannelOption {
                    id: "sort".to_string(),
                    title: "Sort".to_string(),
                    description: "Browse FullPorner by newest videos.".to_string(),
                    systemImage: "arrow.up.arrow.down".to_string(),
                    colorName: "blue".to_string(),
                    options: vec![
                        FilterOption {
                            id: "new".to_string(),
                            title: "Latest".to_string(),
                        },
                    ],
                    multiSelect: false,
                },
            ],
            nsfw: true,
            cacheDuration: Some(1800),
            ytdlpCommand: Some("yt-dlp --impersonate chrome-120".to_string())
        }
    }

    fn build_listing_url(target: &Target) -> String {
        match target {
            Target::Latest { page } => {
                format!("{BASE_URL}/home/{page}")
            }
            Target::Search { query, page } => {
                let encoded: String =
                    url::form_urlencoded::byte_serialize(query.as_bytes()).collect();
                format!("{BASE_URL}/search?q={encoded}&p={page}")
            }
            Target::Category { slug, page } => {
                if *page <= 1 {
                    format!("{BASE_URL}/category/{slug}")
                } else {
                    format!("{BASE_URL}/category/{slug}/{page}")
                }
            }
            Target::Pornstar { slug, page } => {
                if *page <= 1 {
                    format!("{BASE_URL}/pornstar/{slug}")
                } else {
                    format!("{BASE_URL}/pornstar/{slug}/{page}")
                }
            }
        }
    }

    fn pick_target(query: Option<&str>, page: u32) -> Target {
        if let Some(q) = query {
            let q = q.trim();
            if !q.is_empty() {
                if let Some(slug) = q.strip_prefix("cat:") {
                    return Target::Category {
                        slug: slug.trim().to_string(),
                        page,
                    };
                }
                if let Some(slug) = q.strip_prefix("category:") {
                    return Target::Category {
                        slug: slug.trim().to_string(),
                        page,
                    };
                }
                if let Some(slug) = q.strip_prefix("pornstar:") {
                    return Target::Pornstar {
                        slug: slug.trim().to_string(),
                        page,
                    };
                }
                if let Some(slug) = q.strip_prefix("star:") {
                    return Target::Pornstar {
                        slug: slug.trim().to_string(),
                        page,
                    };
                }
                return Target::Search {
                    query: q.to_string(),
                    page,
                };
            }
        }
        Target::Latest { page }
    }

    fn selector(value: &str) -> Result<Selector> {
        Selector::parse(value)
            .map_err(|e| Error::from(format!("selector `{value}` failed: {e}")))
    }

    // Extract the thumb_id from a thumbnail URL like
    // https://imgs.xiaoshenke.net/thumb/3195337.jpg  → "3195337"
    // https://imgs.xiaoshenke.net/thumb/6a0d75fb4182b.jpg → "6a0d75fb4182b"
    fn extract_thumb_id(thumb_url: &str) -> Option<String> {
        let path = thumb_url.split('/').last()?;
        if let Some(stem) = path.strip_suffix(".jpg") {
            if stem.chars().all(|c| c.is_ascii_alphanumeric()) && !stem.is_empty() {
                return Some(stem.to_string());
            }
        }
        None
    }

    fn parse_listing_page(html: &str) -> Result<Vec<CardStub>> {
        let document = Html::parse_document(html);
        let card_sel = Self::selector(".video-card")?;
        let img_sel = Self::selector("img[data-src]")?;
        let time_sel = Self::selector(".time")?;
        let title_sel = Self::selector(".video-title a")?;
        let create_sel = Self::selector(".create")?;
        let link_sel = Self::selector("a[href]")?;

        let mut stubs = Vec::new();
        let mut seen = std::collections::HashSet::new();

        for card in document.select(&card_sel) {
            // Derive the watch URL slug from the first link to /watch/
            let slug = card
                .select(&link_sel)
                .find_map(|a| {
                    let href = a.value().attr("href")?;
                    href.strip_prefix("/watch/").map(|s| s.to_string())
                })
                .unwrap_or_default();

            if slug.is_empty() || !seen.insert(slug.clone()) {
                continue;
            }

            let img_el = card.select(&img_sel).next();
            let thumb = img_el
                .and_then(|img| img.value().attr("data-src"))
                .map(|s| {
                    if s.starts_with("//") {
                        format!("https:{s}")
                    } else {
                        s.to_string()
                    }
                })
                .unwrap_or_default();

            // Title: prefer .video-title a text, fall back to img alt
            let title = card
                .select(&title_sel)
                .next()
                .map(|el| el.text().collect::<Vec<_>>().join("").trim().to_string())
                .filter(|t| !t.is_empty())
                .or_else(|| {
                    img_el
                        .and_then(|img| img.value().attr("alt"))
                        .map(str::to_string)
                        .filter(|t| !t.is_empty())
                })
                .unwrap_or_else(|| slug.clone());

            let duration_text = card
                .select(&time_sel)
                .next()
                .map(|el| el.text().collect::<Vec<_>>().join("").trim().to_string())
                .unwrap_or_default();
            let duration = parse_time_to_seconds(&duration_text)
                .and_then(|s| u32::try_from(s).ok())
                .unwrap_or(0);

            let uploaded_at = card
                .select(&create_sel)
                .next()
                .and_then(|el| {
                    el.text()
                        .collect::<Vec<_>>()
                        .join("")
                        .trim()
                        .parse::<u64>()
                        .ok()
                });

            let thumb_id = Self::extract_thumb_id(&thumb);

            stubs.push(CardStub {
                slug,
                title,
                thumb,
                thumb_id,
                duration,
                uploaded_at,
            });
        }

        Ok(stubs)
    }

    fn build_format(thumb_id: &str) -> VideoFormat {
        let url = format!("{MEDIA_HOST}/vid/{thumb_id}/720");
        let mut fmt = VideoFormat::new(url, "720p".to_string(), "mp4".to_string());
        fmt.add_http_header("Referer".to_string(), format!("{MEDIA_HOST}/"));
        fmt.add_http_header("User-Agent".to_string(), BROWSER_UA.to_string());
        fmt
    }

    fn stub_to_item(stub: CardStub) -> VideoItem {
        let url = format!("{BASE_URL}/watch/{}", stub.slug);
        let formats = stub.thumb_id.as_deref().map(|id| vec![Self::build_format(id)]);

        let mut item = VideoItem::new(
            stub.slug,
            stub.title,
            url,
            CHANNEL_ID.to_string(),
            stub.thumb,
            stub.duration,
        );
        item.formats = formats;
        item.uploadedAt = stub.uploaded_at;
        item.aspectRatio = Some(16.0 / 9.0);
        item
    }

    async fn fetch_page(
        target: &Target,
        per_page: usize,
        options: &ServerOptions,
    ) -> Result<Vec<VideoItem>> {
        let url = Self::build_listing_url(target);
        let mut requester = requester_or_default(options, CHANNEL_ID, "fetch_page");
        let html = requester
            .get(&url, None)
            .await
            .map_err(|e| Error::from(format!("fetch failed for {url}: {e}")))?;

        let stubs = Self::parse_listing_page(&html)?;
        let items = stubs
            .into_iter()
            .take(per_page)
            .map(Self::stub_to_item)
            .collect();
        Ok(items)
    }
}

#[async_trait]
impl Provider for FullpornerProvider {
    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::<u32>().unwrap_or(1).max(1);
        let per_page = per_page
            .parse::<usize>()
            .unwrap_or(DEFAULT_PER_PAGE)
            .clamp(1, 48);

        let normalized_query = query
            .as_deref()
            .map(str::trim)
            .filter(|q| !q.is_empty())
            .map(ToOwned::to_owned);

        let target = Self::pick_target(normalized_query.as_deref(), page);

        match Self::fetch_page(&target, per_page, &options).await {
            Ok(items) => items,
            Err(e) => {
                report_provider_error(CHANNEL_ID, "get_videos", &e.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 builds_latest_url() {
        let t = Target::Latest { page: 1 };
        assert_eq!(
            FullpornerProvider::build_listing_url(&t),
            "https://fullporner.com/home/1"
        );
    }

    #[test]
    fn builds_search_url() {
        let t = Target::Search {
            query: "teen".to_string(),
            page: 2,
        };
        assert_eq!(
            FullpornerProvider::build_listing_url(&t),
            "https://fullporner.com/search?q=teen&p=2"
        );
    }

    #[test]
    fn builds_category_url_page1() {
        let t = Target::Category {
            slug: "anal".to_string(),
            page: 1,
        };
        assert_eq!(
            FullpornerProvider::build_listing_url(&t),
            "https://fullporner.com/category/anal"
        );
    }

    #[test]
    fn builds_category_url_page2() {
        let t = Target::Category {
            slug: "anal".to_string(),
            page: 2,
        };
        assert_eq!(
            FullpornerProvider::build_listing_url(&t),
            "https://fullporner.com/category/anal/2"
        );
    }

    #[test]
    fn builds_pornstar_url() {
        let t = Target::Pornstar {
            slug: "mia-malkova".to_string(),
            page: 1,
        };
        assert_eq!(
            FullpornerProvider::build_listing_url(&t),
            "https://fullporner.com/pornstar/mia-malkova"
        );
    }

    #[test]
    fn extracts_numeric_thumb_id() {
        assert_eq!(
            FullpornerProvider::extract_thumb_id(
                "https://imgs.xiaoshenke.net/thumb/3195337.jpg"
            ),
            Some("3195337".to_string())
        );
    }

    #[test]
    fn extracts_hex_thumb_id() {
        assert_eq!(
            FullpornerProvider::extract_thumb_id(
                "https://imgs.xiaoshenke.net/thumb/6a0d75fb4182b.jpg"
            ),
            Some("6a0d75fb4182b".to_string())
        );
    }

    #[test]
    fn rejects_old_thumb_format() {
        // /imgs/28/54/ff64a89399f570b_main.jpg — underscore in name, different path
        assert_eq!(
            FullpornerProvider::extract_thumb_id(
                "https://imgs.xiaoshenke.net/imgs/28/54/ff64a89399f570b_main.jpg"
            ),
            None
        );
    }

    #[test]
    fn picks_cat_target_from_query_prefix() {
        match FullpornerProvider::pick_target(Some("cat:anal"), 1) {
            Target::Category { slug, page } => {
                assert_eq!(slug, "anal");
                assert_eq!(page, 1);
            }
            other => panic!("expected Category, got {:?}", other),
        }
    }

    #[test]
    fn picks_pornstar_target_from_query_prefix() {
        match FullpornerProvider::pick_target(Some("pornstar:mia-malkova"), 1) {
            Target::Pornstar { slug, page } => {
                assert_eq!(slug, "mia-malkova");
                assert_eq!(page, 1);
            }
            other => panic!("expected Pornstar, got {:?}", other),
        }
    }

    #[test]
    fn parse_listing_page_extracts_cards() {
        let html = r#"<!DOCTYPE html><html><body>
            <div class="video-card">
                <div class="video-card-image">
                    <a class="popout" href="/watch/abc123">
                        <img src="" class="lazy" data-src="https://imgs.xiaoshenke.net/thumb/9876543.jpg" alt="Test Video Title">
                    </a>
                    <div class="time">01:30:00</div>
                </div>
                <div class="video-card-body">
                    <div class="video-title">
                        <a href="/watch/abc123">Test Video Title</a>
                    </div>
                    <div class="video-view">
                        <span class="create">1779289876</span>
                    </div>
                </div>
            </div>
        </body></html>"#;

        let stubs = FullpornerProvider::parse_listing_page(html).unwrap();
        assert_eq!(stubs.len(), 1);
        let s = &stubs[0];
        assert_eq!(s.slug, "abc123");
        assert_eq!(s.title, "Test Video Title");
        assert_eq!(s.duration, 5400);
        assert_eq!(s.thumb_id.as_deref(), Some("9876543"));
        assert_eq!(s.uploaded_at, Some(1779289876));
    }
}
