use crate::DbPool;
use crate::api::ClientVersion;
use crate::providers::{Provider, report_provider_error, requester_or_default};
use crate::status::*;
use crate::uploaders::{UploaderChannelStat, UploaderLayoutRow, UploaderProfile, UploaderVideoRef};
use crate::util::cache::VideoCache;
use crate::videos::{ServerOptions, VideoItem};
use async_trait::async_trait;
use error_chain::error_chain;
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
use serde::Deserialize;
use std::sync::Arc;
use tokio::sync::RwLock;

pub const CHANNEL_METADATA: crate::providers::ProviderChannelMetadata =
    crate::providers::ProviderChannelMetadata {
        group_id: "tiktok",
        tags: &["amateur", "gifs", "creators"],
    };

const BASE_URL: &str = "https://www.redgifs.com";
const API_ROOT: &str = "https://api.redgifs.com";
const CHANNEL_ID: &str = "redgifs";
const DEFAULT_PER_PAGE: usize = 20;
const MAX_PER_PAGE: usize = 30;

// RedGifs' own trending feed only exposes two curated variations (matching the
// site's own "Trending" toggle). Its keyword/tag search endpoint separately
// takes an `order` enum (top, top7, top28, latest, score, trending); we reuse
// the same two-way sort selector for both surfaces rather than exposing every
// combination, mapping each to the closest search order semantically.
const CURATED_CATEGORIES: &[&str] = &[
    "Amateur",
    "Homemade",
    "Solo",
    "Lesbian",
    "Anal",
    "Blowjob",
    "Cumshot",
    "Big Tits",
    "Teen (18+)",
    "MILF",
    "Asian",
    "Ebony",
    "Latina",
    "Redhead",
    "Public",
    "Threesome",
    "Squirting",
    "Creampie",
    "Pussy",
    "Ass",
];

error_chain! {
    foreign_links {
        Json(serde_json::Error);
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Sort {
    Popular,
    Established,
}

impl Sort {
    fn from_sort_id(value: &str) -> Self {
        match value.trim().to_ascii_lowercase().as_str() {
            "established" => Sort::Established,
            _ => Sort::Popular,
        }
    }

    fn trending_variation(self) -> &'static str {
        match self {
            Sort::Popular => "popular",
            Sort::Established => "established",
        }
    }

    fn search_order(self) -> &'static str {
        match self {
            Sort::Popular => "trending",
            Sort::Established => "top",
        }
    }
}

#[derive(Debug, Clone)]
enum Target {
    Trending(Sort),
    Tag(String, Sort),
    Query(String, Sort),
    Creator(String),
}

#[derive(Debug, Deserialize, Clone, Default)]
#[serde(rename_all = "camelCase")]
struct Gif {
    #[serde(default)]
    id: String,
    #[serde(default)]
    create_date: i64,
    #[serde(default)]
    duration: f64,
    #[serde(default)]
    views: u32,
    #[serde(default)]
    width: u32,
    #[serde(default)]
    height: u32,
    #[serde(default)]
    tags: Vec<String>,
    #[serde(default)]
    user_name: String,
    #[serde(default, rename = "type")]
    kind: i32,
    #[serde(default)]
    description: Option<String>,
    #[serde(default)]
    urls: GifUrls,
}

#[derive(Debug, Deserialize, Clone, Default)]
struct GifUrls {
    #[serde(default)]
    poster: Option<String>,
    #[serde(default)]
    thumbnail: Option<String>,
}

#[derive(Debug, Deserialize, Clone, Default)]
struct SearchResponse {
    #[serde(default)]
    gifs: Vec<Gif>,
}

#[derive(Debug, Deserialize, Clone, Default)]
struct TokenResponse {
    #[serde(default)]
    token: String,
}

#[derive(Debug, Deserialize, Clone, Default)]
#[serde(rename_all = "camelCase")]
struct UserProfileResp {
    #[serde(default)]
    username: String,
    #[serde(default)]
    description: Option<String>,
    #[serde(default)]
    followers: u64,
    #[serde(default)]
    gifs: u64,
    #[serde(default)]
    profile_image_url: Option<String>,
    #[serde(default)]
    verified: bool,
}

#[derive(Debug, Clone)]
pub struct RedgifsProvider {
    token: Arc<RwLock<Option<String>>>,
}

impl RedgifsProvider {
    pub fn new() -> Self {
        Self {
            token: Arc::new(RwLock::new(None)),
        }
    }

