use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use chrono::DateTime;
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
use serde::Deserialize;
use tokio::sync::RwLock;

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, VideoFormat, VideoItem};

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

const CHANNEL_ID: &str = "kwiky";
const BASE_URL: &str = "https://kwiky.com";
const API_GW: &str = "https://kwiky.com/api/v1/gateway/v1";
// Observed fixed gateway UUID from browser — identifies the Kwiky/ICF platform gateway.
const X_GATEWAY: &str = "76d59e04-97fa-4ced-aa99-b86ffaf756a2";
// base64("https://recommendedquickies-webservice.icfsys.com/v2/KW/hotlist")
const SOURCE_HOTLIST: &str = "aHR0cHM6Ly9yZWNvbW1lbmRlZHF1aWNraWVzLXdlYnNlcnZpY2UuaWNmc3lzLmNvbS92Mi9LVy9ob3RsaXN0";
const DEFAULT_PER_PAGE: usize = 25;
const MAX_PAGE_WALK: u16 = 5;

// Curated tags from /api/v1/gateway/v1/keywords/populartags?gender=f
const CURATED_TAGS: &[&str] = &[
    "anal", "ass", "asian", "babe", "bbc", "bbw", "bdsm", "bigass", "bigtits",
    "blowjob", "brunette", "cameltoe", "clit", "cosplay", "couples", "cowgirl",
    "cum", "curvy", "deepthroat", "dildo", "ebony", "feet", "fingering", "fishnets",
    "fuckme", "hugetits", "latina", "leather", "lesbian", "masturbation", "mature",
    "milf", "panties", "petite", "pov", "pussy", "redhead", "ride", "shower",
    "smalltits", "spank", "sucking", "tattoos", "tease", "tits", "tongue",
    "toys", "twerk",
];

// ---------------------------------------------------------------------------
// Target routing
// ---------------------------------------------------------------------------

#[derive(Debug, Clone)]
enum Target {
    Recommended,
    Search(String),
}

// ---------------------------------------------------------------------------
// API response types
// ---------------------------------------------------------------------------

#[derive(Debug, Deserialize)]
struct IdListResponse {
    recommended: Vec<IdEntry>,
    #[serde(rename = "pageToken", default)]
    page_token: Option<String>,
}

#[derive(Debug, Deserialize)]
struct IdEntry {
    id: String,
}

type MetadataMap = HashMap<String, QuickyEntry>;

#[derive(Debug, Deserialize, Default)]
struct QuickyEntry {
    media: Option<QuickyMedia>,
    creator: Option<QuickyCreator>,
}

#[derive(Debug, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
struct QuickyMedia {
    #[serde(default)]
    description: String,
    #[serde(default)]
    thumb_url: String,
    #[serde(default)]
    preview_url: String,
    #[serde(default)]
    url: String,
    #[serde(default)]
    duration: u32,
    #[serde(default)]
    created: String,
    #[serde(default)]
    width: u32,
    #[serde(default)]
    height: u32,
}

#[derive(Debug, Deserialize, Default)]
struct QuickyCreator {
    #[serde(default)]
    id: u64,
    #[serde(default)]
    name: String,
}

// ---------------------------------------------------------------------------
// Provider
// ---------------------------------------------------------------------------

