//! `bind_loops!` — declarative loop/cron/batch handler binding.
//!
//! Symmetric to `bind_http!`: one module-level declaration wires up all
//! background loops; `main` only sees the generated management struct.
//!
//! # Syntax
//!
//! ```text
//! bind_loops! {
//!     StructName, StateType {
//!         "name" [mode_attrs] => HandlerType : TxType,
//!         "name" [mode_attrs] => HandlerType : TxType @ DbBinding,
//!     }
//! }
//! ```
//!
//! ## Mode attributes (`[...]`)
//!
//! | Attribute      | Effect                                            |
//! |----------------|---------------------------------------------------|
//! | `batch`        | Use `run_batch_loop` (handler implements `BatchHandler`) |
//! | `cron Ns`      | Batch: sets `round_interval`. Pure cron: uses `run_cron_loop` |
//! | `refresh Ns`   | Batch only: sets `refresh_interval` (default 30s) |
//! | *(empty `[]`)* | Plain `LoopHandler` via `run_handler_loop`        |
//!
//! ## Generated code
//!
//! For each batch entry, the macro generates:
//! - A global `OnceLock<BatchLoopHandle>` singleton
//! - A public `{PascalName}Loop` struct with `notify_refresh()` static method
//!
//! For the whole declaration, it generates `StructName` with:
//! - `start(state) -> Self` — spawns all loops, returns handle bundle
//! - `shutdown_all(&self)` — cancels every loop
//! - `join_all(self)` — awaits every loop to finish
//!
//! ## Example (from engine)
//!
//! ```rust,ignore
//! mad_hatter::bind_loops! {
//!     EngineLoops, Arc<EngineState> {
//!         "beat"     [batch, cron 3s, refresh 30s] => BeatHandler : BeatCycle,
//!         "capture"  [batch, cron 60s, refresh 30s] => CaptureHandler : CaptureCycle,
//!         "compress" [batch, cron 300s, refresh 30s] => CompressHandler : RollCycle,
//!         "slave_reregister" [cron 30s] => SlaveReregisterCron : SlaveReregister,
//!     }
//! }
//!
//! // In main:
//! let loops = EngineLoops::start(state);
//! // ... run server ...
//! loops.shutdown_all();
//! loops.join_all().await;
//!
//! // From anywhere — trigger immediate re-fetch:
//! BeatLoop::notify_refresh();
//! ```

use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{
    parse::{Parse, ParseStream},
    Ident, LitStr, Token, Type, Result,
};

// ---------------------------------------------------------------------------
// AST types
// ---------------------------------------------------------------------------

/// Top-level definition: `bind_loops! { Name, StateType { entries... } }`
pub struct LoopsDef {
    pub struct_name: Ident,
    pub state_type: Type,
    pub entries: Vec<LoopEntry>,
}

/// A single loop entry in the declaration.
pub struct LoopEntry {
    pub name: LitStr,
    pub mode: LoopMode,
    pub handler_type: Type,
    pub fatal_type: Type,
    pub db_binding: Option<syn::Path>,
}

/// Loop execution mode derived from `[batch]`, `[batch, cron 60s]`, etc.
pub struct LoopMode {
    pub is_batch: bool,
    pub refresh_secs: Option<u64>,
    pub cron_secs: Option<u64>,
}

// ---------------------------------------------------------------------------
// Parsing
// ---------------------------------------------------------------------------

impl Parse for LoopsDef {
    fn parse(input: ParseStream) -> Result<Self> {
        let struct_name: Ident = input.parse()?;
        input.parse::<Token![,]>()?;
        let state_type: Type = input.parse()?;

        let content;
        syn::braced!(content in input);

        let mut entries = Vec::new();
        while !content.is_empty() {
            entries.push(content.parse::<LoopEntry>()?);
            // Optional trailing comma
            if content.peek(Token![,]) {
                content.parse::<Token![,]>()?;
            }
        }

        Ok(LoopsDef {
            struct_name,
            state_type,
            entries,
        })
    }
}

impl Parse for LoopEntry {
    fn parse(input: ParseStream) -> Result<Self> {
        let name: LitStr = input.parse()?;

        // Optional mode attributes: [batch], [batch, cron 60s], [batch, refresh 30s], etc.
        let mode = if input.peek(syn::token::Bracket) {
            let attr_content;
            syn::bracketed!(attr_content in input);
            parse_loop_mode(&attr_content)?
        } else {
            LoopMode {
                is_batch: false,
                refresh_secs: None,
                cron_secs: None,
            }
        };

        input.parse::<Token![=>]>()?;
        let handler_type: Type = input.parse()?;
        input.parse::<Token![:]>()?;
        let fatal_type: Type = input.parse()?;

        // Optional `@ DB_BINDING` (database binding)
        let db_binding = if input.peek(Token![@]) {
            input.parse::<Token![@]>()?;
            Some(input.parse::<syn::Path>()?)
        } else {
            None
        };

        Ok(LoopEntry {
            name,
            mode,
            handler_type,
            fatal_type,
            db_binding,
        })
    }
}

fn parse_loop_mode(input: ParseStream) -> Result<LoopMode> {
    let mut is_batch = false;
    let mut refresh_secs = None;
    let mut cron_secs = None;

    loop {
        if input.is_empty() {
            break;
        }
        let ident: Ident = input.parse()?;
        match ident.to_string().as_str() {
            "batch" => {
                is_batch = true;
            }
            "refresh" => {
                let secs = parse_duration_secs(input)?;
                refresh_secs = Some(secs);
            }
            "cron" => {
                let secs = parse_duration_secs(input)?;
                cron_secs = Some(secs);
            }
            other => {
                return Err(syn::Error::new(
                    ident.span(),
                    format!("unknown loop mode attribute: `{}`", other),
                ));
            }
        }
        if input.peek(Token![,]) {
            input.parse::<Token![,]>()?;
        }
    }

    Ok(LoopMode {
        is_batch,
        refresh_secs,
        cron_secs,
    })
}