    fn build_channel(&self, _clientversion: ClientVersion) -> Channel {
        let category_titles = CURATED_CATEGORIES.iter().map(|name| name.to_string()).collect::<Vec<_>>();
        let category_options = CURATED_CATEGORIES
            .iter()
            .map(|name| FilterOption {
                id: name.to_string(),
                title: name.to_string(),
            })
            .collect::<Vec<_>>();

        Channel {
            id: CHANNEL_ID.to_string(),
            name: "RedGifs".to_string(),
            description: "RedGifs amateur clip feed with trending/established sort, tag and creator browsing, and keyword search.".to_string(),
            premium: false,
            favicon: "https://www.google.com/s2/favicons?sz=64&domain=redgifs.com".to_string(),
            status: "active".to_string(),
            categories: category_titles,
            options: vec![
                ChannelOption {
                    id: "sort".to_string(),
                    title: "Sort".to_string(),
                    description: "Browse RedGifs' trending feed, popular or established creators.".to_string(),
                    systemImage: "arrow.up.arrow.down".to_string(),
                    colorName: "blue".to_string(),
                    options: vec![
                        FilterOption {
                            id: "popular".to_string(),
                            title: "Popular".to_string(),
                        },
                        FilterOption {
                            id: "established".to_string(),
                            title: "Established Creators".to_string(),
                        },
                    ],
                    multiSelect: false,
                },
                ChannelOption {
                    id: "categories".to_string(),
                    title: "Tags".to_string(),
                    description: "Open a popular RedGifs tag feed directly.".to_string(),
                    systemImage: "tag".to_string(),
                    colorName: "orange".to_string(),
                    options: category_options,
                    multiSelect: false,
                },
            ],
            nsfw: true,
            cacheDuration: Some(1800),
            ytdlpCommand: Some("yt-dlp --impersonate chrome-120".to_string())
        }
    }

    fn resolve_query_target(query: &str, sort: Sort) -> Option<Target> {
        let trimmed = query.trim();
        let (kind, value) = trimmed.split_once(':')?;
        let value = value.trim();
        if value.is_empty() {
            return None;
        }
        match kind.trim().to_ascii_lowercase().as_str() {
            "tag" | "category" | "cat" => Some(Target::Tag(value.to_string(), sort)),
            "user" | "uploader" | "creator" => Some(Target::Creator(value.to_string())),
            _ => None,
        }
    }

    fn pick_target(query: Option<&str>, sort: Sort, options: &ServerOptions) -> Target {
        if let Some(query) = query {
            if !query.trim().is_empty() {
                if let Some(target) = Self::resolve_query_target(query, sort) {
                    return target;
                }
                return Target::Query(query.trim().to_string(), sort);
            }
        }

        if let Some(category) = options.categories.as_deref() {
            let category = category.trim();
            if !category.is_empty() && category != "all" {
                return Target::Tag(category.to_string(), sort);
            }
        }

        Target::Trending(sort)
    }

    async fn redgifs_token(&self, options: &ServerOptions, force_refresh: bool) -> Result<String> {
        if !force_refresh {
            if let Some(token) = self.token.read().await.clone() {
                return Ok(token);
            }
        }

        let mut guard = self.token.write().await;
        if !force_refresh {
            if let Some(token) = guard.clone() {
                return Ok(token);
            }
        }

        let url = format!("{API_ROOT}/v2/auth/temporary");
        let mut requester = requester_or_default(options, CHANNEL_ID, "redgifs_token");
        let text = requester
            .get_with_headers(&url, vec![("Referer".to_string(), format!("{BASE_URL}/"))], None)
            .await
            .map_err(|error| Error::from(format!("request failed for {url}: {error}")))?;
        let parsed: TokenResponse = serde_json::from_str(&text)?;
        if parsed.token.is_empty() {
            return Err(Error::from(format!("empty redgifs token response for {url}")));
        }
        *guard = Some(parsed.token.clone());
        Ok(parsed.token)
    }

