use std::collections::BTreeMap;
use std::fmt::Write;
use std::fs::File;
use std::io::BufReader;

use path_macro::path;
use reqwest::header;
use reqwest::Client;
use serde::Deserialize;
use serde_with::serde_as;
use serde_with::VecSkipError;

use crate::model::Text;
use crate::util::DICT_DIR;
use crate::util::SRC_DIR;

#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LanguageMenu {
    #[serde_as(as = "VecSkipError<_>")]
    actions: Vec<ActionWrap>,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ActionWrap {
    open_popup_action: OpenPopupAction,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct OpenPopupAction {
    popup: Popup,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Popup {
    multi_page_menu_renderer: MultiPageMenuRenderer<MenuSectionRenderer>,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MultiPageMenuRenderer<T> {
    sections: Vec<MenuSectionRendererWrap<T>>,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MenuSectionRendererWrap<T> {
    multi_page_menu_section_renderer: T,
}

#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MenuSectionRenderer {
    #[serde_as(as = "VecSkipError<_>")]
    items: Vec<CompactLinkRendererWrap>,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CompactLinkRendererWrap {
    compact_link_renderer: CompactLinkRenderer,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CompactLinkRenderer {
    icon: Icon,
    service_endpoint: ServiceEndpoint<MenuAction>,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Icon {
    pub icon_type: String,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ServiceEndpoint<T> {
    signal_service_endpoint: SignalServiceEndpoint<T>,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SignalServiceEndpoint<T> {
    actions: Vec<T>,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MenuAction {
    get_multi_page_menu_action: MultiPageMenuAction,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MultiPageMenuAction {
    menu: Menu,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Menu {
    multi_page_menu_renderer: MultiPageMenuRenderer<ItemSectionRenderer>,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ItemSectionRenderer {
    items: Vec<LanguageItemWrap>,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LanguageItemWrap {
    compact_link_renderer: LanguageItem,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LanguageItem {
    title: Text,
    service_endpoint: ServiceEndpoint<LanguageCountryAction>,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LanguageCountryAction {
    #[serde(alias = "selectCountryCommand")]
    select_language_command: LanguageCountryCommand,
}

#[derive(Clone, Debug, Deserialize)]
struct LanguageCountryCommand {
    #[serde(alias = "gl")]
    hl: String,
}

pub async fn generate_locales() {
    let (languages, countries) = get_locales().await;

    let json_path = path!(*DICT_DIR / "lang_names.json");
    let json_file = File::open(json_path).unwrap();
    let lang_names: BTreeMap<String, String> =
        serde_json::from_reader(BufReader::new(json_file)).unwrap();

    let code_head = r#"// This file is automatically generated. DO NOT EDIT.

//! Languages and countries

use std::str::FromStr;

use serde::{Deserialize, Serialize};

use crate::error::Error;
"#;

    let code_foot = r#"impl FromStr for Language {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut sub = s;
        loop {
            if let Ok(v) = serde_plain::from_str(sub) {
                return Ok(v);
            }
            match sub.rfind('-') {
                Some(pos) => {
                    sub = &sub[..pos];
                }
                None => return Err(Error::Other("could not parse language `{s}`".into())),
            }
        }
    }
}

serde_plain::derive_display_from_serialize!(Language);

serde_plain::derive_fromstr_from_deserialize!(Country, Error);
serde_plain::derive_display_from_serialize!(Country);
"#;

    let mut code_langs = r#"/// Available languages
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum Language {
"#
    .to_owned();

    let mut code_countries = r#"/// Available countries
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "UPPERCASE")]
#[non_exhaustive]
pub enum Country {
"#
    .to_owned();

    let mut code_lang_array = format!(
        r#"/// Array of all available languages
/// The languages are sorted by their native names. This array can be used to display
/// a language selection or to get the language code from a language name using binary search.
pub const LANGUAGES: [Language; {}] = [
"#,
        languages.len()
    );
    let mut code_country_array = format!(
        r#"/// Array of all available countries
///
/// The countries are sorted by their english names. This array can be used to display
/// a country selection or to get the country code from a country name using binary search.
pub const COUNTRIES: [Country; {}] = [
"#,
        countries.len()
    );

    let mut code_lang_names = r#"impl Language {
    /// Get the native name of the language
    ///
    /// Examples: "English (US)", "Deutsch", "中文 (简体)"
    pub fn name(&self) -> &str {
        match self {
"#
    .to_owned();
    let mut code_country_names = r#"impl Country {
    /// Get the English name of the country
    ///
    /// Examples: "United States", "Germany"
    pub fn name(&self) -> &str {
        match self {
"#
    .to_owned();

    for (code, native_name) in &languages {
        let enum_name = code.split('-').fold(String::new(), |mut output, c| {
            let _ = write!(
                output,
                "{}{}",
                c[0..1].to_owned().to_uppercase(),
                c[1..].to_owned().to_lowercase()
            );
            output
        });

        let en_name = lang_names.get(code).expect(code);

        // Language enum
        if en_name == native_name || code.starts_with("en") {
            write!(code_langs, "    /// {native_name}\n    ").unwrap();
        } else {
            write!(code_langs, "    /// {en_name} / {native_name}\n    ").unwrap();
        }
        if code.contains('-') {
            write!(code_langs, "#[serde(rename = \"{code}\")]\n    ").unwrap();
        }
        code_langs += &enum_name;
        code_langs += ",\n";

        // Language names
        writeln!(
            code_lang_names,
            "            Language::{enum_name} => \"{native_name}\","
        )
        .unwrap();
    }
    code_langs += "}\n";

    // Language array
    let languages_by_name = languages
        .iter()
        .map(|(k, v)| (v, k))
        .collect::<BTreeMap<_, _>>();
    for code in languages_by_name.values() {
        let enum_name = code.split('-').fold(String::new(), |mut output, c| {
            let _ = write!(
                output,
                "{}{}",
                c[0..1].to_owned().to_uppercase(),
                c[1..].to_owned().to_lowercase()
            );
            output
        });
        writeln!(code_lang_array, "    Language::{enum_name},").unwrap();
    }

    for (c, n) in &countries {
        let enum_name = c[0..1].to_owned().to_uppercase() + &c[1..].to_owned().to_lowercase();

        // Country enum
        writeln!(code_countries, "    /// {n}").unwrap();
        writeln!(code_countries, "    {enum_name},").unwrap();

        // Country names
        writeln!(
            code_country_names,
            "            Country::{enum_name} => \"{n}\","
        )
        .unwrap();
    }

    // Country array
    let countries_by_name = countries
        .iter()
        .map(|(k, v)| (v, k))
        .collect::<BTreeMap<_, _>>();
    for c in countries_by_name.values() {
        let enum_name = c[0..1].to_owned().to_uppercase() + &c[1..].to_owned().to_lowercase();
        writeln!(code_country_array, "    Country::{enum_name},").unwrap();
    }

    // Add Country::Zz / Global
    code_countries += "    /// Global (can only be used for music charts)\n";
    code_countries += "    Zz,\n";
    code_country_names += "            Country::Zz => \"Global\",\n";

    code_countries += "}\n";

    code_lang_array += "];\n";
    code_country_array += "];\n";
    code_lang_names += "        }\n    }\n}\n";
    code_country_names += "        }\n    }\n}\n";

    let code = format!(
        "{code_head}\n{code_langs}\n{code_countries}\n{code_lang_array}\n{code_country_array}\n{code_lang_names}\n{code_country_names}\n{code_foot}"
    );

    let target_path = path!(*SRC_DIR / "param" / "locale.rs");
    std::fs::write(target_path, code).unwrap();
}

async fn get_locales() -> (BTreeMap<String, String>, BTreeMap<String, String>) {
    let client = Client::new();
    let resp = client
        .post("https://www.youtube.com/youtubei/v1/account/account_menu?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8&prettyPrint=false")
        .header(header::CONTENT_TYPE, "application/json")
        .body(
            r#"{"context":{"client":{"clientName":"WEB","clientVersion":"2.20220914.06.00","platform":"DESKTOP","originalUrl":"https://www.youtube.com/","hl":"en","gl":"US"},"request":{"internalExperimentFlags":[],"useSsl":true},"user":{"lockedSafetyMode":false}}}"#
        )
        .send().await
        .unwrap()
        .error_for_status()
        .unwrap();

    let language_menu = resp.json::<LanguageMenu>().await.unwrap();

    let lm_section = &language_menu.actions[0]
        .open_popup_action
        .popup
        .multi_page_menu_renderer
        .sections
        .iter()
        .find(|s| s.multi_page_menu_section_renderer.items.len() >= 2)
        .unwrap();

    let lang_section = lm_section
        .multi_page_menu_section_renderer
        .items
        .iter()
        .find(|s| s.compact_link_renderer.icon.icon_type == "TRANSLATE")
        .unwrap();

    let country_section = lm_section
        .multi_page_menu_section_renderer
        .items
        .iter()
        .find(|s| s.compact_link_renderer.icon.icon_type == "LANGUAGE")
        .unwrap();

    let languages = map_language_section(lang_section);
    let countries = map_language_section(country_section);

    (languages, countries)
}

fn map_language_section(section: &CompactLinkRendererWrap) -> BTreeMap<String, String> {
    section
        .compact_link_renderer
        .service_endpoint
        .signal_service_endpoint
        .actions[0]
        .get_multi_page_menu_action
        .menu
        .multi_page_menu_renderer
        .sections[0]
        .multi_page_menu_section_renderer
        .items
        .iter()
        .map(|i| {
            (
                i.compact_link_renderer
                    .service_endpoint
                    .signal_service_endpoint
                    .actions[0]
                    .select_language_command
                    .hl
                    .clone(),
                i.compact_link_renderer.title.text.clone(),
            )
        })
        .collect()
}
