#[doc = include_str!("../README.md")]
use std::collections::HashMap;

use proc_macro::TokenStream;
use quote::quote;
use titlecase::titlecase;
use url::Url;

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Info {
    documentation: String,
    api_scope: String,
    valid_rust_name: String,
    original_name: String,
}

/// Checks if a line should be skipped (it's a header line)
fn is_header_line(line: &str) -> bool {
    line == "Scope\tDescription"
}

/// Checks if a line contains a scope reference
fn is_scope_line(line: &str, check: &[&str]) -> bool {
    check.iter().any(|keyword| line.contains(keyword))
}

/// Generates a valid Rust identifier from a scope URL/name
fn generate_identifier_from_scope(api_scope_thing: &str) -> String {
    if !api_scope_thing.contains("http") {
        // Not a URL scope
        titlecase(api_scope_thing)
    } else {
        // URL scope - extract and process the path
        match Url::parse(api_scope_thing) {
            Ok(parsed) => {
                let paths: Vec<&str> = parsed.path_segments().unwrap().collect();
                if paths.len() >= 2 {
                    let paths_slice = &paths[paths.len() - 2..];
                    let joined = paths_slice.join(" ");
                    let processed = joined
                        .replace("-", " ")
                        .replace(".", " ")
                        .replace("read", "Read")
                        .replace("only", "Only")
                        .replace("(", "")
                        .replace(")", "")
                        .replace("@", "At");
                    let titlecased = titlecase(&processed);
                    titlecased.split_whitespace().collect::<String>()
                } else if api_scope_thing.contains("mail") {
                    "GMail".to_string()
                } else {
                    panic!("Weird scope: {api_scope_thing}")
                }
            }
            Err(_) => panic!("Failed to parse URL: {api_scope_thing}"),
        }
    }
}

/// Generates a valid Rust identifier from a header line
fn generate_header_identifier(line: &str) -> String {
    line.replace(",", "")
        .replace("-", "")
        .replace(".", "Point")
        .replace("(", "")
        .replace(")", "")
        .replace("/", "")
        .replace("&", "")
        .replace("@", "At")
        .split(" ")
        .map(|e| e.to_string())
        .collect::<Vec<String>>()
        .join("")
}

/// Parses a scope line into scope and documentation
fn parse_scope_line(line: &str) -> (String, String) {
    let mut split = line.split("\t");
    let scope = split.next().unwrap_or("").to_string();
    let doc = split.next().unwrap_or("").to_string();
    (scope, doc)
}

/// Collects all scopes from the content indexed by header
fn collect_grouped_scopes(content: &str, check: &[&str]) -> HashMap<String, Vec<Info>> {
    let mut scopes_by_header: HashMap<String, Vec<Info>> = HashMap::new();
    let mut current_header = String::new();
    let mut original_header_name = String::new();

    for line in content.lines() {
        if is_header_line(line) {
            continue;
        }

        if is_scope_line(line, check) {
            let (scope, doc) = parse_scope_line(line);
            let identifier = generate_identifier_from_scope(&scope);
            scopes_by_header
                .entry(current_header.clone())
                .or_insert_with(Vec::new)
                .push(Info {
                    documentation: doc,
                    api_scope: scope,
                    valid_rust_name: identifier,
                    original_name: original_header_name.clone(),
                });
        } else if !line.is_empty() && line.chars().next().unwrap().is_ascii_uppercase() {
            current_header = generate_header_identifier(line);
            original_header_name = line.to_string();
        }
    }

    scopes_by_header
}

/// Generates code for grouped scope enums
fn generate_grouped_scope_code(
    scopes_by_header: &HashMap<String, Vec<Info>>,
) -> (Vec<proc_macro2::TokenStream>, Vec<proc_macro2::TokenStream>, Vec<proc_macro2::TokenStream>) {
    let mut enums = Vec::new();
    let mut impl_scope_to = Vec::new();
    let mut impl_scope_from = Vec::new();

    for (header, infos) in scopes_by_header {
        let name = syn::Ident::new(header, proc_macro2::Span::call_site());
        let variants = infos
            .iter()
            .map(|i| syn::Ident::new(&i.valid_rust_name, proc_macro2::Span::call_site()))
            .collect::<Vec<proc_macro2::Ident>>();
        let scope_variants = infos.iter().map(|i| &i.api_scope).collect::<Vec<&String>>();
        let doc_variants = infos
            .iter()
            .map(|i| {
                let fd = format!("Documentation: {}, Scope: {}", i.documentation, i.api_scope);
                quote! {
                    #[doc = #fd]
                }
            })
            .collect::<Vec<proc_macro2::TokenStream>>();
        let orig = infos.first().unwrap().original_name.clone();
        let enum_doc = quote! {
            #[doc = #orig]
        };

        enums.push(quote! {
            #enum_doc
            #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
            pub enum #name {
                #(#doc_variants
                #variants),*
            }
        });

        impl_scope_to.push(quote! {
            impl ToGoogleScope for #name {
                fn to_google_scope(&self) -> &'static str {
                    match self {
                        #(Self::#variants => #scope_variants),*
                    }
                }
            }
        });

        impl_scope_from.push(quote! {
            impl FromGoogleScope<#name> for #name {
                fn from_google_scope(google_scope: &str) -> Result<#name, ()> {
                    match google_scope {
                        #(scope_variants => Ok(#name::#variants)),*,
                        _ => Err(())
                    }
                }
            }
        });
    }

    (enums, impl_scope_to, impl_scope_from)
}