    async fn authed_get(&self, url: &str, options: &ServerOptions) -> Result<String> {
        const ATTEMPTS: u32 = 3;
        let mut force_refresh = false;
        for attempt in 0..ATTEMPTS {
            let token = self.redgifs_token(options, force_refresh).await?;
            force_refresh = false;
            let mut requester = requester_or_default(options, CHANNEL_ID, "authed_get");
            let response = requester
                .get_raw_with_headers(
                    url,
                    vec![
                        ("Authorization".to_string(), format!("Bearer {token}")),
                        ("Referer".to_string(), format!("{BASE_URL}/")),
                    ],
                )
                .await
                .map_err(|error| Error::from(format!("request failed for {url}: {error}")))?;

            let status = response.status().as_u16();
            let last_attempt = attempt + 1 == ATTEMPTS;
            crate::flow_debug!(
                "trace=authed_get url={} attempt={} status={}",
                crate::util::flow_debug::preview(url, 160),
                attempt + 1,
                status
            );
            if status == 401 && !last_attempt {
                force_refresh = true;
                continue;
            }
            // RedGifs rate-limits bursty search/browse traffic with 429s; retry
            // the same token after a short backoff instead of surfacing an
            // empty result set to the client.
            if status == 429 && !last_attempt {
                tokio::time::sleep(std::time::Duration::from_secs(1)).await;
                continue;
            }
            if !response.status().is_success() {
                return Err(Error::from(format!("redgifs request failed for {url}: status={}", response.status())));
            }
            return response
                .text()
                .await
                .map_err(|error| Error::from(format!("response read failed for {url}: {error}")));
        }
        Err(Error::from(format!("redgifs request failed for {url}: exhausted retries")))
    }

    async fn fetch_trending(&self, sort: Sort, page: u16, per_page: usize, options: &ServerOptions) -> Result<Vec<Gif>> {
        let url = format!(
            "{API_ROOT}/v2/feeds/trending/{}?page={}&count={}",
            sort.trending_variation(),
            page,
            per_page
        );
        let text = self.authed_get(&url, options).await?;
        let parsed: SearchResponse = serde_json::from_str(&text)?;
        Ok(parsed.gifs)
    }

    async fn fetch_tag(&self, tag: &str, sort: Sort, page: u16, per_page: usize, options: &ServerOptions) -> Result<Vec<Gif>> {
        let encoded = utf8_percent_encode(tag, NON_ALPHANUMERIC).to_string();
        let url = format!(
            "{API_ROOT}/v2/gifs/search?type=g&tags={encoded}&order={}&page={page}&count={per_page}",
            sort.search_order()
        );
        let text = self.authed_get(&url, options).await?;
        let parsed: SearchResponse = serde_json::from_str(&text)?;
        Ok(parsed.gifs)
    }

    async fn fetch_query(&self, text_query: &str, sort: Sort, page: u16, per_page: usize, options: &ServerOptions) -> Result<Vec<Gif>> {
        let encoded = utf8_percent_encode(text_query, NON_ALPHANUMERIC).to_string();
        let url = format!(
            "{API_ROOT}/v2/gifs/search?type=g&query={encoded}&order={}&page={page}&count={per_page}",
            sort.search_order()
        );
        let text = self.authed_get(&url, options).await?;
        let parsed: SearchResponse = serde_json::from_str(&text)?;
        Ok(parsed.gifs)
    }

    async fn fetch_creator(&self, username: &str, page: u16, per_page: usize, options: &ServerOptions) -> Result<Vec<Gif>> {
        let encoded = utf8_percent_encode(username, NON_ALPHANUMERIC).to_string();
        let url = format!("{API_ROOT}/v2/users/{encoded}/search?page={page}&count={per_page}");
        let text = self.authed_get(&url, options).await?;
        let parsed: SearchResponse = serde_json::from_str(&text)?;
        Ok(parsed.gifs)
    }

    async fn fetch_target_gifs(&self, target: &Target, page: u16, per_page: usize, options: &ServerOptions) -> Result<Vec<Gif>> {
        match target {
            Target::Trending(sort) => self.fetch_trending(*sort, page, per_page, options).await,
            Target::Tag(name, sort) => self.fetch_tag(name, *sort, page, per_page, options).await,
            Target::Query(text, sort) => self.fetch_query(text, *sort, page, per_page, options).await,
            Target::Creator(name) => self.fetch_creator(name, page, per_page, options).await,
        }
    }

