use proc_macro2::TokenStream;
use quote::quote;
use syn::{parse::{Parse, ParseStream}, braced, Attribute, Expr, Ident, LitStr, Token, Type};

pub struct EnvSchemaDef {
    name: Ident,
    prefix: String,
    fields: Vec<EnvField>,
}

struct EnvField {
    custom_env: Option<String>,
    name: Ident,
    ty: Type,
    default_value: Option<Expr>,
}

impl Parse for EnvSchemaDef {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let name: Ident = input.parse()?;
        let _comma: Token![,] = input.parse()?;
        let prefix_lit: LitStr = input.parse()?;
        let prefix = prefix_lit.value();

        let content;
        braced!(content in input);

        let mut fields = Vec::new();
        while !content.is_empty() {
            fields.push(content.parse::<EnvField>()?);
        }

        Ok(EnvSchemaDef { name, prefix, fields })
    }
}

impl Parse for EnvField {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let attrs = input.call(Attribute::parse_outer)?;
        let custom_env = extract_env_attr(&attrs);

        let name: Ident = input.parse()?;
        let _colon: Token![:] = input.parse()?;
        let ty: Type = input.parse()?;

        let default_value = if input.peek(Token![=]) {
            let _eq: Token![=] = input.parse()?;
            let expr: Expr = input.parse()?;
            Some(expr)
        } else {
            None
        };

        if input.peek(Token![,]) {
            let _comma: Token![,] = input.parse()?;
        }

        Ok(EnvField { custom_env, name, ty, default_value })
    }
}

fn extract_env_attr(attrs: &[Attribute]) -> Option<String> {
    for attr in attrs {
        if attr.path().is_ident("env") {
            let value: LitStr = attr.parse_args().ok()?;
            return Some(value.value());
        }
    }
    None
}

fn extract_option_inner(ty: &Type) -> Option<&Type> {
    if let Type::Path(tp) = ty {
        let seg = tp.path.segments.last()?;
        if seg.ident == "Option" {
            if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
                if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
                    return Some(inner);
                }
            }
        }
    }
    None
}

fn is_string_type(ty: &Type) -> bool {
    if let Type::Path(tp) = ty {
        if let Some(seg) = tp.path.segments.last() {
            return seg.ident == "String";
        }
    }
    false
}

fn is_bool_type(ty: &Type) -> bool {
    if let Type::Path(tp) = ty {
        if let Some(seg) = tp.path.segments.last() {
            return seg.ident == "bool";
        }
    }
    false
}

fn get_env_name(field: &EnvField, prefix: &str) -> String {
    match &field.custom_env {
        Some(custom) => custom.clone(),
        None => format!("{}_{}", prefix, field.name.to_string().to_uppercase()),
    }
}

pub fn expand(def: EnvSchemaDef) -> TokenStream {
    let struct_name = &def.name;

    let field_defs: Vec<_> = def.fields.iter().map(|f| {
        let name = &f.name;
        let ty = &f.ty;
        quote! { pub #name: #ty }
    }).collect();

    let env_names: Vec<String> = def.fields.iter().map(|f| get_env_name(f, &def.prefix)).collect();

    let field_bindings: Vec<_> = def.fields.iter().zip(env_names.iter()).map(|(f, env_name)| {
        gen_field_binding(f, env_name)
    }).collect();

    let field_names: Vec<_> = def.fields.iter().map(|f| &f.name).collect();

    let env_name_lits: Vec<_> = env_names.iter().map(|n| {
        syn::LitStr::new(n, proc_macro2::Span::call_site())
    }).collect();

    let prefix_underscore = format!("{}_", def.prefix);

    quote! {
        #[derive(Clone, Debug)]
        pub struct #struct_name {
            #(#field_defs,)*
        }

        impl #struct_name {
            /// Read and validate all environment variables. Panics with all errors if any fail.
            pub fn from_env() -> Self {
                let mut __errors: Vec<String> = Vec::new();

                #(#field_bindings)*

                // Detect unknown prefix variables
                {
                    let __known: std::collections::HashSet<String> = vec![
                        #(#env_name_lits.to_string(),)*
                    ].into_iter().collect();
                    let __prefix = #prefix_underscore;
                    for (__key, _) in std::env::vars() {
                        if __key.starts_with(__prefix) && !__known.contains(&__key) {
                            __errors.push(format!("Unknown env: {}", __key));
                        }
                    }
                }

                if !__errors.is_empty() {
                    panic!("\nEnvironment variable errors:\n{}", __errors.join("\n"));
                }

                Self {
                    #(#field_names,)*
                }
            }
        }
    }
}