pub struct KwikyProvider {
    xsrf: Arc<RwLock<Option<String>>>,
}

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

    fn api_headers(xsrf: &str) -> Vec<(String, String)> {
        vec![
            ("Referer".to_string(), format!("{BASE_URL}/")),
            ("x-xsrf-token".to_string(), xsrf.to_string()),
            ("x-gateway".to_string(), X_GATEWAY.to_string()),
            ("Accept".to_string(), "application/json, text/plain, */*".to_string()),
        ]
    }

    fn parse_xsrf(body: &str) -> Option<String> {
        let needle = "meta.content = '";
        let start = body.find(needle)?;
        let after = &body[start + needle.len()..];
        let end = after.find('\'')?;
        let token = after[..end].trim().to_string();
        if token.is_empty() { None } else { Some(token) }
    }

    async fn get_xsrf(&self, options: &ServerOptions) -> Option<String> {
        {
            let guard = self.xsrf.read().await;
            if let Some(ref token) = *guard {
                return Some(token.clone());
            }
        }

        let mut guard = self.xsrf.write().await;
        // Double-check after acquiring write lock
        if let Some(ref token) = *guard {
            return Some(token.clone());
        }

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

        // Fetch main page first to establish session cookies, then get XSRF token
        let _ = requester.get(BASE_URL, None).await;

        let preload_url = format!("{BASE_URL}/api/state/v1/preload");
        let body = match requester.get(&preload_url, None).await {
            Ok(body) => body,
            Err(e) => {
                report_provider_error(CHANNEL_ID, "get_xsrf", &format!("preload failed: {e}")).await;
                return None;
            }
        };

        let token = Self::parse_xsrf(&body);
        if token.is_none() {
            report_provider_error(CHANNEL_ID, "get_xsrf", "xsrf token not found in preload").await;
        }
        *guard = token.clone();
        token
    }

    fn invalidate_xsrf(&self) {
        if let Ok(mut guard) = self.xsrf.try_write() {
            *guard = None;
        }
    }

    async fn fetch_id_list(&self, url: &str, xsrf: &str, options: &ServerOptions) -> Option<IdListResponse> {
        let mut requester = requester_or_default(options, CHANNEL_ID, "fetch_id_list");
        let text = match requester.get_with_headers(url, Self::api_headers(xsrf), None).await {
            Ok(text) => text,
            Err(e) => {
                let msg = e.to_string();
                if msg.contains("401") || msg.contains("Unauthorized") {
                    self.invalidate_xsrf();
                }
                report_provider_error(CHANNEL_ID, "fetch_id_list", &format!("request failed for {url}: {e}")).await;
                return None;
            }
        };

        match serde_json::from_str::<IdListResponse>(&text) {
            Ok(resp) => Some(resp),
            Err(e) => {
                report_provider_error(CHANNEL_ID, "fetch_id_list", &format!("parse failed: {e}")).await;
                None
            }
        }
    }

    async fn fetch_metadata(&self, ids: &[String], xsrf: &str, options: &ServerOptions) -> MetadataMap {
        if ids.is_empty() {
            return MetadataMap::new();
        }
        let ids_param = ids.join(",");
        let url = format!("{API_GW}/quickies/metadata?quickieIds={ids_param}");

        let mut requester = requester_or_default(options, CHANNEL_ID, "fetch_metadata");
        let text = match requester.get_with_headers(&url, Self::api_headers(xsrf), None).await {
            Ok(text) => text,
            Err(e) => {
                report_provider_error(CHANNEL_ID, "fetch_metadata", &format!("request failed: {e}")).await;
                return MetadataMap::new();
            }
        };

        serde_json::from_str::<MetadataMap>(&text).unwrap_or_default()
    }

    async fn recommended_ids(
        &self,
        page: u16,
        per_page: usize,
        xsrf: &str,
        options: &ServerOptions,
    ) -> Vec<String> {
        let mut cursor: Option<String> = None;
        let mut ids = Vec::new();

        let walk = page.min(MAX_PAGE_WALK);
        for _ in 0..walk {
            let mut url = format!(
                "{API_GW}/quickies/recommended?pageSize={per_page}&source={SOURCE_HOTLIST}&gender=f"
            );
            if let Some(ref token) = cursor {
                url.push_str(&format!("&pageToken={token}"));
            }

            let resp = match self.fetch_id_list(&url, xsrf, options).await {
                Some(resp) => resp,
                None => return vec![],
            };

            ids = resp.recommended.into_iter().map(|e| e.id).collect();
            cursor = resp.page_token.filter(|t| !t.trim().is_empty());

            if ids.is_empty() || cursor.is_none() {
                break;
            }
        }

        ids
    }

    async fn search_ids(
        &self,
        keyword: &str,
        page: u16,
        per_page: usize,
        xsrf: &str,
        options: &ServerOptions,
    ) -> Vec<String> {
        let page_offset = (page as usize - 1) * per_page;
        let encoded = utf8_percent_encode(keyword, NON_ALPHANUMERIC).to_string();
        let mut url = format!(
            "{API_GW}/quickies/keywordSearch?pageSize={per_page}&keyword={encoded}&gender=f"
        );
        if page_offset > 0 {
            url.push_str(&format!("&pageToken={page_offset}"));
        }

        match self.fetch_id_list(&url, xsrf, options).await {
            Some(resp) => resp.recommended.into_iter().map(|e| e.id).collect(),
            None => vec![],
        }
    }

    fn pick_target(query: Option<&str>, options: &ServerOptions) -> Target {
        // Any query (raw keyword, tag: prefix, or #tag) routes to keywordSearch.
        // The recommended endpoint's &tag= param does not reliably filter;
        // keywordSearch handles both keywords and tag names.
        if let Some(q) = query.map(str::trim).filter(|q| !q.is_empty()) {
            let keyword = if let Some((kind, value)) = q.split_once(':') {
                match kind.trim().to_ascii_lowercase().as_str() {
                    "tag" | "category" | "cat" | "genre" => value.trim().to_string(),
                    _ => q.to_string(),
                }
            } else if let Some(tag) = q.strip_prefix('#') {
                tag.trim().to_string()
            } else {
                q.to_string()
            };
            return Target::Search(keyword);
        }

        if let Some(cat) = options.categories.as_deref().filter(|c| *c != "all" && !c.trim().is_empty()) {
            return Target::Search(cat.to_string());
        }

        Target::Recommended
    }

    fn build_video_item(id: &str, entry: QuickyEntry) -> Option<VideoItem> {
        let media = entry.media?;
        if media.url.trim().is_empty() {
            return None;
        }

        let title = if media.description.trim().is_empty() {
            format!("Kwiky clip {id}")
        } else {
            media.description.trim().to_string()
        };

        let page_url = format!("{BASE_URL}/quickies/{id}");
        let thumb = media.thumb_url.clone();
        let duration = media.duration;

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

        // Direct MP4 playback via formats (page URL is not yt-dlp-resolvable)
        let format = VideoFormat::new(media.url, "auto".to_string(), "mp4".to_string());
        item.formats = Some(vec![format]);

        if !media.preview_url.trim().is_empty() {
            item.preview = Some(media.preview_url);
        }

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

        if !media.created.trim().is_empty() {
            if let Ok(dt) = DateTime::parse_from_rfc3339(&media.created) {
                item.uploadedAt = Some(dt.timestamp().max(0) as u64);
            }
        }

        if let Some(creator) = entry.creator {
            if !creator.name.trim().is_empty() {
                let name = creator.name.trim().to_string();
                item.uploader = Some(name.clone());
                item.uploaderUrl = Some(format!("{BASE_URL}/profile/{name}"));
                if creator.id > 0 {
                    item.uploaderId = Some(format!("{CHANNEL_ID}:{}", creator.id));
                }
            }
        }

        Some(item)
    }

    fn build_channel(&self, _cv: ClientVersion) -> Channel {
        let category_options: Vec<FilterOption> = CURATED_TAGS
            .iter()
            .map(|tag| FilterOption {
                id: tag.to_string(),
                title: capitalize_first(tag),
            })
            .collect();

        Channel {
            id: CHANNEL_ID.to_string(),
            name: "Kwiky".to_string(),
            description: "Kwiky short-form cam clips — amateur performer quickies with direct MP4 playback and tag browsing.".to_string(),
            premium: false,
            favicon: "https://www.google.com/s2/favicons?sz=64&domain=kwiky.com".to_string(),
            status: "active".to_string(),
            categories: CURATED_TAGS.iter().map(|t| capitalize_first(t)).collect(),
            options: vec![
                ChannelOption {
                    id: "categories".to_string(),
                    title: "Tags".to_string(),
                    description: "Browse Kwiky clips by tag".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 capitalize_first(s: &str) -> String {
    let mut chars = s.chars();
    match chars.next() {
        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
        None => String::new(),
    }
}

#[async_trait]
impl Provider for KwikyProvider {
    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 _ = sort;

        let page = page.parse::<u16>().unwrap_or(1).max(1);
        let per_page = per_page
            .parse::<usize>()
            .unwrap_or(DEFAULT_PER_PAGE)
            .clamp(1, 50);

        let normalized_query = query.as_deref().map(str::trim).filter(|q| !q.is_empty());
        let target = Self::pick_target(normalized_query, &options);

        let xsrf = match self.get_xsrf(&options).await {
            Some(xsrf) => xsrf,
            None => return vec![],
        };

        let ids = match &target {
            Target::Recommended => {
                self.recommended_ids(page, per_page, &xsrf, &options).await
            }
            Target::Search(keyword) => {
                self.search_ids(keyword, page, per_page, &xsrf, &options).await
            }
        };

        if ids.is_empty() {
            return vec![];
        }

        let mut metadata = self.fetch_metadata(&ids, &xsrf, &options).await;

        ids.into_iter()
            .filter_map(|id| {
                metadata.remove(&id).and_then(|entry| Self::build_video_item(&id, entry))
            })
            .collect()
    }

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