    // RedGifs' listing payloads already carry duration, poster, tags, and
    // uploader for every item, so unlike xxxtik (which reposts links and must
    // resolve each one against the RedGifs API individually) no per-item
    // network call or bounded-concurrency enrichment step is needed here.
    fn build_video_item(gif: Gif) -> Option<VideoItem> {
        // type 2 is a static image/gallery post with no video source.
        if gif.kind != 1 || gif.id.is_empty() {
            return None;
        }

        let thumb = gif.urls.poster.clone().or_else(|| gif.urls.thumbnail.clone())?;

        let title = gif
            .description
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(ToOwned::to_owned)
            .unwrap_or_else(|| {
                if !gif.user_name.trim().is_empty() {
                    format!("RedGifs clip by {}", gif.user_name.trim())
                } else {
                    format!("RedGifs clip {}", gif.id)
                }
            });

        let url = format!("{BASE_URL}/watch/{}", gif.id);
        let duration = gif.duration.round().max(0.0) as u32;

        let mut item = VideoItem::new(gif.id.clone(), title, url, CHANNEL_ID.to_string(), thumb, duration);
        item.views = Some(gif.views);
        item.uploadedAt = (gif.create_date > 0).then_some(gif.create_date as u64);

        if !gif.user_name.trim().is_empty() {
            let username = gif.user_name.trim().to_string();
            item.uploader = Some(username.clone());
            item.uploaderUrl = Some(format!("{BASE_URL}/users/{username}"));
            item.uploaderId = Some(format!("{CHANNEL_ID}:{username}"));
        }

        let tags = gif
            .tags
            .iter()
            .map(|tag| tag.trim().to_string())
            .filter(|tag| !tag.is_empty())
            .collect::<Vec<_>>();
        item.tags = (!tags.is_empty()).then_some(tags);

        if gif.height > 0 {
            item.aspectRatio = Some(gif.width as f32 / gif.height as f32);
        }

        // video.url is the redgifs.com watch page, which yt-dlp resolves
        // natively; formats is intentionally left unset.
        Some(item)
    }

    async fn fetch_target_items(&self, target: Target, page: u16, per_page: usize, options: &ServerOptions) -> Result<Vec<VideoItem>> {
        let gifs = self.fetch_target_gifs(&target, page, per_page, options).await?;
        Ok(gifs.into_iter().filter_map(Self::build_video_item).collect())
    }

    async fn fetch_user_profile(&self, username: &str, options: &ServerOptions) -> Result<Option<UserProfileResp>> {
        let encoded = utf8_percent_encode(username, NON_ALPHANUMERIC).to_string();
        let url = format!("{API_ROOT}/v1/users/{encoded}");
        let text = match self.authed_get(&url, options).await {
            Ok(text) => text,
            Err(_) => return Ok(None),
        };
        match serde_json::from_str::<UserProfileResp>(&text) {
            Ok(profile) if !profile.username.trim().is_empty() => Ok(Some(profile)),
            _ => Ok(None),
        }
    }

    async fn build_uploader_profile(
        &self,
        username: &str,
        query: Option<&str>,
        profile_content: bool,
        options: &ServerOptions,
    ) -> Result<Option<UploaderProfile>> {
        let Some(profile) = self.fetch_user_profile(username, options).await? else {
            return Ok(None);
        };

        let canonical_id = format!("{CHANNEL_ID}:{}", profile.username);
        let video_count = profile.gifs;
        let mut videos = None;
        let mut total_views: u64 = 0;

        if profile_content {
            let items = self
                .fetch_target_items(Target::Creator(profile.username.clone()), 1, 24, options)
                .await?;

            let filtered_items = if let Some(query) = query.filter(|value| !value.trim().is_empty()) {
                let normalized_query = query.to_ascii_lowercase();
                items
                    .into_iter()
                    .filter(|item| {
                        let haystack = format!(
                            "{} {}",
                            item.title,
                            item.tags.as_ref().map(|values| values.join(" ")).unwrap_or_default()
                        )
                        .to_ascii_lowercase();
                        haystack.contains(&normalized_query)
                    })
                    .collect::<Vec<_>>()
            } else {
                items
            };

            total_views = filtered_items.iter().filter_map(|item| item.views).map(|v| v as u64).sum();

            let refs = filtered_items
                .iter()
                .map(|item| UploaderVideoRef::from_video_item(item, &profile.username, &canonical_id))
                .collect::<Vec<_>>();
            videos = Some(refs);
        }

        Ok(Some(UploaderProfile {
            id: canonical_id,
            name: profile.username.clone(),
            url: Some(format!("{BASE_URL}/users/{}", profile.username)),
            channel: Some(CHANNEL_ID.to_string()),
            verified: profile.verified,
            videoCount: video_count,
            totalViews: total_views,
            channels: Some(vec![UploaderChannelStat {
                channel: CHANNEL_ID.to_string(),
                videoCount: video_count,
                firstSeenAt: None,
                lastSeenAt: None,
            }]),
            avatar: profile.profile_image_url,
            description: profile.description.clone(),
            bio: profile.description,
            videos,
            tapes: Some(vec![]),
            playlists: Some(vec![]),
            layout: Some(vec![UploaderLayoutRow::videos(Some("Gifs".to_string()))]),
        }))
    }
}

