use proc_macro2::TokenStream;

use syn::{
    Attribute, Expr, FieldValue, Ident, parse::Parse, punctuated::Punctuated, spanned::Spanned,
    token::Comma,
};

use crate::{
    args::{self, Arg},
    hook, key,
    util::FieldValueKey,
};

pub struct Args {
    pub inspect: bool,
}

impl Parse for Args {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let mut inspect = Arg::bool("inspect");

        args::set_from_field_values(
            input.parse_terminated(FieldValue::parse, Token![,])?.iter(),
            [&mut inspect],
        )?;

        Ok(Args {
            inspect: inspect.take_or_default(),
        })
    }
}

pub fn eval_key_value_with_hook(
    attrs: &[Attribute],
    fv: &FieldValue,
    fn_name: &TokenStream,
    interpolated: bool,
    captured: bool,
) -> syn::Result<TokenStream> {
    let key_expr = fv.key_expr()?;
    let value_expr = &fv.expr;

    let key_tokens = key::key_with_hook(&[], &key_expr, interpolated, captured);
    let value_tokens = value_with_hook(&value_expr, fn_name, interpolated, captured);

    hook::eval_hooks(
        &attrs,
        syn::parse_quote_spanned!(fv.span()=>
        {
            (#key_tokens, #value_tokens)
        }),
    )
}

pub fn raw_key_value(fv: &FieldValue) -> syn::Result<TokenStream> {
    let key = fv.key_expr()?;
    let value = &fv.expr;

    Ok(quote_spanned!(fv.span()=> (#key, #value)))
}

pub fn eval_key_with_hook(
    attrs: &[Attribute],
    fv: &FieldValue,
    interpolated: bool,
    captured: bool,
) -> syn::Result<TokenStream> {
    let key_expr = fv.key_expr()?;

    let key_tokens = key::key_with_hook(&[], &key_expr, interpolated, captured);

    hook::eval_hooks(&attrs, syn::parse_quote_spanned!(fv.span()=>#key_tokens))
}

pub(crate) fn value_with_hook(
    expr: &Expr,
    fn_name: &TokenStream,
    interpolated: bool,
    captured: bool,
) -> TokenStream {
    let interpolated_expr = if interpolated {
        quote!(.__private_interpolated())
    } else {
        quote!(.__private_uninterpolated())
    };

    let captured_expr = if captured {
        quote!(.__private_captured())
    } else {
        quote!(.__private_uncaptured())
    };

    quote_spanned!(expr.span()=> #[allow(unused_imports)] {
        use emit::__private::{__PrivateCaptureHook as _, __PrivateOptionalCaptureHook as _, __PrivateOptionalHook as _, __PrivateInterpolatedHook as _, __PrivateKeyExternalHook as _};
        (#expr).#fn_name().__private_key_external() #interpolated_expr #captured_expr
    })
}

pub fn default_fn_name(fv: &FieldValue) -> TokenStream {
    match fv.key_name().as_deref() {
        // Well-known properties
        Ok(emit_core::well_known::KEY_LVL) => {
            quote_spanned!(fv.span()=> __private_capture_as_level)
        }
        Ok(emit_core::well_known::KEY_ERR) => {
            quote_spanned!(fv.span()=> __private_capture_as_error)
        }
        Ok(emit_core::well_known::KEY_SPAN_ID) => {
            quote_spanned!(fv.span()=> __private_capture_as_span_id)
        }
        Ok(emit_core::well_known::KEY_SPAN_PARENT) => {
            quote_spanned!(fv.span()=> __private_capture_as_span_id)
        }
        Ok(emit_core::well_known::KEY_TRACE_ID) => {
            quote_spanned!(fv.span()=> __private_capture_as_trace_id)
        }
        // In other cases, capture using the default implementation
        _ => quote_spanned!(fv.span()=> __private_capture_as_default),
    }
}

pub struct RenameHookTokens<T> {
    pub name: &'static str,
    pub args: TokenStream,
    pub expr: TokenStream,
    pub to: T,
}

pub fn rename_hook_tokens(
    opts: RenameHookTokens<impl Fn(&Args) -> TokenStream>,
) -> Result<TokenStream, syn::Error> {
    hook::rename_hook_tokens(hook::RenameHookTokens {
        name: opts.name,
        target: "values in `emit` macros",
        args: opts.args,
        expr: opts.expr,
        predicate: |ident: &str| {
            ident.starts_with("__private_capture") || ident.starts_with("__private_captured")
        },
        to: move |hook_args: &Args, ident: &Ident, args: &Punctuated<Expr, Comma>| {
            if ident.to_string().starts_with("__private_captured") {
                return None;
            }

            let to_ident = (opts.to)(hook_args);

            Some((to_ident, quote!(#args)))
        },
    })
}