/// Collects all scopes into a flat map
fn collect_flat_scopes(content: &str, check: &[&str]) -> HashMap<String, (String, String, String)> {
    let mut scopes_map: HashMap<String, (String, String, String)> = HashMap::new();

    for line in content.lines() {
        if is_header_line(line) {
            continue;
        }

        if is_scope_line(line, check) {
            let (scope, doc) = parse_scope_line(line);
            let identifier = generate_identifier_from_scope(&scope);
            scopes_map.insert(scope.clone(), (identifier, doc, scope));
        }
    }

    scopes_map
}

/// Generates code for flat scope enum
fn generate_flat_scope_code(
    scopes_map: &HashMap<String, (String, String, String)>,
) -> (proc_macro2::TokenStream, proc_macro2::TokenStream, proc_macro2::TokenStream) {
    let enum_variants = scopes_map
        .iter()
        .map(|(_, (valided_rust, doc, api_scope_thing))| {
            let fd = format!("Documentation: {doc}, Scope: {api_scope_thing}");
            let variant = syn::Ident::new(valided_rust, proc_macro2::Span::call_site());
            quote! {
                #[doc = #fd]
                #variant,
            }
        });

    let to_google_scope_impl = scopes_map
        .iter()
        .map(|(_, (valided_rust, _, api_scope_thing))| {
            let variant = syn::Ident::new(valided_rust, proc_macro2::Span::call_site());
            quote! {
                Scopes::#variant => #api_scope_thing,
            }
        });

    let from_google_scope_impl = scopes_map
        .iter()
        .map(|(_, (valided_rust, _, api_scope_thing))| {
            let variant = syn::Ident::new(valided_rust, proc_macro2::Span::call_site());
            quote! {
                #api_scope_thing => Ok(Scopes::#variant),
            }
        });

    let cloned_from_google_scope_impl = from_google_scope_impl.clone();

    let enum_tokens = quote! {
        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
        pub enum Scopes {
            #(#enum_variants)*
        }
    };

    let to_scope_impl = quote! {
        impl ToGoogleScope for Scopes {
            fn to_google_scope(&self) -> &'static str {
                match self {
                    #(#to_google_scope_impl)*
                }
            }
        }
    };

    let from_scope_impl = quote! {
        impl FromGoogleScope<Scopes> for Scopes {
            fn from_google_scope(google_scope: &str) -> Result<Scopes, ()> {
                match google_scope {
                    #(#from_google_scope_impl)*
                    _ => Err(()),
                }
            }
        }

        impl FromStr for Scopes {
            type Err = ();

            fn from_str(s: &str) -> Result<Self, Self::Err> {
                match s {
                    #(#cloned_from_google_scope_impl)*
                    _ => Err(()),
                }
            }
        }
    };

    (enum_tokens, to_scope_impl, from_scope_impl)
}

#[proc_macro]
/// A procedural macro for generating group scope enums
pub fn generate_grouped_scopes_enums(_: TokenStream) -> TokenStream {
    let trait_for_scopes = quote! {
        use tlns_google_oauth2_traits::{*};
    };

    let content = include_str!("../info.txt");
    let check = ["openid", "profile", "email", "http"];

    let scopes = collect_grouped_scopes(content, &check);
    let (enums, impl_scope_to, impl_scope_from) = generate_grouped_scope_code(&scopes);

    let output = quote! {
        #trait_for_scopes
        #(#enums)*
        #(#impl_scope_from)*
        #(#impl_scope_to)*
    };
    output.into()
}

#[proc_macro]
/// A procedural macro for generating scope enums
pub fn generate_scopes_enums(_: TokenStream) -> TokenStream {
    let trait_for_scopes = quote! {
        use tlns_google_oauth2_traits::{*};
        use std::str::FromStr;
    };

    let content = include_str!("../info.txt");
    let check = vec!["openid", "profile", "email", "http"];

    let scopes_map = collect_flat_scopes(content, &check);
    let (enum_tokens, to_scope_impl, from_scope_impl) = generate_flat_scope_code(&scopes_map);

    let expanded = quote! {
        #trait_for_scopes
        #enum_tokens
        #to_scope_impl
        #from_scope_impl
    };

    expanded.into()
}