/// Parse a duration like `30s` or `300s`.
fn parse_duration_secs(input: ParseStream) -> Result<u64> {
    let lit: syn::LitInt = input.parse()?;
    let suffix = lit.suffix();
    if suffix != "s" {
        return Err(syn::Error::new(
            lit.span(),
            "duration must have `s` suffix (e.g., `30s`)",
        ));
    }
    lit.base10_parse::<u64>()
}

// ---------------------------------------------------------------------------
// Code generation
// ---------------------------------------------------------------------------

pub fn generate(def: LoopsDef) -> TokenStream {
    let struct_name = &def.struct_name;
    let state_type = &def.state_type;

    let mut singleton_defs = Vec::new();
    let mut singleton_impls = Vec::new();
    let mut start_body = Vec::new();
    let mut handle_fields = Vec::new();
    let mut handle_field_names = Vec::new();
    let mut shutdown_body = Vec::new();
    let mut join_body = Vec::new();

    for entry in &def.entries {
        let name_str = entry.name.value();
        let handler_ty = &entry.handler_type;
        let _fatal_ty = &entry.fatal_type;
        let field_name = format_ident!("{}", name_str.replace('-', "_"));

        if entry.mode.is_batch {
            let singleton_name = format_ident!(
                "__{}_BATCH_HANDLE",
                name_str.replace('-', "_").to_uppercase()
            );
            let refresh = entry.mode.refresh_secs.unwrap_or(30);
            let round_interval = if let Some(cron) = entry.mode.cron_secs {
                quote! { Some(::std::time::Duration::from_secs(#cron)) }
            } else {
                quote! { None }
            };

            // Global singleton for BatchLoopHandle — notify API entry point
            singleton_defs.push(quote! {
                static #singleton_name: ::std::sync::OnceLock<mad_hatter::BatchLoopHandle>
                    = ::std::sync::OnceLock::new();
            });

            // Per-loop public API struct with notify_refresh()
            let api_struct = format_ident!(
                "{}Loop",
                to_pascal_case(&name_str)
            );
            singleton_impls.push(quote! {
                /// Auto-generated loop API entry point.
                pub struct #api_struct;

                impl #api_struct {
                    /// Trigger an immediate items refresh for this loop.
                    pub fn notify_refresh() {
                        if let Some(h) = #singleton_name.get() {
                            h.notify_refresh();
                        }
                    }
                }
            });

            // start() body — batch loop
            let db_expr = if let Some(ref db_path) = entry.db_binding {
                quote! { Some(#db_path.get().expect("DB not initialized").db().clone()) }
            } else {
                quote! { None }
            };
            start_body.push(quote! {
                let __handler = <#handler_ty>::new(state.clone());
                let __config = mad_hatter::BatchLoopConfig {
                    refresh_interval: ::std::time::Duration::from_secs(#refresh),
                    round_interval: #round_interval,
                };
                let (__loop_handle, __batch_handle) = mad_hatter::run_batch_loop(
                    #name_str.to_string(),
                    __handler,
                    __config,
                    #db_expr,
                );
                #singleton_name.set(__batch_handle).ok();
                let #field_name = __loop_handle;
            });
        } else if entry.mode.cron_secs.is_some() {
            // CronHandler — use run_cron_loop directly
            let db_expr = if let Some(ref db_path) = entry.db_binding {
                quote! { Some(#db_path.get().expect("DB not initialized").db().clone()) }
            } else {
                quote! { None }
            };
            start_body.push(quote! {
                let __handler = <#handler_ty>::new(state.clone());
                let #field_name = mad_hatter::run_cron_loop(
                    #name_str.to_string(),
                    __handler,
                    #db_expr,
                );
            });
        } else {
            // Plain LoopHandler — use run_handler_loop directly
            let db_expr = if let Some(ref db_path) = entry.db_binding {
                quote! { Some(#db_path.get().expect("DB not initialized").db().clone()) }
            } else {
                quote! { None }
            };
            start_body.push(quote! {
                let __handler = <#handler_ty>::new(state.clone());
                let #field_name = mad_hatter::run_handler_loop(
                    #name_str.to_string(),
                    __handler,
                    #db_expr,
                );
            });
        }

        handle_fields.push(quote! {
            #field_name: mad_hatter::LoopHandle
        });
        handle_field_names.push(field_name.clone());

        shutdown_body.push(quote! {
            self.#field_name.cancel();
        });

        join_body.push(quote! {
            self.#field_name.join().await;
        });
    }

    quote! {
        #(#singleton_defs)*

        #(#singleton_impls)*

        pub struct #struct_name {
            #(#handle_fields),*
        }

        impl #struct_name {
            /// Start all declared loops. Call once from main.
            pub fn start(state: #state_type) -> Self {
                #(#start_body)*

                Self {
                    #(#handle_field_names),*
                }
            }

            /// Trigger graceful shutdown of all loops.
            pub fn shutdown_all(&self) {
                #(#shutdown_body)*
            }

            /// Wait for all loops to finish.
            pub async fn join_all(self) {
                #(#join_body)*
            }
        }
    }
}

/// Convert "hot-scan" to "HotScan" for struct names.
fn to_pascal_case(s: &str) -> String {
    s.split('-')
        .map(|part| {
            let mut chars = part.chars();
            match chars.next() {
                Some(c) => c.to_uppercase().to_string() + chars.as_str(),
                None => String::new(),
            }
        })
        .collect()
}