use heck::*;
use indexmap::IndexMap;

const KEYWORDS: &[&str] = &[
    "type", "match", "enum", "struct", "fn", "mod", "pub", "use", "crate", "self", "super", "as",
    "in", "let", "mut", "ref", "static", "trait", "where", "box",
];

#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
pub struct JellyfinOpenapi {
    components: Components,
}

#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
pub struct Components {
    schemas: indexmap::IndexMap<String, Schema>,
}

#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
pub struct Schema {
    #[serde(rename = "type")]
    _type: Types,
    properties: Option<indexmap::IndexMap<String, Property>>,
    #[serde(rename = "oneOf")]
    one_of: Option<Vec<RefName>>,
    #[serde(rename = "enum")]
    _enum: Option<Vec<String>>,
    description: Option<String>,
}

#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
pub struct RefName {
    #[serde(rename = "$ref")]
    _ref: String,
}

impl RefName {
    pub fn get_type_name(&self) -> String {
        self._ref.split('/').last().unwrap().to_string()
    }
}

#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
pub struct Property {
    #[serde(rename = "type")]
    _type: Option<Types>,
    nullable: Option<bool>,
    format: Option<String>,
    items: Option<Box<Property>>,
    properties: Option<indexmap::IndexMap<String, Property>>,
    #[serde(rename = "additionalProperties")]
    additional_properties: Option<Box<Property>>,
    #[serde(rename = "enum")]
    _enum: Option<Vec<String>>,
    #[serde(rename = "allOf")]
    all_of: Option<Vec<RefName>>,
    #[serde(rename = "oneOf")]
    one_of: Option<Vec<RefName>>,
    description: Option<String>,
    #[serde(rename = "$ref")]
    _ref: Option<String>,
}

impl Property {
    pub fn to_string(&self) -> String {
        let out = match self._type {
            Some(Types::String) => match self.format.as_deref() {
                Some("uuid") => "uuid::Uuid".to_string(),
                Some("date-time") => "jiff::Timestamp".to_string(),
                _ => "String".to_string(),
            },
            Some(Types::Integer) => match self.format.as_deref() {
                Some("int32") => "i32".to_string(),
                Some("int64") => "i64".to_string(),
                _ => "i32".to_string(),
            },
            Some(Types::Boolean) => "bool".to_string(),
            Some(Types::Number) => match self.format.as_deref() {
                Some("double") => "f64".to_string(),
                Some("float") => "f32".to_string(),
                _ => "f64".to_string(),
            },
            Some(Types::Array) => {
                if let Some(ref items) = self.items {
                    format!("Vec<{}>", items.to_string())
                } else {
                    "Vec<()>".to_string()
                }
            }
            Some(Types::Object) => {
                if let Some(properties) = &self.additional_properties {
                    format!(
                        "std::collections::HashMap<String, {}>",
                        properties.to_string()
                    )
                // } else if let Some(props) = &self.properties {
                // todo!()
                } else {
                    "std::collections::HashMap<String, serde_json::Value>".to_string()
                }
            }

            None => {
                if let Some(ref _ref) = self._ref {
                    _ref.split('/').last().unwrap().to_string()
                } else if let Some(ref all_of) = self.all_of {
                    all_of[0].get_type_name()
                } else {
                    "()".into()
                }
            }
        };
        if let Some(true) = self.nullable {
            format!("Option<{}>", out)
        } else if self.nullable.is_none() && self._type == Some(Types::Object) {
            format!("Option<{}>", out)
        } else {
            out
        }
    }
    pub fn description(&self) -> Option<String> {
        self.description.clone()
    }
}

#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Types {
    Object,
    String,
    Boolean,
    Array,
    Integer,
    Number,
}

