//! `#[derive(InodeModel)]`
//!
//! Implements [`inode_db::oim::InodeModel`] for a struct that has been
//! annotated with `#[oim(table = "name")]` plus exactly one
//! `#[oim(primary_key)]` field and any number of `#[oim(index)]` fields.
//!
//! Errors (e.g. missing `#[oim(table)]`, multiple `primary_key`s) are
//! surfaced via `syn::Error::to_compile_error` so they appear at the
//! derive site.

use proc_macro::TokenStream;
use quote::quote;
use syn::{
    parse_macro_input, Attribute, Data, DeriveInput, Expr, ExprLit, Fields, Lit, Meta,
    MetaList, MetaNameValue,
};

#[proc_macro_derive(InodeModel, attributes(oim))]
pub fn derive_inode_model(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    match expand(input) {
        Ok(ts) => ts,
        Err(e) => e.to_compile_error().into(),
    }
}

fn expand(input: DeriveInput) -> syn::Result<TokenStream> {
    let name = &input.ident;
    let generics = &input.generics;
    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

    let struct_data = match &input.data {
        Data::Struct(d) => d,
        _ => {
            return Err(syn::Error::new(
                input.ident.span(),
                "InodeModel can only be derived for structs",
            ))
        }
    };
    let fields = match &struct_data.fields {
        Fields::Named(n) => &n.named,
        _ => {
            return Err(syn::Error::new(
                struct_data.struct_token.span,
                "InodeModel requires named fields",
            ))
        }
    };

    let table = parse_table_attr(&input.attrs)?
        .ok_or_else(|| syn::Error::new(input.ident.span(), "OIM model is missing #[oim(table = \"...\")]"))?;

    let mut primary_key: Option<&syn::Field> = None;
    let mut indexed_fields: Vec<&syn::Field> = Vec::new();
    for field in fields.iter() {
        for attr in &field.attrs {
            if !is_oim_attr(attr) {
                continue;
            }
            let kind = parse_field_kind(attr)?;
            match kind {
                FieldKind::PrimaryKey => {
                    if primary_key.is_some() {
                        return Err(syn::Error::new(
                            field.ident.as_ref().unwrap().span(),
                            "OIM model has multiple #[oim(primary_key)] fields",
                        ));
                    }
                    primary_key = Some(field);
                }
                FieldKind::Index => indexed_fields.push(field),
            }
        }
    }
    let primary_key = primary_key
        .ok_or_else(|| syn::Error::new(input.ident.span(), "OIM model requires a #[oim(primary_key)] field"))?;
    let key_ident = primary_key.ident.as_ref().unwrap();
    let key_ty = &primary_key.ty;

    let index_emits: Vec<proc_macro2::TokenStream> = indexed_fields
        .iter()
        .map(|f| {
            let fident = f.ident.as_ref().unwrap();
            let fname = fident.to_string();
            quote! {
                ( #fname ,
                  match bincode::serde::encode_to_vec(
                      &self.#fident,
                      bincode::config::standard(),
                  ) {
                      Ok(v) => v,
                      Err(e) => panic!("OIM encode error for secondary index: {e}"),
                  }
                ),
            }
        })
        .collect();

    // Static assertion that `Key` satisfies the trait's bounds. Surfaced
    // as a normal E0277 if not.
    let key_assertion = quote! {
        #[allow(dead_code)]
        const _: fn() = || {
            fn _assert<
                T: ::std::clone::Clone
                    + ::std::fmt::Debug
                    + ::std::cmp::PartialEq
                    + ::std::cmp::Eq
                    + ::std::hash::Hash
                    + ::std::cmp::Ord
                    + serde::Serialize
                    + serde::de::DeserializeOwned
                    + Send
                    + Sync
            >() {}
            _assert::<#key_ty>();
        };
    };

    let expanded: proc_macro2::TokenStream = quote! {
        #[automatically_derived]
        impl #impl_generics inode_db::oim::InodeModel for #name #ty_generics #where_clause {
            const TABLE_NAME: &'static str = #table;
            type Key = #key_ty;

            fn primary_key(&self) -> &Self::Key {
                &self.#key_ident
            }

            fn encode(&self) -> inode_db::Result<::std::vec::Vec<u8>> {
                ::std::result::Result::Ok(
                    bincode::serde::encode_to_vec(self, bincode::config::standard())?
                )
            }

            fn decode(bytes: &[u8]) -> inode_db::Result<Self> {
                let (v, _): (Self, usize) = bincode::serde::decode_from_slice(
                    bytes,
                    bincode::config::standard(),
                )?;
                Ok(v)
            }

            fn secondary_indexes(&self) -> ::std::vec::Vec<(&'static str, ::std::vec::Vec<u8>)> {
                ::std::vec![ #(#index_emits)* ]
            }
        }

        #key_assertion
    };

    Ok(proc_macro::TokenStream::from(expanded))
}

fn is_oim_attr(attr: &Attribute) -> bool {
    attr.path().is_ident("oim")
}

fn parse_table_attr(attrs: &[Attribute]) -> syn::Result<Option<String>> {
    let mut table: Option<String> = None;
    for attr in attrs.iter().filter(|a| is_oim_attr(a)) {
        match &attr.meta {
            Meta::List(MetaList { tokens, .. }) => {
                let nv: MetaNameValue = syn::parse2(tokens.clone())?;
                if !nv.path.is_ident("table") {
                    return Err(syn::Error::new_spanned(&nv.path, "unknown #[oim(...)] key"));
                }
                let Expr::Lit(ExprLit { lit: Lit::Str(s), .. }) = &nv.value else {
                    return Err(syn::Error::new_spanned(
                        &nv.value,
                        "#[oim(table)] requires a string literal, e.g. #[oim(table = \"users\")]",
                    ));
                };
                table = Some(s.value());
            }
            _ => {
                return Err(syn::Error::new_spanned(
                    attr,
                    "expected `#[oim(table = \"...\")]`",
                ));
            }
        }
    }
    Ok(table)
}

fn parse_field_kind(attr: &Attribute) -> syn::Result<FieldKind> {
    match &attr.meta {
        Meta::List(MetaList { tokens, .. }) => {
            let ident: syn::Ident = syn::parse2(tokens.clone())?;
            if ident == "primary_key" {
                Ok(FieldKind::PrimaryKey)
            } else if ident == "index" {
                Ok(FieldKind::Index)
            } else {
                Err(syn::Error::new_spanned(
                    &ident,
                    "unknown #[oim(...)] field attribute; expected primary_key or index",
                ))
            }
        }
        _ => Err(syn::Error::new_spanned(
            attr,
            "expected `#[oim(primary_key)]` or `#[oim(index)]`",
        )),
    }
}

enum FieldKind {
    PrimaryKey,
    Index,
}
