use std::collections::BTreeMap;

use anyhow::{bail, Result};
use futures_util::{stream, StreamExt};
use indicatif::{ProgressBar, ProgressStyle};
use num_enum::TryFromPrimitive;
use once_cell::sync::Lazy;
use regex::Regex;
use rustypipe::client::{ClientType, RustyPipe, RustyPipeQuery};
use rustypipe::model::{MusicItem, YouTubeItem};
use rustypipe::param::search_filter::{ItemType, SearchFilter};
use rustypipe::param::ChannelVideoTab;
use serde::de::IgnoredAny;
use serde::{Deserialize, Serialize};

use crate::model::QCont;

#[derive(
    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, TryFromPrimitive, Serialize, Deserialize,
)]
#[repr(u16)]
pub enum ABTest {
    AttributedTextDescription = 1,
    ThreeTabChannelLayout = 2,
    ChannelHandlesInSearchResults = 3,
    TrendsVideoTab = 4,
    TrendsPageHeaderRenderer = 5,
    DiscographyPage = 6,
    ShortDateFormat = 7,
    TrackViewcount = 8,
    PlaylistsForShorts = 9,
    ChannelAboutModal = 10,
    LikeButtonViewmodel = 11,
    ChannelPageHeader = 12,
    MusicPlaylistTwoColumn = 13,
    CommentsFrameworkUpdate = 14,
    ChannelShortsLockup = 15,
    PlaylistPageHeader = 16,
    ChannelPlaylistsLockup = 17,
    MusicPlaylistFacepile = 18,
}

/// List of active A/B tests that are run when none is manually specified
const TESTS_TO_RUN: [ABTest; 3] = [
    ABTest::ChannelPageHeader,
    ABTest::MusicPlaylistTwoColumn,
    ABTest::CommentsFrameworkUpdate,
];

#[derive(Debug, Serialize, Deserialize)]
pub struct ABTestRes {
    id: u16,
    name: ABTest,
    tests: usize,
    occurrences: usize,
    vd_present: Option<String>,
    vd_absent: Option<String>,
}

#[derive(Debug, Serialize)]
struct QVideo<'a> {
    video_id: &'a str,
    content_check_ok: bool,
    racy_check_ok: bool,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QBrowse<'a> {
    browse_id: &'a str,
    #[serde(skip_serializing_if = "Option::is_none")]
    params: Option<&'a str>,
}

pub async fn run_test(
    ab: ABTest,
    n: usize,
    concurrency: usize,
) -> (usize, Option<String>, Option<String>) {
    eprintln!("🧪 A/B test #{}: {:?}", ab as u16, ab);

    let rp = RustyPipe::new();
    let pb = ProgressBar::new(n as u64);
    pb.set_style(
        ProgressStyle::with_template(
            "{msg} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len}",
        )
        .unwrap(),
    );

    let results = stream::iter(0..n)
        .map(|_| {
            let rp = rp.clone();
            let pb = pb.clone();
            async move {
                let visitor_data = rp.query().get_visitor_data().await.unwrap();
                let query = rp.query().visitor_data(&visitor_data);
                let is_present = match ab {
                    ABTest::AttributedTextDescription => attributed_text_description(&query).await,
                    ABTest::ThreeTabChannelLayout => three_tab_channel_layout(&query).await,
                    ABTest::ChannelHandlesInSearchResults => {
                        channel_handles_in_search_results(&query).await
                    }
                    ABTest::TrendsVideoTab => trends_video_tab(&query).await,
                    ABTest::TrendsPageHeaderRenderer => trends_page_header_renderer(&query).await,
                    ABTest::DiscographyPage => discography_page(&query).await,
                    ABTest::ShortDateFormat => short_date_format(&query).await,
                    ABTest::PlaylistsForShorts => playlists_for_shorts(&query).await,
                    ABTest::TrackViewcount => track_viewcount(&query).await,
                    ABTest::ChannelAboutModal => channel_about_modal(&query).await,
                    ABTest::LikeButtonViewmodel => like_button_viewmodel(&query).await,
                    ABTest::ChannelPageHeader => channel_page_header(&query).await,
                    ABTest::MusicPlaylistTwoColumn => music_playlist_two_column(&query).await,
                    ABTest::CommentsFrameworkUpdate => comments_framework_update(&query).await,
                    ABTest::ChannelShortsLockup => channel_shorts_lockup(&query).await,
                    ABTest::PlaylistPageHeader => playlist_page_header_renderer(&query).await,
                    ABTest::ChannelPlaylistsLockup => channel_playlists_lockup(&query).await,
                    ABTest::MusicPlaylistFacepile => music_playlist_facepile(&query).await,
                }
                .unwrap();
                pb.inc(1);
                (is_present, visitor_data)
            }
        })
        .buffer_unordered(concurrency)
        .collect::<Vec<_>>()
        .await;

    let count = results.iter().filter(|(p, _)| *p).count();
    let vd_present = results
        .iter()
        .find_map(|(p, vd)| if *p { Some(vd.clone()) } else { None });
    let vd_absent = results
        .iter()
        .find_map(|(p, vd)| if *p { None } else { Some(vd.clone()) });

    (count, vd_present, vd_absent)
}

