use async_trait::async_trait;
use error_chain::error_chain;
use futures::future::join_all;
use serde_json::json;
use std::vec;

use crate::DbPool;
use crate::api::ClientVersion;
use crate::db;
use crate::providers::{Provider, report_provider_error, report_provider_error_background};
use crate::status::*;
use crate::util::cache::VideoCache;
use crate::videos::{self, ServerOptions, VideoItem};

pub const CHANNEL_METADATA: crate::providers::ProviderChannelMetadata =
    crate::providers::ProviderChannelMetadata {
        group_id: "hentai-animation",
        tags: &["hentai", "anime", "premium"],
    };

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

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
struct HanimeSearchRequest {
    search_text: String,
    tags: Vec<String>,
    tags_mode: String,
    brands: Vec<String>,
    blacklist: Vec<String>,
    order_by: String,
    ordering: String,
    page: u8,
}

impl HanimeSearchRequest {
    pub fn new() -> Self {
        HanimeSearchRequest {
            search_text: "".to_string(),
            tags: vec![],
            tags_mode: "AND".to_string(),
            brands: vec![],
            blacklist: vec![],
            order_by: "created_at_unix".to_string(),
            ordering: "desc".to_string(),
            page: 0,
        }
    }
    pub fn search_text(mut self, search_text: String) -> Self {
        self.search_text = search_text;
        self
    }
    pub fn order_by(mut self, order_by: String) -> Self {
        self.order_by = order_by;
        self
    }
    pub fn ordering(mut self, ordering: String) -> Self {
        self.ordering = ordering;
        self
    }
    pub fn page(mut self, page: u8) -> Self {
        self.page = page;
        self
    }
}

