use async_trait::async_trait;
use futures::FutureExt;
use once_cell::sync::Lazy;
use rustc_hash::FxHashMap as HashMap;
use std::future::Future;
use std::panic::AssertUnwindSafe;
use std::sync::Arc;

use crate::{
    DbPool,
    api::ClientVersion,
    status::{Channel, ChannelGroup, ChannelView, Status, StatusResponse},
    util::{cache::VideoCache, discord::send_discord_error_report, requester::Requester},
    videos::{ServerOptions, VideoItem},
};

include!(concat!(env!("OUT_DIR"), "/provider_selection.rs"));
include!(concat!(env!("OUT_DIR"), "/provider_modules.rs"));

// convenient alias
pub type DynProvider = Arc<dyn Provider>;

#[derive(Clone, Copy)]
pub struct ProviderChannelMetadata {
    pub group_id: &'static str,
    pub tags: &'static [&'static str],
}

pub static ALL_PROVIDERS: Lazy<HashMap<&'static str, DynProvider>> = Lazy::new(|| {
    let mut m = HashMap::default();
    include!(concat!(env!("OUT_DIR"), "/provider_registry.rs"));
    m
});

pub fn init_providers_now() {
    // Idempotent & thread-safe: runs the Lazy init exactly once.
    crate::flow_debug!(
        "provider init selection={:?}",
        compile_time_selected_provider()
    );
    Lazy::force(&ALL_PROVIDERS);
}

pub fn compile_time_selected_provider() -> Option<&'static str> {
    COMPILE_TIME_SELECTED_PROVIDER
}

pub fn resolve_provider_for_build<'a>(channel: &'a str) -> &'a str {
    match compile_time_selected_provider() {
        Some(selected) if channel == "all" => selected,
        _ => channel,
    }
}

pub fn panic_payload_to_string(payload: Box<dyn std::any::Any + Send>) -> String {
    if let Some(s) = payload.downcast_ref::<&str>() {
        return (*s).to_string();
    }
    if let Some(s) = payload.downcast_ref::<String>() {
        return s.clone();
    }
    "unknown panic payload".to_string()
}

pub async fn run_provider_guarded<F>(provider_name: &str, context: &str, fut: F) -> Vec<VideoItem>
where
    F: Future<Output = Vec<VideoItem>>,
{
    crate::flow_debug!(
        "provider guard enter provider={} context={}",
        provider_name,
        context
    );
    match AssertUnwindSafe(fut).catch_unwind().await {
        Ok(videos) => {
            crate::flow_debug!(
                "provider guard exit provider={} context={} videos={}",
                provider_name,
                context,
                videos.len()
            );
            videos
        }
        Err(payload) => {
            let panic_msg = panic_payload_to_string(payload);
            crate::flow_debug!(
                "provider guard panic provider={} context={} panic={}",
                provider_name,
                context,
                &panic_msg
            );
            let _ = send_discord_error_report(
                format!("Provider panic: {}", provider_name),
                None,
                Some("Provider Guard"),
                Some(&format!("context={}; panic={}", context, panic_msg)),
                file!(),
                line!(),
                module_path!(),
            )
            .await;
            vec![]
        }
    }
}

pub async fn report_provider_error(provider_name: &str, context: &str, msg: &str) {
    let _ = send_discord_error_report(
        format!("Provider error: {}", provider_name),
        None,
        Some("Provider Guard"),
        Some(&format!("context={}; error={}", context, msg)),
        file!(),
        line!(),
        module_path!(),
    )
    .await;
}

pub fn report_provider_error_background(provider_name: &str, context: &str, msg: &str) {
    let provider_name = provider_name.to_string();
    let context = context.to_string();
    let msg = msg.to_string();
    tokio::spawn(async move {
        report_provider_error(&provider_name, &context, &msg).await;
    });
}

pub fn requester_or_default(
    options: &ServerOptions,
    provider_name: &str,
    context: &str,
) -> Requester {
    match options.requester.clone() {
        Some(requester) => {
            crate::flow_debug!(
                "provider requester existing provider={} context={} trace={}",
                provider_name,
                context,
                requester.debug_trace_id().unwrap_or("none")
            );
            requester
        }
        None => {
            crate::flow_debug!(
                "provider requester fallback provider={} context={}",
                provider_name,
                context
            );
            report_provider_error_background(
                provider_name,
                context,
                "ServerOptions.requester missing; using default Requester",
            );
            Requester::new()
        }
    }
}

pub fn strip_url_scheme(url: &str) -> String {
    url.strip_prefix("https://")
        .or_else(|| url.strip_prefix("http://"))
        .unwrap_or(url)
        .trim_start_matches('/')
        .to_string()
}

pub fn build_proxy_url(options: &ServerOptions, proxy: &str, target: &str) -> String {
    let target = target.trim_start_matches('/');
    let base = options
        .public_url_base
        .as_deref()
        .unwrap_or("")
        .trim_end_matches('/');

    if base.is_empty() {
        format!("/proxy/{proxy}/{target}")
    } else {
        format!("{base}/proxy/{proxy}/{target}")
    }
}