pub async fn run_all_tests(n: usize, concurrency: usize) -> Vec<ABTestRes> {
    let mut results = Vec::new();

    for ab in TESTS_TO_RUN {
        let (occurrences, vd_present, vd_absent) = run_test(ab, n, concurrency).await;
        results.push(ABTestRes {
            id: ab as u16,
            name: ab,
            tests: n,
            occurrences,
            vd_present,
            vd_absent,
        });
    }
    results
}

pub async fn attributed_text_description(rp: &RustyPipeQuery) -> Result<bool> {
    let q = QVideo {
        video_id: "ZeerrnuLi5E",
        content_check_ok: false,
        racy_check_ok: false,
    };
    let response_txt = rp.raw(ClientType::Desktop, "next", &q).await?;

    if !response_txt.contains("\"Black Mamba\"") {
        bail!("invalid response data");
    }

    Ok(response_txt.contains("\"attributedDescription\""))
}

pub async fn three_tab_channel_layout(rp: &RustyPipeQuery) -> Result<bool> {
    let channel = rp.channel_videos("UCR-DXc1voovS8nhAvccRZhg").await?;
    Ok(channel.has_live || channel.has_shorts)
}

pub async fn channel_handles_in_search_results(rp: &RustyPipeQuery) -> Result<bool> {
    let search = rp
        .search_filter("rust", &SearchFilter::new().item_type(ItemType::Channel))
        .await
        .unwrap();

    Ok(search.items.items.iter().any(|itm| match itm {
        YouTubeItem::Channel(channel) => channel
            .subscriber_count
            .map(|sc| sc > 100 && channel.handle.is_some())
            .unwrap_or_default(),
        _ => false,
    }))
}

pub async fn trends_video_tab(rp: &RustyPipeQuery) -> Result<bool> {
    let res = rp
        .raw(
            ClientType::Desktop,
            "browse",
            &QBrowse {
                browse_id: "FEtrending",
                params: None,
            },
        )
        .await?;

    Ok(res.contains("\"4gIOGgxtb3N0X3BvcHVsYXI%3D\""))
}

pub async fn trends_page_header_renderer(rp: &RustyPipeQuery) -> Result<bool> {
    let res = rp
        .raw(
            ClientType::Desktop,
            "browse",
            &QBrowse {
                browse_id: "FEtrending",
                params: None,
            },
        )
        .await?;

    #[derive(Debug, Deserialize)]
    struct D {
        header: BTreeMap<String, IgnoredAny>,
    }

    let data = serde_json::from_str::<D>(&res)?;

    Ok(data.header.contains_key("pageHeaderRenderer"))
}

pub async fn discography_page(rp: &RustyPipeQuery) -> Result<bool> {
    let id = "UC7cl4MmM6ZZ2TcFyMk_b4pg";
    let res = rp
        .raw(
            ClientType::DesktopMusic,
            "browse",
            &QBrowse {
                browse_id: id,
                params: None,
            },
        )
        .await?;
    Ok(res.contains(&format!("\"MPAD{id}\"")))
}

pub async fn short_date_format(rp: &RustyPipeQuery) -> Result<bool> {
    static SHORT_DATE: Lazy<Regex> = Lazy::new(|| Regex::new("\\d(?:y|mo|w|d|h|min) ").unwrap());
    let channel = rp.channel_videos("UC2DjFE7Xf11URZqWBigcVOQ").await?;

    Ok(channel.content.items.iter().any(|itm| {
        itm.publish_date_txt
            .as_deref()
            .map(|d| SHORT_DATE.is_match(d))
            .unwrap_or_default()
    }))
}

pub async fn playlists_for_shorts(rp: &RustyPipeQuery) -> Result<bool> {
    let playlist = rp.playlist("UUSHh8gHdtzO2tXd593_bjErWg").await?;
    let v1 = playlist
        .videos
        .items
        .first()
        .ok_or_else(|| anyhow::anyhow!("no videos"))?;
    Ok(v1.publish_date_txt.is_none())
}