#[derive(serde::Serialize, serde::Deserialize, Debug)]
struct HanimeSearchResponse {
    page: u8,
    nbPages: u8,
    nbHits: u32,
    hitsPerPage: u8,
    hits: String,
}

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
struct HanimeSearchResult {
    id: u64,
    name: String,
    titles: Vec<String>,
    slug: String,
    description: String,
    views: u64,
    interests: u64,
    poster_url: String,
    cover_url: String,
    brand: String,
    brand_id: u64,
    duration_in_ms: u32,
    is_censored: bool,
    rating: Option<u32>,
    likes: u64,
    dislikes: u64,
    downloads: u64,
    monthly_ranked: Option<u64>,
    tags: Vec<String>,
    created_at: u64,
    released_at: u64,
}

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

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

    fn build_channel(&self, _clientversion: ClientVersion) -> Channel {
        Channel {
            id: "hanime".to_string(),
            name: "Hanime".to_string(),
            description: "Free Hentai from Hanime".to_string(),
            premium: false,
            favicon: "https://www.google.com/s2/favicons?sz=64&domain=hanime.tv".to_string(),
            status: "active".to_string(),
            categories: vec![],
            options: vec![ChannelOption {
                id: "sort".to_string(),
                title: "Sort".to_string(),
                description: "Sort the Videos".to_string(),
                systemImage: "list.number".to_string(),
                colorName: "blue".to_string(),
                options: vec![
                    FilterOption {
                        id: "created_at_unix.desc".to_string(),
                        title: "Recent Upload".to_string(),
                    },
                    FilterOption {
                        id: "created_at_unix.asc".to_string(),
                        title: "Old Upload".to_string(),
                    },
                    FilterOption {
                        id: "views.desc".to_string(),
                        title: "Most Views".to_string(),
                    },
                    FilterOption {
                        id: "views.asc".to_string(),
                        title: "Least Views".to_string(),
                    },
                    FilterOption {
                        id: "likes.desc".to_string(),
                        title: "Most Likes".to_string(),
                    },
                    FilterOption {
                        id: "likes.asc".to_string(),
                        title: "Least Likes".to_string(),
                    },
                    FilterOption {
                        id: "released_at_unix.desc".to_string(),
                        title: "New".to_string(),
                    },
                    FilterOption {
                        id: "released_at_unix.asc".to_string(),
                        title: "Old".to_string(),
                    },
                    FilterOption {
                        id: "title_sortable.asc".to_string(),
                        title: "A - Z".to_string(),
                    },
                    FilterOption {
                        id: "title_sortable.desc".to_string(),
                        title: "Z - A".to_string(),
                    },
                ],
                multiSelect: false,
            }],
            nsfw: true,
            cacheDuration: None,
            ytdlpCommand: Some("yt-dlp --impersonate chrome-120".to_string())
        }
    }

    fn db_key(slug: &str) -> String {
        format!("https://h.freeanimehentai.net/api/v8/video?id={slug}&")
    }

    fn build_video_item(
        id: String,
        title: String,
        video_url: String,
        channel: String,
        thumb: String,
        duration: u32,
        tags: Vec<String>,
        brand: String,
        views: u64,
        likes: u64,
        dislikes: u64,
    ) -> VideoItem {
        VideoItem::new(id, title, video_url.clone(), channel, thumb, duration)
            .tags(tags)
            .uploader(brand)
            .views(views as u32)
            .rating((likes as f32 / (likes + dislikes) as f32) * 100_f32)
            .aspect_ratio(0.68)
            .formats(vec![videos::VideoFormat::new(
                video_url,
                "1080".to_string(),
                "m3u8".to_string(),
            )])
    }

    async fn fetch_stream_url(&self, id: &str, slug: &str, options: &ServerOptions) -> Result<String> {
        let manifest_url = format!(
            "https://cached.freeanimehentai.net/api/v8/guest/videos/{id}/manifest"
        );
        let mut requester =
            crate::providers::requester_or_default(options, module_path!(), "missing_requester");
        let payload = json!({ "width": 571, "height": 703, "ab": "kh" });
        let _ = requester
            .post_json(
                &format!(
                    "https://cached.freeanimehentai.net/api/v8/hentai_videos/{slug}/play"
                ),
                &payload,
                vec![
                    ("Origin".to_string(), "https://hanime.tv".to_string()),
                    ("Referer".to_string(), "https://hanime.tv/".to_string()),
                ],
            )
            .await;
        ntex::time::sleep(ntex::time::Seconds(1)).await;
        let text = requester
            .get_raw_with_headers(
                &manifest_url,
                vec![
                    ("Origin".to_string(), "https://hanime.tv".to_string()),
                    ("Referer".to_string(), "https://hanime.tv/".to_string()),
                ],
            )
            .await
            .map_err(|e| {
                report_provider_error_background(
                    "hanime",
                    "fetch_stream_url.get_raw_with_headers",
                    &e.to_string(),
                );
                Error::from(format!("Failed to fetch manifest: {e}"))
            })?
            .text()
            .await
            .map_err(|e| {
                report_provider_error_background(
                    "hanime",
                    "fetch_stream_url.response_text",
                    &e.to_string(),
                );
                Error::from(format!("Failed to decode manifest body: {e}"))
            })?;

        if text.contains("Unautho") {
            return Err(Error::from("Unauthorized"));
        }

        let urls_section = text
            .split("streams")
            .nth(1)
            .ok_or_else(|| Error::from("Missing streams section in manifest"))?;

        let mut url_vec = vec![];
        for el in urls_section.split("\"url\":\"") {
            let url = el.split('"').next().unwrap_or_default();
            if !url.is_empty() && url.contains("m3u8") {
                url_vec.push(url.to_string());
            }
        }

        url_vec
            .into_iter()
            .next()
            .ok_or_else(|| Error::from("No stream URL found in manifest"))
    }

    async fn get_video_item(
        &self,
        hit: HanimeSearchResult,
        pool: DbPool,
        options: ServerOptions,
    ) -> Result<VideoItem> {
        let id = hit.id.to_string();
        let title = hit.name;
        let thumb = crate::providers::build_proxy_url(
            &options,
            "hanime-cdn",
            &crate::providers::strip_url_scheme(&hit.cover_url),
        );
        let duration = (hit.duration_in_ms / 1000) as u32;
        let channel = "hanime".to_string();
        let db_key = Self::db_key(&hit.slug);

        match self.fetch_stream_url(&id, &hit.slug, &options).await {
            Ok(stream_url) => {
                if let Ok(mut conn) = pool.get() {
                    let _ = db::insert_video(&mut conn, &db_key, &stream_url);
                }
                return Ok(Self::build_video_item(
                    id, title, stream_url, channel, thumb, duration,
                    hit.tags, hit.brand, hit.views, hit.likes, hit.dislikes,
                ));
            }
            Err(e) => {
                report_provider_error_background("hanime", "get_video_item.fetch_stream_url", &e.to_string());
            }
        }

        // API failed — fall back to DB
        let db_result = pool.get().ok().and_then(|mut conn| {
            db::get_video(&mut conn, db_key.clone()).ok().flatten()
        });

        match db_result {
            Some(video_url) if video_url != "https://streamable.cloud/hls/stream.m3u8" => {
                Ok(Self::build_video_item(
                    id, title, video_url, channel, thumb, duration,
                    hit.tags, hit.brand, hit.views, hit.likes, hit.dislikes,
                ))
            }
            Some(_) => {
                if let Ok(mut conn) = pool.get() {
                    let _ = db::delete_video(&mut conn, db_key);
                }
                Err(Error::from("Stale DB entry and API unavailable"))
            }
            None => Err(Error::from("API unavailable and no DB fallback")),
        }
    }

    async fn get(
        &self,
        cache: VideoCache,
        pool: DbPool,
        page: u8,
        query: String,
        sort: String,
        options: ServerOptions,
    ) -> Result<Vec<VideoItem>> {
        let index = format!("hanime:{}:{}:{}", query, page, sort);
        let order_by = match sort.contains(".") {
            true => sort
                .split(".")
                .collect::<Vec<&str>>()
                .get(0)
                .copied()
                .unwrap_or_default()
                .to_string(),
            false => "created_at_unix".to_string(),
        };
        let ordering = match sort.contains(".") {
            true => sort
                .split(".")
                .collect::<Vec<&str>>()
                .get(1)
                .copied()
                .unwrap_or_default()
                .to_string(),
            false => "desc".to_string(),
        };
        let old_items = match cache.get(&index) {
            Some((time, items)) => {
                if time.elapsed().unwrap_or_default().as_secs() < 1 {
                    //println!("Cache hit for URL: {}", index);
                    return Ok(items.clone());
                } else {
                    items.clone()
                }
            }
            None => {
                vec![]
            }
        };

        let search = HanimeSearchRequest::new()
            .page(page - 1)
            .search_text(query.clone())
            .order_by(order_by)
            .ordering(ordering);

        let mut requester =
            crate::providers::requester_or_default(&options, module_path!(), "missing_requester");
        let response = match requester
            .post_json("https://search.htv-services.com/search", &search, vec![])
            .await
        {
            Ok(response) => response,
            Err(e) => {
                report_provider_error(
                    "hanime",
                    "get.search_request",
                    &format!("query={query}; page={page}; error={e}"),
                )
                .await;
                return Ok(old_items);
            }
        };

        let hits = match response.json::<HanimeSearchResponse>().await {
            Ok(resp) => resp.hits,
            Err(e) => {
                println!("Failed to parse HanimeSearchResponse: {}", e);
                return Ok(old_items);
            }
        };
        let hits_json: Vec<HanimeSearchResult> = serde_json::from_str(hits.as_str())
            .map_err(|e| format!("Failed to parse hits JSON: {}", e))?;
        // let timeout_duration = Duration::from_secs(120);
        let futures = hits_json
            .into_iter()
            .map(|el| self.get_video_item(el.clone(), pool.clone(), options.clone()));
        let results: Vec<Result<VideoItem>> = join_all(futures).await;
        let video_items: Vec<VideoItem> = results.into_iter().filter_map(Result::ok).collect();
        if !video_items.is_empty() {
            cache.remove(&index);
            cache.insert(index.clone(), video_items.clone());
        } else {
            return Ok(old_items);
        }

        Ok(video_items)
    }
}

#[async_trait]
impl Provider for HanimeProvider {
    async fn get_videos(
        &self,
        cache: VideoCache,
        pool: DbPool,
        sort: String,
        query: Option<String>,
        page: String,
        per_page: String,
        options: ServerOptions,
    ) -> Vec<VideoItem> {
        let _ = options;
        let _ = per_page;
        let _ = sort;
        let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
            Some(q) => {
                self.get(
                    cache,
                    pool,
                    page.parse::<u8>().unwrap_or(1),
                    q,
                    sort,
                    options,
                )
                .await
            }
            None => {
                self.get(
                    cache,
                    pool,
                    page.parse::<u8>().unwrap_or(1),
                    "".to_string(),
                    sort,
                    options,
                )
                .await
            }
        };
        match videos {
            Ok(v) => v,
            Err(e) => {
                println!("Error fetching videos: {}", e);
                vec![]
            }
        }
    }

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