use std::collections::BTreeMap;

use proc_macro2::TokenStream;
use quote::quote;
use syn::{
    parse::{Parse, ParseStream},
    parse_quote, Block, ExprStruct, Ident, Path, Token,
};

pub fn write_api_impl(input: Options) -> Block {
    let api_object = generate_api_impl(&input);
    let name = input.name;

    parse_quote! {
        {
            #[cfg(target_arch = "wasm32")]
            compile_error!("can't compile schema generator for the `wasm32` arch\nhint: are you trying to compile a smart contract without specifying `--lib`?");
            use ::std::env;
            use ::std::fs::{create_dir_all, write};

            use ::cosmwasm_schema::{remove_schemas, Api, QueryResponses};

            let mut out_dir = env::current_dir().unwrap();
            out_dir.push("schema");
            create_dir_all(&out_dir).unwrap();
            remove_schemas(&out_dir).unwrap();

            let api = #api_object.render();


            let path = out_dir.join(concat!(#name, ".json"));

            let json = api.to_string().unwrap();
            write(&path, json + "\n").unwrap();
            println!("Exported the full API as {}", path.to_str().unwrap());

            let raw_dir = out_dir.join("raw");
            create_dir_all(&raw_dir).unwrap();

            for (filename, json) in api.to_schema_files().unwrap() {
                let path = raw_dir.join(filename);

                write(&path, json + "\n").unwrap();
                println!("Exported {}", path.to_str().unwrap());
            }
        }
    }
}

pub fn generate_api_impl(input: &Options) -> ExprStruct {
    let Options {
        name,
        version,
        instantiate,
        execute,
        query,
        migrate,
        sudo,
        responses,
    } = input;

    parse_quote! {
        ::cosmwasm_schema::Api {
            contract_name: #name.to_string(),
            contract_version: #version.to_string(),
            instantiate: ::cosmwasm_schema::schema_for!(#instantiate),
            execute: #execute,
            query: #query,
            migrate: #migrate,
            sudo: #sudo,
            responses: #responses,
        }
    }
}

#[derive(Debug)]
enum Value {
    Type(syn::Path),
    Str(syn::LitStr),
}

impl Value {
    fn unwrap_type(self) -> syn::Path {
        if let Self::Type(p) = self {
            p
        } else {
            panic!("expected a type");
        }
    }

    fn unwrap_str(self) -> syn::LitStr {
        if let Self::Str(s) = self {
            s
        } else {
            panic!("expected a string literal");
        }
    }
}

impl Parse for Value {
    fn parse(input: ParseStream) -> syn::parse::Result<Self> {
        if let Ok(p) = input.parse::<syn::Path>() {
            Ok(Self::Type(p))
        } else {
            Ok(Self::Str(input.parse::<syn::LitStr>()?))
        }
    }
}

#[derive(Debug)]
struct Pair((Ident, Value));

impl Parse for Pair {
    fn parse(input: ParseStream) -> syn::parse::Result<Self> {
        let k = input.parse::<syn::Ident>()?;
        input.parse::<Token![:]>()?;
        let v = input.parse::<Value>()?;

        Ok(Self((k, v)))
    }
}

#[derive(Debug)]
pub struct Options {
    name: TokenStream,
    version: TokenStream,
    instantiate: Path,
    execute: TokenStream,
    query: TokenStream,
    migrate: TokenStream,
    sudo: TokenStream,
    responses: TokenStream,
}

impl Parse for Options {
    fn parse(input: ParseStream) -> syn::parse::Result<Self> {
        let pairs = input.parse_terminated::<Pair, Token![,]>(Pair::parse)?;
        let mut map: BTreeMap<_, _> = pairs.into_iter().map(|p| p.0).collect();

        let name = if let Some(name_override) = map.remove(&parse_quote!(name)) {
            let name_override = name_override.unwrap_str();
            quote! {
                #name_override
            }
        } else {
            quote! {
                ::std::env!("CARGO_PKG_NAME")
            }
        };

        let version = if let Some(version_override) = map.remove(&parse_quote!(version)) {
            let version_override = version_override.unwrap_str();
            quote! {
                #version_override
            }
        } else {
            quote! {
                ::std::env!("CARGO_PKG_VERSION")
            }
        };

        let instantiate = map
            .remove(&parse_quote!(instantiate))
            .unwrap()
            .unwrap_type();

        let execute = match map.remove(&parse_quote!(execute)) {
            Some(ty) => {
                let ty = ty.unwrap_type();
                quote! {Some(::cosmwasm_schema::schema_for!(#ty))}
            }
            None => quote! { None },
        };

        let (query, responses) = match map.remove(&parse_quote!(query)) {
            Some(ty) => {
                let ty = ty.unwrap_type();
                (
                    quote! {Some(::cosmwasm_schema::schema_for!(#ty))},
                    quote! { Some(<#ty as ::cosmwasm_schema::QueryResponses>::response_schemas().unwrap()) },
                )
            }
            None => (quote! { None }, quote! { None }),
        };

        let migrate = match map.remove(&parse_quote!(migrate)) {
            Some(ty) => {
                let ty = ty.unwrap_type();
                quote! {Some(::cosmwasm_schema::schema_for!(#ty))}
            }
            None => quote! { None },
        };

        let sudo = match map.remove(&parse_quote!(sudo)) {
            Some(ty) => {
                let ty = ty.unwrap_type();
                quote! {Some(::cosmwasm_schema::schema_for!(#ty))}
            }
            None => quote! { None },
        };

        if let Some((invalid_option, _)) = map.into_iter().next() {
            panic!("unknown generate_api option: {invalid_option}");
        }

        Ok(Self {
            name,
            version,
            instantiate,
            execute,
            query,
            migrate,
            sudo,
            responses,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn api_object_minimal() {
        assert_eq!(
            generate_api_impl(&parse_quote! {
                instantiate: InstantiateMsg,
            }),
            parse_quote! {
                ::cosmwasm_schema::Api {
                    contract_name: ::std::env!("CARGO_PKG_NAME").to_string(),
                    contract_version: ::std::env!("CARGO_PKG_VERSION").to_string(),
                    instantiate: ::cosmwasm_schema::schema_for!(InstantiateMsg),
                    execute: None,
                    query: None,
                    migrate: None,
                    sudo: None,
                    responses: None,
                }
            }
        );
    }

    #[test]
    fn api_object_name_vesion_override() {
        assert_eq!(
            generate_api_impl(&parse_quote! {
                name: "foo",
                version: "bar",
                instantiate: InstantiateMsg,
            }),
            parse_quote! {
                ::cosmwasm_schema::Api {
                    contract_name: "foo".to_string(),
                    contract_version: "bar".to_string(),
                    instantiate: ::cosmwasm_schema::schema_for!(InstantiateMsg),
                    execute: None,
                    query: None,
                    migrate: None,
                    sudo: None,
                    responses: None,
                }
            }
        );
    }

    #[test]
    fn api_object_all_msgs() {
        assert_eq!(
            generate_api_impl(&parse_quote! {
                instantiate: InstantiateMsg,
                execute: ExecuteMsg,
                query: QueryMsg,
                migrate: MigrateMsg,
                sudo: SudoMsg,
            }),
            parse_quote! {
                ::cosmwasm_schema::Api {
                    contract_name: ::std::env!("CARGO_PKG_NAME").to_string(),
                    contract_version: ::std::env!("CARGO_PKG_VERSION").to_string(),
                    instantiate: ::cosmwasm_schema::schema_for!(InstantiateMsg),
                    execute: Some(::cosmwasm_schema::schema_for!(ExecuteMsg)),
                    query: Some(::cosmwasm_schema::schema_for!(QueryMsg)),
                    migrate: Some(::cosmwasm_schema::schema_for!(MigrateMsg)),
                    sudo: Some(::cosmwasm_schema::schema_for!(SudoMsg)),
                    responses: Some(<QueryMsg as ::cosmwasm_schema::QueryResponses>::response_schemas().unwrap()),
                }
            }
        );
    }

    #[test]
    #[should_panic(expected = "unknown generate_api option: asd")]
    fn invalid_option() {
        let _options: Options = parse_quote! {
            instantiate: InstantiateMsg,
            asd: Asd,
        };
    }
}