fn gen_field_binding(field: &EnvField, env_name: &str) -> TokenStream {
    let var_name = &field.name;
    let env_lit = syn::LitStr::new(env_name, proc_macro2::Span::call_site());

    let is_option = extract_option_inner(&field.ty).is_some();

    if is_option {
        gen_option_field(field, var_name, &env_lit)
    } else if field.default_value.is_some() {
        gen_default_field(field, var_name, &env_lit)
    } else {
        gen_required_field(field, var_name, &env_lit)
    }
}

fn gen_option_field(field: &EnvField, var_name: &Ident, env_lit: &syn::LitStr) -> TokenStream {
    let inner_ty = extract_option_inner(&field.ty).unwrap();
    let field_ty = &field.ty;

    if is_string_type(inner_ty) {
        quote! {
            let #var_name: #field_ty = std::env::var(#env_lit).ok();
        }
    } else if is_bool_type(inner_ty) {
        quote! {
            let #var_name: #field_ty = match std::env::var(#env_lit) {
                Ok(__v) => match __v.as_str() {
                    "true" | "1" => Some(true),
                    "false" | "0" => Some(false),
                    _ => {
                        __errors.push(format!("{}='{}' is not a valid bool (expected true/false/1/0)", #env_lit, __v));
                        None
                    }
                },
                Err(_) => None,
            };
        }
    } else {
        // numeric Option
        quote! {
            let #var_name: #field_ty = match std::env::var(#env_lit) {
                Ok(__v) => match __v.parse::<#inner_ty>() {
                    Ok(__parsed) => Some(__parsed),
                    Err(_) => {
                        __errors.push(format!("{}='{}' is not a valid {}", #env_lit, __v, stringify!(#inner_ty)));
                        None
                    }
                },
                Err(_) => None,
            };
        }
    }
}

fn gen_default_field(field: &EnvField, var_name: &Ident, env_lit: &syn::LitStr) -> TokenStream {
    let default_expr = field.default_value.as_ref().unwrap();
    let ty = &field.ty;

    if is_string_type(ty) {
        quote! {
            let #var_name: #ty = match std::env::var(#env_lit) {
                Ok(__v) => __v,
                Err(_) => (#default_expr).into(),
            };
        }
    } else if is_bool_type(ty) {
        quote! {
            let #var_name: #ty = match std::env::var(#env_lit) {
                Ok(__v) => match __v.as_str() {
                    "true" | "1" => true,
                    "false" | "0" => false,
                    _ => {
                        __errors.push(format!("{}='{}' is not a valid bool (expected true/false/1/0)", #env_lit, __v));
                        #default_expr
                    }
                },
                Err(_) => #default_expr,
            };
        }
    } else {
        // numeric with default
        quote! {
            let #var_name: #ty = match std::env::var(#env_lit) {
                Ok(__v) => match __v.parse::<#ty>() {
                    Ok(__parsed) => __parsed,
                    Err(_) => {
                        __errors.push(format!("{}='{}' is not a valid {}", #env_lit, __v, stringify!(#ty)));
                        #default_expr
                    }
                },
                Err(_) => #default_expr,
            };
        }
    }
}

fn gen_required_field(field: &EnvField, var_name: &Ident, env_lit: &syn::LitStr) -> TokenStream {
    let ty = &field.ty;

    if is_string_type(ty) {
        quote! {
            let #var_name: #ty = match std::env::var(#env_lit) {
                Ok(__v) => __v,
                Err(_) => {
                    __errors.push(format!("Missing required env: {}", #env_lit));
                    String::new()
                }
            };
        }
    } else if is_bool_type(ty) {
        quote! {
            let #var_name: #ty = match std::env::var(#env_lit) {
                Ok(__v) => match __v.as_str() {
                    "true" | "1" => true,
                    "false" | "0" => false,
                    _ => {
                        __errors.push(format!("{}='{}' is not a valid bool (expected true/false/1/0)", #env_lit, __v));
                        false
                    }
                },
                Err(_) => {
                    __errors.push(format!("Missing required env: {}", #env_lit));
                    false
                }
            };
        }
    } else {
        // required numeric
        quote! {
            let #var_name: #ty = match std::env::var(#env_lit) {
                Ok(__v) => match __v.parse::<#ty>() {
                    Ok(__parsed) => __parsed,
                    Err(_) => {
                        __errors.push(format!("{}='{}' is not a valid {}", #env_lit, __v, stringify!(#ty)));
                        0 as #ty
                    }
                },
                Err(_) => {
                    __errors.push(format!("Missing required env: {}", #env_lit));
                    0 as #ty
                }
            };
        }
    }
}