use proc_macro2::TokenStream;
use quote::{format_ident, quote};

use crate::parse::{AuthLevel, BindDef, BindingDef, HttpMethod, ServerDef};

fn method_routing_fn(method: &HttpMethod) -> TokenStream {
    match method {
        HttpMethod::Get => quote! { ::mad_hatter::__axum::routing::get },
        HttpMethod::Post => quote! { ::mad_hatter::__axum::routing::post },
        HttpMethod::Put => quote! { ::mad_hatter::__axum::routing::put },
        HttpMethod::Delete => quote! { ::mad_hatter::__axum::routing::delete },
        HttpMethod::Patch => quote! { ::mad_hatter::__axum::routing::patch },
        HttpMethod::Any => quote! { ::mad_hatter::__axum::routing::any },
    }
}

/// Method name identifier for chaining on MethodRouter.
/// e.g., `.post(handler)`

// ===========================================================================
// bind_http! code generation (new syntax: struct + router + serve)
// ===========================================================================

pub fn generate_bind(def: BindDef) -> TokenStream {
    let struct_name = &def.name;
    let prefix = &def.prefix;
    let state_type = &def.state_type;

    // Collect all bindings as references
    let all_refs: Vec<&BindingDef> = def.bindings.iter().collect();

    // Split bindings by auth level: auth(none) → public, everything else → protected
    let protected_refs: Vec<&BindingDef> = def
        .bindings
        .iter()
        .filter(|b| !matches!(b.auth, Some(AuthLevel::None)))
        .collect();
    let public_refs: Vec<&BindingDef> = def
        .bindings
        .iter()
        .filter(|b| matches!(b.auth, Some(AuthLevel::None)))
        .collect();

    // Collect auth-none full paths for AUTH_NONE_PATHS constant
    let auth_none_paths: Vec<String> = public_refs
        .iter()
        .map(|b| if b.absolute { b.path.clone() } else { format!("{}{}", prefix, &b.path) })
        .collect();

    // Generate route calls for each router
    let all_route_calls = generate_bind_routes(&all_refs, prefix);
    let protected_route_calls = generate_bind_routes(&protected_refs, prefix);
    let public_route_calls = generate_bind_routes(&public_refs, prefix);

    quote! {
        /// HTTP binding (auto-generated by bind_http!)
        pub struct #struct_name;

        impl #struct_name {
            /// Paths that require no authentication (derived from `#[auth(none)]` annotations).
            pub const AUTH_NONE_PATHS: &[&str] = &[#(#auth_none_paths),*];

            /// Build an axum Router with all declared routes (both protected and public).
            pub fn router() -> ::mad_hatter::__axum::Router<#state_type> {
                ::mad_hatter::__axum::Router::new()
                    #(#all_route_calls)*
            }

            /// Build an axum Router with only protected routes (requires authentication).
            ///
            /// Includes routes annotated with `#[auth(session)]` and routes with no auth annotation
            /// (default is session).
            pub fn protected_router() -> ::mad_hatter::__axum::Router<#state_type> {
                ::mad_hatter::__axum::Router::new()
                    #(#protected_route_calls)*
            }

            /// Build an axum Router with only public routes (no authentication required).
            ///
            /// Includes routes annotated with `#[auth(none)]`.
            pub fn public_router() -> ::mad_hatter::__axum::Router<#state_type> {
                ::mad_hatter::__axum::Router::new()
                    #(#public_route_calls)*
            }

            /// Start serving HTTP on the given address.
            ///
            /// Binds a TCP listener and runs the axum server with the provided state.
            pub async fn serve(
                addr: &str,
                state: #state_type,
            ) -> ::std::result::Result<(), ::std::io::Error> {
                let app = Self::router().with_state(state);
                let listener = ::mad_hatter::__tokio::net::TcpListener::bind(addr).await?;
                ::mad_hatter::__axum::serve(listener, app).await
            }
        }
    }
}