#[async_trait]
impl Provider for RedgifsProvider {
    async fn get_videos(
        &self,
        cache: VideoCache,
        pool: DbPool,
        sort: String,
        query: Option<String>,
        page: String,
        per_page: String,
        options: ServerOptions,
    ) -> Vec<VideoItem> {
        let _ = cache;
        let _ = pool;

        let page = page.parse::<u16>().unwrap_or(1).max(1);
        let per_page = per_page
            .parse::<usize>()
            .unwrap_or(DEFAULT_PER_PAGE)
            .clamp(1, MAX_PER_PAGE);
        let normalized_query = query
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty());

        let sort = Sort::from_sort_id(&sort);
        let target = Self::pick_target(normalized_query, sort, &options);

        match self.fetch_target_items(target, page, per_page, &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))
    }

    async fn get_uploader(
        &self,
        cache: VideoCache,
        pool: DbPool,
        uploader_id: Option<String>,
        uploader_name: Option<String>,
        query: Option<String>,
        profile_content: bool,
        options: ServerOptions,
    ) -> std::result::Result<Option<UploaderProfile>, String> {
        let _ = cache;
        let _ = pool;

        let username = uploader_id
            .as_deref()
            .and_then(|id| id.strip_prefix(&format!("{CHANNEL_ID}:")))
            .map(ToOwned::to_owned)
            .or(uploader_name.clone())
            .map(|value| value.trim().to_string())
            .filter(|value| !value.is_empty());

        let Some(username) = username else {
            return Ok(None);
        };

        self.build_uploader_profile(&username, query.as_deref(), profile_content, &options)
            .await
            .map_err(|error| error.to_string())
    }
}

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

    #[test]
    fn resolves_query_shortcuts() {
        match RedgifsProvider::resolve_query_target("tag:Big Tits", Sort::Popular) {
            Some(Target::Tag(name, _)) => assert_eq!(name, "Big Tits"),
            other => panic!("expected tag target, got {other:?}"),
        }

        match RedgifsProvider::resolve_query_target("user:SomeCreator", Sort::Popular) {
            Some(Target::Creator(name)) => assert_eq!(name, "SomeCreator"),
            other => panic!("expected creator target, got {other:?}"),
        }

        assert!(RedgifsProvider::resolve_query_target("blonde teen", Sort::Popular).is_none());
    }

    fn empty_options() -> ServerOptions {
        ServerOptions {
            client_version: None,
            featured: None,
            category: None,
            sites: None,
            filter: None,
            language: None,
            public_url_base: None,
            requester: None,
            network: None,
            stars: None,
            categories: None,
            duration: None,
            sort: None,
            sexuality: None,
        }
    }

    #[test]
    fn falls_back_to_free_text_query() {
        match RedgifsProvider::pick_target(Some("blonde teen"), Sort::Popular, &empty_options()) {
            Target::Query(text, _) => assert_eq!(text, "blonde teen"),
            other => panic!("expected query target, got {other:?}"),
        }
    }

    #[test]
    fn maps_sort_ids() {
        assert_eq!(Sort::from_sort_id("popular"), Sort::Popular);
        assert_eq!(Sort::from_sort_id("established"), Sort::Established);
        assert_eq!(Sort::from_sort_id("unknown"), Sort::Popular);
    }

    #[test]
    fn skips_image_gallery_items() {
        let json = r#"{"id":"abc","type":2,"urls":{"poster":"https://media.redgifs.com/abc.jpg"}}"#;
        let gif: Gif = serde_json::from_str(json).expect("parses");
        assert!(RedgifsProvider::build_video_item(gif).is_none());
    }

    #[test]
    fn parses_gif_payload_and_builds_item() {
        let json = r#"{
            "id": "someid",
            "createDate": 1700000000,
            "duration": 12.5,
            "views": 42,
            "width": 1080,
            "height": 1920,
            "tags": ["Amateur", "Solo"],
            "userName": "creator1",
            "type": 1,
            "description": "",
            "urls": {"poster": "https://media.redgifs.com/someid-poster.jpg", "thumbnail": "https://media.redgifs.com/someid-mobile.jpg"}
        }"#;

        let gif: Gif = serde_json::from_str(json).expect("parses");
        let item = RedgifsProvider::build_video_item(gif).expect("builds item");
        assert_eq!(item.id, "someid");
        assert_eq!(item.url, "https://www.redgifs.com/watch/someid");
        assert!(item.formats.is_none());
        assert_eq!(item.uploaderId.as_deref(), Some("redgifs:creator1"));
        assert_eq!(item.tags, Some(vec!["Amateur".to_string(), "Solo".to_string()]));
    }
}