fn main() {
    let json = include_str!("../../jellyfin.json");
    let jellyfin_openapi: JellyfinOpenapi = serde_json::from_str(json).unwrap();
    let structs: IndexMap<String, Schema> = jellyfin_openapi
        .components
        .schemas
        .iter()
        .filter(|(_k, v)| v.properties.is_some())
        .map(|(k, v)| (k.clone(), v.clone()))
        .collect();
    let enums: IndexMap<String, Schema> = jellyfin_openapi
        .components
        .schemas
        .iter()
        .filter(|(_, v)| v._enum.is_some())
        .map(|(k, v)| (k.clone(), v.clone()))
        .collect();

    let syn_structs: Vec<syn::Item> = structs
        .iter()
        .map(|(key, value)| generate_struct(key, value))
        .flatten()
        .collect();

    let syn_enums = enums
        .iter()
        .map(|(key, value)| {
            let variants = value
                ._enum
                .as_ref()
                .expect("Possible oneOf")
                .iter()
                .map(|variant| {
                    let og_variant = variant.clone();
                    let name =
                        syn::Ident::new(&variant.to_pascal_case(), proc_macro2::Span::call_site());
                    syn::parse_quote! {
                        #[serde(rename = #og_variant)]
                        #name
                    }
                })
                .collect::<Vec<syn::Variant>>();
            let key = modify_keyword(key);
            let key = syn::Ident::new(&key.to_pascal_case(), proc_macro2::Span::call_site());
            let desc = value.description.clone();
            let tokens = if let Some(desc) = desc {
                let desc = format!(" {}", desc);
                quote::quote! {
                    #[doc = #desc]
                    #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
                    pub enum #key {
                        #(#variants),*
                    }
                }
            } else {
                quote::quote! {
                    #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
                    pub enum #key {
                        #(#variants),*
                    }
                }
            };
            syn::parse2(tokens).expect("Failed to parse enum")
        })
        .collect::<Vec<syn::ItemEnum>>();

    let file = syn::File {
        shebang: None,
        attrs: vec![],
        items: syn_structs
            .into_iter()
            .chain(syn_enums.into_iter().map(syn::Item::Enum))
            .collect(),
    };
    let code = prettyplease::unparse(&file);
    std::fs::write("jellyfin.rs", code).expect("Unable to write file");
}

fn modify_keyword(name: &str) -> String {
    if KEYWORDS.contains(&name) {
        format!("_{}", name)
    } else {
        name.to_string()
    }
}

fn generate_struct(key: impl AsRef<str>, value: &Schema) -> Vec<syn::Item> {
    let key = key.as_ref();

    let extra_structs = value
        .properties
        .as_ref()
        .expect("Possible properties")
        .iter()
        .filter_map(|(name, property)| {
            if property._type == Some(Types::Object) && property.properties.is_some() {
                Some(generate_struct(
                    &format!("{}_{}", key, name),
                    &Schema {
                        _type: Types::Object,
                        properties: property.properties.clone(),
                        one_of: None,
                        _enum: None,
                        description: property.description.clone(),
                    },
                ))
            } else {
                None
            }
        })
        .flatten()
        .collect::<Vec<syn::Item>>();

    let fields = value
        .properties
        .as_ref()
        .expect("Possible properties")
        .iter()
        .map(|(name, property)| {
            let nested_struct =
                property._type == Some(Types::Object) && property.properties.is_some();
            let og_name = name.clone();
            let name = modify_keyword(&name.to_snake_case());
            let _type_desc = property.description();
            let _type_desc = if let Some(desc) = &_type_desc {
                Some(format!(" {}", desc))
            } else {
                None
            };
            let _type = if !nested_struct {
                property.to_string()
            } else {
                format!("{}_{}", key, og_name).to_pascal_case()
            };
            let _type = if _type.contains(key) {
                _type.replace(&format!("<{}>", key), format!("<Box<{}>>", key).as_str())
            } else {
                _type
            };
            syn::Field {
                attrs: if let Some(desc) = _type_desc {
                    syn::parse_quote! {
                        #[doc = #desc]
                        #[serde(rename = #og_name)]
                    }
                } else {
                    syn::parse_quote! {
                        #[serde(rename = #og_name)]
                    }
                },
                mutability: syn::FieldMutability::None,
                vis: syn::Visibility::Public(syn::Token![pub](proc_macro2::Span::call_site())),
                ident: Some(syn::Ident::new(&name, proc_macro2::Span::call_site())),
                colon_token: Some(syn::token::Colon(proc_macro2::Span::call_site())),
                ty: syn::parse_str(&_type).expect("Failed to parse type"),
            }
        })
        .collect::<Vec<syn::Field>>();

    let key = modify_keyword(key);
    let desc = value.description.clone();
    let key = syn::Ident::new(&key.to_pascal_case(), proc_macro2::Span::call_site());
    let tokens = if let Some(desc) = desc {
        let desc = format!(" {}", desc);
        quote::quote! {
            #[doc = #desc]
            #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
            pub struct #key {
                #(#fields),*
            }
        }
    } else {
        quote::quote! {
            #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
            pub struct #key {
                #(#fields),*
            }
        }
    };

    let mut out = syn::parse2::<syn::File>(tokens)
        .expect("Failed to parse struct")
        .items;
    out.extend(extra_structs);
    out
}