// ===========================================================================
// bind_server! code generation
// ===========================================================================

pub fn generate_server(def: ServerDef) -> TokenStream {
    let struct_name = &def.name;
    let state_type = &def.state_type;

    // Generate optional config constants
    let cookie_prefix_tokens = if let Some(ref cp) = def.session_cookie_prefix {
        quote! {
            /// Session cookie name prefix (auto-generated from declaration).
            pub const SESSION_COOKIE_PREFIX: &'static str = #cp;

            /// Build session cookie name for a given port.
            pub fn session_cookie_name(port: u16) -> String {
                let mut name = String::from(#cp);
                name.push_str(&port.to_string());
                name
            }
        }
    } else {
        quote! {}
    };

    let public_dir_tokens = if let Some(ref pd) = def.public_dir {
        quote! {
            /// Public directory prefix (auto-generated from declaration).
            pub const PUBLIC_DIR: &'static str = #pd;
        }
    } else {
        quote! {}
    };

    let auth_path_tokens: Vec<proc_macro2::TokenStream> = def.auth_paths.iter().map(|(key, value)| {
        let const_name = format_ident!("{}_PATH", key.to_uppercase());
        quote! {
            pub const #const_name: &'static str = #value;
        }
    }).collect();

    let body_limit_tokens = if let Some(ref expr) = def.body_limit {
        quote! {
            /// Body size limit in bytes (auto-generated from declaration).
            pub const BODY_LIMIT: usize = (#expr) as usize;
        }
    } else {
        quote! {}
    };

    // Generate embedded pages fallback handler
    let embedded_pages_tokens = if !def.embedded_pages.is_empty() {
        let match_arms: Vec<proc_macro2::TokenStream> = def.embedded_pages.iter().map(|(url_path, file_path)| {
            let url = url_path.trim_start_matches('/');
            let filename = if url.is_empty() {
                "index.html".to_string()
            } else if url.ends_with(".html") {
                url.to_string()
            } else {
                format!("{}.html", url)
            };
            quote! {
                #filename => ::mad_hatter::http::embedded_html_response(include_str!(#file_path)),
            }
        }).collect();

        quote! {
            /// Fallback handler for embedded HTML pages (auto-generated from `embedded_pages` declaration).
            pub async fn embedded_fallback(
                uri: ::mad_hatter::__axum::http::Uri,
            ) -> ::mad_hatter::__axum::response::Response {
                let path = uri.path().trim_start_matches('/');
                let filename = if path.is_empty() || path == "index" || path == "index.html" {
                    "index.html".to_string()
                } else if path.ends_with(".html") {
                    path.to_string()
                } else {
                    format!("{}.html", path)
                };
                match filename.as_str() {
                    #(#match_arms)*
                    _ => ::mad_hatter::not_found("Page not found"),
                }
            }
        }
    } else {
        quote! {}
    };

    // Generate auth middleware (when auth_policy is declared)
    // Reverse design: no path checking. Auth middleware only runs on protected routes.
    // Flow: skip_auth → pre_auth_check → cookie validation → unauthorized
    let auth_tokens = if def.auth_policy.is_some() {
        quote! {
            /// Authentication middleware (auto-generated by bind_server!).
            ///
            /// Only apply this to protected routes (via `.layer()`).
            /// Public routes should NOT go through this middleware.
            ///
            /// Flow: skip_auth → pre_auth_check → cookie validation → unauthorized
            pub async fn auth_middleware(
                ::mad_hatter::__axum::extract::State(state): ::mad_hatter::__axum::extract::State<#state_type>,
                mut req: ::mad_hatter::__axum::extract::Request,
                next: ::mad_hatter::__axum::middleware::Next,
            ) -> ::mad_hatter::__axum::response::Response {
                use ::mad_hatter::AuthPolicy as _;
                use ::mad_hatter::__axum::response::IntoResponse as _;

                let hook = ::mad_hatter::auth::PreAuthHook::new(move |auth_fs, req: &::mad_hatter::__axum::extract::Request| {
                    if state.skip_auth() {
                        return None;
                    }

                    let path = req.uri().path().to_string();

                    match state.pre_auth_check(&path, req, auth_fs) {
                        ::mad_hatter::PreAuthResult::Allow => {
                            return None;
                        }
                        ::mad_hatter::PreAuthResult::Redirect(url) => {
                            return Some(::mad_hatter::__axum::response::Redirect::to(&url).into_response());
                        }
                        ::mad_hatter::PreAuthResult::Continue => {}
                    }

                    if let Some(token) = ::mad_hatter::http::extract_cookie_value(
                        req.headers(),
                        state.session_cookie_name(),
                    ) {
                        if state.validate_session(&token) {
                            return None;
                        }
                    }

                    Some(state.unauthorized_response())
                });
                req.extensions_mut().insert(hook);
                next.run(req).await
            }
        }
    } else {
        quote! {}
    };

    quote! {
        /// HTTP server configuration (auto-generated by bind_server!)
        pub struct #struct_name;

        impl #struct_name {
            #cookie_prefix_tokens
            #public_dir_tokens
            #(#auth_path_tokens)*
            #body_limit_tokens
            #embedded_pages_tokens
            #auth_tokens

            /// Start the HTTP server as a managed loop task.
            ///
            /// `router` should be the fully-assembled Router (all route groups merged).
            /// `addr` is the socket address to bind to.
            ///
            /// Returns a `LoopHandle` registered in the global loop registry,
            /// so `shutdown_all_loops()` will trigger graceful shutdown.
            pub fn serve(router: mad_hatter::__axum::Router<()>, addr: std::net::SocketAddr) -> mad_hatter::LoopHandle {
                mad_hatter::__spawn_http_server("http", router, addr)
            }
        }
    }
}

fn generate_bind_routes(bindings: &[&BindingDef], prefix: &str) -> Vec<TokenStream> {
    // Group bindings by full path, preserving insertion order
    let mut groups: Vec<(String, Vec<&BindingDef>)> = Vec::new();
    let mut path_index: std::collections::HashMap<String, usize> =
        std::collections::HashMap::new();

    for b in bindings {
        let full_path = if b.absolute { b.path.clone() } else { format!("{}{}", prefix, &b.path) };
        if let Some(&idx) = path_index.get(&full_path) {
            groups[idx].1.push(b);
        } else {
            path_index.insert(full_path.clone(), groups.len());
            groups.push((full_path, vec![b]));
        }
    }

    groups
        .iter()
        .map(|(full_path, bs)| {
            // Build per-method routers with tx_guard layers (all handlers have tx), then merge
            let method_routers: Vec<TokenStream> = bs.iter().map(|b| {
                let handler = &b.handler;
                let routing_fn = method_routing_fn(&b.method);
                let tx_type = &b.tx_type;
                if let Some(ref db_path) = b.db_binding {
                    quote! {
                        #routing_fn(#handler)
                            .layer(::mad_hatter::__axum::middleware::from_fn({
                                let __db = #db_path.get().expect("DB not initialized").db().clone();
                                move |req, next| {
                                    let db = __db.clone();
                                    ::mad_hatter::tx_guard_with_db::<#tx_type>(req, next, db)
                                }
                            }))
                    }
                } else {
                    quote! {
                        #routing_fn(#handler)
                            .layer(::mad_hatter::__axum::middleware::from_fn(::mad_hatter::tx_guard::<#tx_type>))
                    }
                }
            }).collect();

            let first = &method_routers[0];
            let rest = &method_routers[1..];
            let mut merged = quote! { #first };
            for r in rest {
                merged = quote! { #merged.merge(#r) };
            }

            quote! {
                .route(#full_path, #merged)
            }
        })
        .collect()
}