fn channel_metadata_for(id: &str) -> Option<ProviderChannelMetadata> {
    include!(concat!(env!("OUT_DIR"), "/provider_metadata_fn.rs"))
}

fn channel_group_title(group_id: &str) -> &'static str {
    match group_id {
        "meta-search" => "Meta Search",
        "mainstream-tube" => "Mainstream Tube",
        "tiktok" => "Tiktok",
        "studio-network" => "Studio & Network",
        "amateur-homemade" => "Amateur & Homemade",
        "onlyfans" => "OnlyFans",
        "chinese" => "Chinese",
        "jav" => "JAV",
        "fetish-kink" => "Fetish & Kink",
        "hentai-animation" => "Hentai & Animation",
        "ai" => "AI",
        "gay-male" => "Gay & Male",
        "live-cams" => "Live Cams",
        "pmv-compilation" => "PMV & Compilation",
        _ => "Other",
    }
}

fn channel_group_system_image(group_id: &str) -> Option<&'static str> {
    match group_id {
        "jav" | "chinese" => Some("globe"),
        _ => None,
    }
}

fn channel_group_order(group_id: &str) -> usize {
    match group_id {
        "meta-search" => 0,
        "mainstream-tube" => 1,
        "tiktok" => 2,
        "studio-network" => 3,
        "onlyfans" => 4,
        "chinese" => 5,
        "jav" => 6,
        "fetish-kink" => 7,
        "hentai-animation" => 8,
        "ai" => 9,
        "gay-male" => 10,
        "live-cams" => 11,
        "pmv-compilation" => 12,
        _ => 99,
    }
}

pub fn decorate_channel(channel: Channel) -> ChannelView {
    let metadata = channel_metadata_for(&channel.id);
    let ytdlp_command = match channel.id.as_str() {
        "pimpbunny" => Some("yt-dlp --compat-options allow-unsafe-ext".to_string()),
        _ => None,
    };
    ChannelView {
        id: channel.id,
        name: channel.name,
        description: channel.description,
        premium: channel.premium,
        favicon: channel.favicon,
        status: channel.status,
        categories: channel.categories,
        options: channel.options,
        nsfw: channel.nsfw,
        groupKey: metadata.map(|value| value.group_id.to_string()),
        sortOrder: None,
        tags: metadata.map(|value| {
            value
                .tags
                .iter()
                .take(3)
                .map(|tag| (*tag).to_string())
                .collect()
        }),
        ytdlpCommand: ytdlp_command,
        cacheDuration: channel.cacheDuration,
    }
}

pub fn build_channel_groups(channels: &[ChannelView]) -> Vec<ChannelGroup> {
    let mut groups = Vec::new();

    let mut group_ids = channels
        .iter()
        .filter_map(|channel| channel.groupKey.clone())
        .collect::<Vec<_>>();
    group_ids.sort_by_key(|group_id| (channel_group_order(group_id), group_id.clone()));
    group_ids.dedup();

    for group_id in group_ids {
        let mut grouped_channels = channels
            .iter()
            .filter(|channel| channel.groupKey.as_deref() == Some(group_id.as_str()))
            .collect::<Vec<_>>();
        grouped_channels.sort_by(|a, b| {
            (a.sortOrder.unwrap_or(u32::MAX), &a.name, &a.id).cmp(&(
                b.sortOrder.unwrap_or(u32::MAX),
                &b.name,
                &b.id,
            ))
        });
        let channel_ids = grouped_channels
            .into_iter()
            .map(|channel| channel.id.clone())
            .collect::<Vec<_>>();
        groups.push(ChannelGroup {
            id: group_id.clone(),
            title: channel_group_title(&group_id).to_string(),
            systemImage: channel_group_system_image(&group_id).map(str::to_string),
            channelIds: channel_ids,
        });
    }

    groups
}

fn assign_channel_sort_order(channels: &mut [ChannelView]) {
    let mut ordered = channels
        .iter()
        .enumerate()
        .map(|(index, channel)| {
            (
                index,
                channel.groupKey.clone(),
                channel.name.to_ascii_lowercase(),
                channel.id.to_ascii_lowercase(),
            )
        })
        .collect::<Vec<_>>();

    ordered.sort_by(|a, b| {
        let a_group = a.1.as_deref().unwrap_or("");
        let b_group = b.1.as_deref().unwrap_or("");
        (channel_group_order(a_group), a_group, &a.2, &a.3).cmp(&(
            channel_group_order(b_group),
            b_group,
            &b.2,
            &b.3,
        ))
    });

    for (sort_index, (channel_index, _, _, _)) in ordered.into_iter().enumerate() {
        channels[channel_index].sortOrder = Some((sort_index + 1) as u32);
    }
}