pub async fn track_viewcount(rp: &RustyPipeQuery) -> Result<bool> {
    let res = rp.music_search_main("lieblingsmensch namika").await?;

    let track = &res
        .items
        .items
        .iter()
        .find_map(|itm| {
            if let MusicItem::Track(track) = itm {
                if track.id == "6485PhOtHzY" {
                    Some(track)
                } else {
                    None
                }
            } else {
                None
            }
        })
        .unwrap_or_else(|| {
            panic!("could not find track, got {:#?}", &res.items.items);
        });

    Ok(track.view_count.is_some())
}

pub async fn channel_about_modal(rp: &RustyPipeQuery) -> Result<bool> {
    let id = "UC2DjFE7Xf11URZqWBigcVOQ";
    let res = rp
        .raw(
            ClientType::Desktop,
            "browse",
            &QBrowse {
                browse_id: id,
                params: None,
            },
        )
        .await?;
    Ok(!res.contains("\"EgVhYm91dPIGBAoCEgA%3D\""))
}

pub async fn like_button_viewmodel(rp: &RustyPipeQuery) -> Result<bool> {
    let res = rp
        .raw(
            ClientType::Desktop,
            "next",
            &QVideo {
                video_id: "ZeerrnuLi5E",
                content_check_ok: true,
                racy_check_ok: true,
            },
        )
        .await?;
    Ok(res.contains("\"segmentedLikeDislikeButtonViewModel\""))
}

pub async fn channel_page_header(rp: &RustyPipeQuery) -> Result<bool> {
    let channel = rp
        .channel_videos_tab("UCh8gHdtzO2tXd593_bjErWg", ChannelVideoTab::Shorts)
        .await?;
    Ok(channel.video_count.is_some())
}

pub async fn music_playlist_two_column(rp: &RustyPipeQuery) -> Result<bool> {
    let id = "VLRDCLAK5uy_kb7EBi6y3GrtJri4_ZH56Ms786DFEimbM";
    let res = rp
        .raw(
            ClientType::DesktopMusic,
            "browse",
            &QBrowse {
                browse_id: id,
                params: None,
            },
        )
        .await?;
    Ok(res.contains("\"musicResponsiveHeaderRenderer\""))
}

pub async fn comments_framework_update(rp: &RustyPipeQuery) -> Result<bool> {
    let continuation =
        "Eg0SC3dMZHBSN2d1S3k4GAYyJSIRIgt3TGRwUjdndUt5ODAAeAJCEGNvbW1lbnRzLXNlY3Rpb24%3D";
    let res = rp
        .raw(ClientType::Desktop, "next", &QCont { continuation })
        .await?;
    Ok(res.contains("\"frameworkUpdates\""))
}

pub async fn channel_shorts_lockup(rp: &RustyPipeQuery) -> Result<bool> {
    let id = "UCh8gHdtzO2tXd593_bjErWg";
    let res = rp
        .raw(
            ClientType::Desktop,
            "browse",
            &QBrowse {
                browse_id: id,
                params: Some("EgZzaG9ydHPyBgUKA5oBAA%3D%3D"),
            },
        )
        .await?;
    Ok(res.contains("\"shortsLockupViewModel\""))
}

pub async fn playlist_page_header_renderer(rp: &RustyPipeQuery) -> Result<bool> {
    let id = "VLPLZN_exA7d4RVmCQrG5VlWIjMOkMFZVVOc";
    let res = rp
        .raw(
            ClientType::Desktop,
            "browse",
            &QBrowse {
                browse_id: id,
                params: None,
            },
        )
        .await?;
    Ok(res.contains("\"pageHeaderRenderer\""))
}

pub async fn channel_playlists_lockup(rp: &RustyPipeQuery) -> Result<bool> {
    let id = "UC2DjFE7Xf11URZqWBigcVOQ";
    let res = rp
        .raw(
            ClientType::Desktop,
            "browse",
            &QBrowse {
                browse_id: id,
                params: Some("EglwbGF5bGlzdHMgAQ%3D%3D"),
            },
        )
        .await?;
    Ok(res.contains("\"lockupViewModel\""))
}

pub async fn music_playlist_facepile(rp: &RustyPipeQuery) -> Result<bool> {
    let id = "VLPL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe";
    let res = rp
        .raw(
            ClientType::DesktopMusic,
            "browse",
            &QBrowse {
                browse_id: id,
                params: None,
            },
        )
        .await?;
    Ok(res.contains("\"facepile\""))
}