pub fn build_status_response(status: Status) -> StatusResponse {
    let mut channels = status
        .channels
        .into_iter()
        .map(decorate_channel)
        .collect::<Vec<_>>();
    assign_channel_sort_order(&mut channels);
    let channelGroups = build_channel_groups(&channels);
    crate::flow_debug!(
        "status response build channels={} groups={}",
        channels.len(),
        channelGroups.len()
    );

    StatusResponse {
        id: status.id,
        name: status.name,
        subtitle: status.subtitle,
        description: status.description,
        iconUrl: status.iconUrl,
        color: status.color,
        status: status.status,
        notices: status.notices,
        channels,
        channelGroups,
        subscription: status.subscription,
        nsfw: status.nsfw,
        categories: status.categories,
        options: status.options,
        filtersFooter: status.filtersFooter,
    }
}

#[async_trait]
pub trait Provider: Send + Sync {
    async fn get_videos(
        &self,
        cache: VideoCache,
        pool: DbPool,
        sort: String,
        query: Option<String>,
        page: String,
        per_page: String,
        options: ServerOptions,
    ) -> Vec<VideoItem>;

    fn get_channel(&self, clientversion: ClientVersion) -> Option<Channel> {
        println!(
            "Getting channel for placeholder with client version: {:?}",
            clientversion
        );
        let _ = clientversion;
        Some(Channel {
            id: "placeholder".to_string(),
            name: "PLACEHOLDER".to_string(),
            description: "PLACEHOLDER FOR PARENT CLASS".to_string(),
            premium: false,
            favicon: "https://www.google.com/s2/favicons?sz=64&domain=missav.ws".to_string(),
            status: "active".to_string(),
            categories: vec![],
            options: vec![],
            nsfw: true,
            cacheDuration: None,
        })
    }
}

#[cfg(all(test, not(hottub_single_provider)))]
mod tests {
    use super::*;
    use crate::status::ChannelOption;

    fn base_channel(id: &str) -> Channel {
        Channel {
            id: id.to_string(),
            name: id.to_string(),
            description: String::new(),
            premium: false,
            favicon: String::new(),
            status: "active".to_string(),
            categories: vec![],
            options: Vec::<ChannelOption>::new(),
            nsfw: true,
            cacheDuration: None,
        }
    }

    #[test]
    fn decorates_channel_with_group_and_tags() {
        let channel = decorate_channel(base_channel("hsex"));
        assert_eq!(channel.groupKey.as_deref(), Some("chinese"));
        assert_eq!(channel.sortOrder, None);
        assert_eq!(channel.ytdlpCommand, None);
        assert_eq!(
            channel.tags.as_deref(),
            Some(
                &[
                    "amateur".to_string(),
                    "chinese".to_string(),
                    "homemade".to_string(),
                ][..]
            )
        );
    }

    #[test]
    fn builds_group_index() {
        let channels = vec![
            decorate_channel(base_channel("all")),
            decorate_channel(base_channel("hsex")),
            decorate_channel(base_channel("missav")),
        ];
        let groups = build_channel_groups(&channels);
        assert_eq!(groups[0].id, "meta-search");
        assert_eq!(groups[1].id, "chinese");
        assert_eq!(groups[2].id, "jav");
    }

    #[test]
    fn decorates_pimpbunny_with_ytdlp_command() {
        let channel = decorate_channel(base_channel("pimpbunny"));
        assert_eq!(
            channel.ytdlpCommand.as_deref(),
            Some("yt-dlp --compat-options allow-unsafe-ext")
        );
    }

    #[test]
    fn reflects_updated_group_moves() {
        assert_eq!(
            decorate_channel(base_channel("perverzija"))
                .groupKey
                .as_deref(),
            Some("studio-network")
        );
        assert_eq!(
            decorate_channel(base_channel("rule34gen"))
                .groupKey
                .as_deref(),
            Some("ai")
        );
    }

    #[test]
    fn status_response_uses_documented_group_keys() {
        let mut status = Status::new();
        status.channels = vec![
            base_channel("missav"),
            base_channel("hsex"),
            base_channel("all"),
            base_channel("pimpbunny"),
        ];

        let json = serde_json::to_value(build_status_response(status)).expect("valid status json");

        let channels = json["channels"].as_array().expect("channels array");
        let all_channel = channels
            .iter()
            .find(|channel| channel["id"] == "all")
            .expect("all channel present");
        assert_eq!(all_channel["groupKey"], "meta-search");
        assert!(all_channel.get("group").is_none());
        assert!(all_channel["sortOrder"].is_number());

        let groups = json["channelGroups"].as_array().expect("group array");
        let meta_group = groups
            .iter()
            .find(|group| group["id"] == "meta-search")
            .expect("meta group present");
        assert_eq!(meta_group["channelIds"], serde_json::json!(["all"]));
        assert!(meta_group.get("channels").is_none());

        let chinese_group = groups
            .iter()
            .find(|group| group["id"] == "chinese")
            .expect("chinese group present");
        assert_eq!(chinese_group["systemImage"], "globe");

        let pimpbunny_channel = channels
            .iter()
            .find(|channel| channel["id"] == "pimpbunny")
            .expect("pimpbunny channel present");
        assert_eq!(
            pimpbunny_channel["ytdlpCommand"],
            "yt-dlp --compat-options allow-unsafe-ext"
        );
    }
}
