//! # RUSTELO Server Binary
//!
//! <div align="center">
//!   <img src="../logos/rustelo_dev-logo-h.svg" alt="RUSTELO" width="300" />
//! </div>
//!
//! Main server executable for the RUSTELO web application framework.
//!
//! ## Overview
//!
//! This is the main entry point for the RUSTELO server application. It
//! initializes all services, sets up routing, configures middleware, and starts
//! the web server with support for:
//!
//! - **Authentication & Authorization** - JWT tokens, OAuth2, 2FA, RBAC
//! - **Content Management** - Markdown processing, media handling, database
//!   storage
//! - **Email Services** - Multi-provider email with templating
//! - **Security** - CSRF protection, rate limiting, HTTPS/TLS
//! - **Performance** - Metrics collection, connection pooling, caching
//!
//! ## Features
//!
//! ### Core Features (Always Available)
//! - **HTTP Server** - Fast Axum-based web server
//! - **Static Files** - Efficient static file serving
//! - **Health Checks** - Server health monitoring
//! - **CSRF Protection** - Cross-site request forgery protection
//! - **Rate Limiting** - Request rate limiting and throttling
//! - **Security Headers** - Comprehensive security headers
//!
//! ### Optional Features (Feature-Gated)
//! - **`auth`** - Complete authentication system with JWT, OAuth2, 2FA
//! - **`content-db`** - Database-backed content management
//! - **`email`** - Email sending with multiple providers
//! - **`tls`** - HTTPS/TLS encryption support
//! - **`metrics`** - Performance monitoring and metrics collection
//!
//! ## Usage
//!
//! ```bash
//! # Start with default configuration
//! cargo run
//!
//! # Start with specific features
//! cargo run --features "auth,content-db,email,tls"
//!
//! # Start with custom configuration
//! RUSTELO_CONFIG=custom.toml cargo run
//! ```
//!
//! ## Configuration
//!
//! The server can be configured through:
//! - Environment variables (prefixed with `RUSTELO_`)
//! - TOML configuration files
//! - Command-line arguments
//! - Default values with sensible fallbacks
//!
//! ### Example Configuration
//!
//! ```toml
//! [server]
//! host = "127.0.0.1"
//! port = 3030
//! protocol = "https"
//!
//! [database]
//! url = "postgresql://user:pass@localhost/rustelo"
//! max_connections = 10
//!
//! [auth]
//! jwt_secret = "your-secret-key"
//! jwt_expiration_hours = 24
//! enable_2fa = true
//!
//! [email]
//! provider = "smtp"
//! from_address = "noreply@rustelo.dev"
//! ```
//!
//! ## Architecture
//!
//! The server follows a modular architecture with clear separation of concerns:
//!
//! ### Application State
//! - **Leptos Integration** - SSR options and hydration
//! - **Security Services** - CSRF protection and rate limiting
//! - **Authentication** - JWT service and user management
//! - **Content Services** - Content processing and storage
//! - **Email Services** - Email templating and delivery
//! - **Metrics** - Performance monitoring and collection
//!
//! ### Request Handling
//! - **Routing** - API endpoints and static file serving
//! - **Middleware** - Authentication, CORS, security headers
//! - **Error Handling** - Comprehensive error responses
//! - **Logging** - Structured logging with tracing
//!
//! ## Security
//!
//! The server implements multiple security layers:
//!
//! - **Memory Safety** - Rust's ownership system prevents vulnerabilities
//! - **Input Validation** - Comprehensive validation of all inputs
//! - **CSRF Protection** - Token-based CSRF protection
//! - **Rate Limiting** - Request throttling and abuse prevention
//! - **Security Headers** - HSTS, CSP, X-Frame-Options, etc.
//! - **TLS/HTTPS** - End-to-end encryption (when enabled)
//!
//! ## Performance
//!
//! Optimized for high performance:
//!
//! - **Async/Await** - Non-blocking I/O with Tokio
//! - **Connection Pooling** - Efficient database connections
//! - **Static File Caching** - Optimized static asset serving
//! - **Request Deduplication** - Efficient request handling
//! - **Metrics Collection** - Performance monitoring and optimization
//!
//! ## Monitoring
//!
//! Built-in monitoring capabilities:
//!
//! - **Health Endpoints** - `/health` for service monitoring
//! - **Metrics Collection** - Prometheus-compatible metrics
//! - **Structured Logging** - JSON-formatted logs for analysis
//! - **Error Tracking** - Comprehensive error reporting
//!
//! ## License
//!
//! This project is licensed under the MIT License - see the [LICENSE](https://github.com/yourusername/rustelo/blob/main/LICENSE) file for details.

// #![allow(unused_variables)]
// #![recursion_limit = "512"]

use std::sync::{Arc, OnceLock};

use rustelo_core_types::rendering::{
    workspace_rendering_config, workspace_rendering_profile, RenderingConfig, RenderingProfile,
};

#[cfg(feature = "leptos-hydration")]
use crate::shell::shell_with_path;
use crate::templating::TemplateEngine;

/// Minimal site configuration passed to shell renderers.
///
/// Replaces `leptos_config::LeptosOptions` in all public renderer APIs so that
/// `leptos_config` is not required for htmx-only builds. Carries only what the
/// shells and static-file serving need: the listen address and the site root.
#[derive(Clone, Debug)]
pub struct SiteOptions {
    /// Address the server listens on.
    pub site_addr: std::net::SocketAddr,
    /// Filesystem root for serving the site tree (e.g. `"site"` or
    /// `"/var/www/site"`).
    pub site_root: String,
}

/// v1 shell renderer callback: receives site options and path.
pub type ShellRendererV1 = fn(SiteOptions, Option<String>) -> String;

/// v2 shell renderer callback: receives site options, path, the resolved
/// rendering profile, and the list of htmx extensions to enable in the shell.
///
/// Per-request headers are not available here — use [`current_request_headers`]
/// inside the renderer body to access cookies/HX-* headers.
pub type ShellRendererV2 = fn(SiteOptions, Option<String>, RenderingProfile, Vec<String>) -> String;

tokio::task_local! {
    /// Per-request `HeaderMap` set by the dispatcher before invoking the
    /// shell. Shells read it with [`current_request_headers`] to make
    /// rendering decisions based on cookies/HX-* headers (e.g. omit the
    /// cookie banner when consent is already present).
    static REQUEST_HEADERS: axum::http::HeaderMap;
}

/// Read the current request's headers from the shell.
///
/// Returns `None` when called outside a dispatcher-scoped task (e.g. in
/// tests that bypass `render_route_response`). Shells should treat `None` as
/// "no information"; never error out.
pub fn current_request_headers() -> Option<axum::http::HeaderMap> {
    REQUEST_HEADERS.try_with(|h| h.clone()).ok()
}

/// Fragment renderer callback: returns the page body only, without the shell.
pub type FragmentRenderer =
    fn(SiteOptions, &rustelo_core_lib::routing::engine::RouteResolution) -> String;

static SHELL_RENDERER: OnceLock<ShellRendererV1> = OnceLock::new();

/// Profile-aware shell renderer override. Takes precedence over the v1
/// callback.
static SHELL_RENDERER_V2: OnceLock<ShellRendererV2> = OnceLock::new();

/// HTMX fragment renderer override — returns only the body fragment for a
/// route, without the page shell or hydration scripts. Required for htmx-ssr
/// requests.
///
/// Implementations register the function that knows how to invoke their root
/// page component for a given resolution and serialize the output to a String.
/// When unset, the dispatcher falls back to the full shell with a warning.
static FRAGMENT_RENDERER: OnceLock<FragmentRenderer> = OnceLock::new();

/// Shared Minijinja engine used to render `kind = "template"` routes.
/// Registered by the implementation at startup via
/// [`register_template_engine`].
static TEMPLATE_ENGINE: OnceLock<Arc<TemplateEngine>> = OnceLock::new();

/// Install the Minijinja [`TemplateEngine`] shared by all template routes.
///
/// Must be called before the server starts handling requests. Subsequent calls
/// are silently ignored.
pub fn register_template_engine(engine: Arc<TemplateEngine>) {
    let _ = TEMPLATE_ENGINE.set(engine);
}

/// Read the `[rendering]` table from a NCL or TOML config file and apply it to
/// the workspace rendering registry.
///
/// Accepts both `.ncl` (parsed via the bundled nickel CLI) and `.toml` inputs.
/// Returns the applied [`RenderingConfig`] on success, or `None` when the file
/// contains no `[rendering]` table (the registry then keeps its default).
///
/// Failure modes are logged via `tracing::warn!` and surfaced through the
/// returned `Result` so callers can decide whether to abort startup. Most sites
/// pass the error through to `eprintln!` and continue with the default profile.
pub fn install_workspace_rendering_from_file(
    path: &std::path::Path,
) -> Result<Option<RenderingConfig>, RenderingLoadError> {
    if !path.exists() {
        return Err(RenderingLoadError::FileNotFound(path.to_path_buf()));
    }
    let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("");
    let json = match extension {
        "ncl" => nickel_export_to_json(path)?,
        "toml" => {
            let raw = std::fs::read_to_string(path).map_err(RenderingLoadError::Io)?;
            // Round-trip through a toml::Value → serde_json::Value to reuse the
            // same deserialiser path the NCL branch uses.
            let value: toml::Value = toml::from_str(&raw).map_err(|e| {
                RenderingLoadError::Parse(format!("invalid TOML in {}: {e}", path.display()))
            })?;
            serde_json::to_string(&value)
                .map_err(|e| RenderingLoadError::Parse(format!("toml→json failed: {e}")))?
        }
        other => {
            return Err(RenderingLoadError::Parse(format!(
                "unsupported extension `{other}` (expected .ncl or .toml)"
            )))
        }
    };
    apply_rendering_json(&json)
}

#[derive(Debug)]
pub enum RenderingLoadError {
    FileNotFound(std::path::PathBuf),
    Io(std::io::Error),
    NickelCli(String),
    Parse(String),
}

impl std::fmt::Display for RenderingLoadError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::FileNotFound(p) => write!(f, "rendering config not found: {}", p.display()),
            Self::Io(e) => write!(f, "rendering config io error: {e}"),
            Self::NickelCli(msg) => write!(f, "nickel export failed: {msg}"),
            Self::Parse(msg) => write!(f, "rendering config parse error: {msg}"),
        }
    }
}

impl std::error::Error for RenderingLoadError {}

fn nickel_export_to_json(path: &std::path::Path) -> Result<String, RenderingLoadError> {
    let output = std::process::Command::new("nickel")
        .arg("export")
        .arg("--format")
        .arg("json")
        .arg(path)
        .output()
        .map_err(|e| RenderingLoadError::NickelCli(format!("could not run `nickel`: {e}")))?;
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
        return Err(RenderingLoadError::NickelCli(stderr));
    }
    String::from_utf8(output.stdout)
        .map_err(|e| RenderingLoadError::Parse(format!("nickel stdout not UTF-8: {e}")))
}

fn apply_rendering_json(json: &str) -> Result<Option<RenderingConfig>, RenderingLoadError> {
    #[derive(serde::Deserialize)]
    struct Wrapper {
        rendering: Option<RenderingConfig>,
    }
    let wrapper: Wrapper = serde_json::from_str(json).map_err(|e| {
        RenderingLoadError::Parse(format!(
            "expected JSON with optional `rendering` field: {e}"
        ))
    })?;
    if let Some(cfg) = wrapper.rendering.clone() {
        rustelo_core_types::rendering::set_workspace_rendering_config(cfg);
    }
    Ok(wrapper.rendering)
}

/// Access the registered Minijinja engine, when present.
pub fn template_engine() -> Option<Arc<TemplateEngine>> {
    TEMPLATE_ENGINE.get().cloned()
}

/// Register a custom shell renderer so implementations can control the HTML
/// shell without modifying the framework.
///
/// Must be called before `run_server()`. Subsequent calls are silently ignored.
pub fn register_shell_renderer(renderer: ShellRendererV1) {
    let _ = SHELL_RENDERER.set(renderer);
}

/// Register a profile-aware shell renderer. When set, takes precedence over the
/// v1 callback so the renderer can switch between the Leptos hydration and HTMX
/// SSR shells per request.
pub fn register_shell_renderer_v2(renderer: ShellRendererV2) {
    let _ = SHELL_RENDERER_V2.set(renderer);
}

/// Register the htmx-ssr fragment renderer.
///
/// Without this registered, requests with `HX-Request: true` to htmx-ssr routes
/// fall back to the full shell and log a warning — the swap will still work but
/// duplicates the shell content.
pub fn register_fragment_renderer(renderer: FragmentRenderer) {
    let _ = FRAGMENT_RENDERER.set(renderer);
}

fn render_shell(
    site_options: SiteOptions,
    path: Option<String>,
    profile: RenderingProfile,
) -> String {
    let htmx_extensions = if matches!(profile, RenderingProfile::HtmxSsr) {
        workspace_rendering_config().htmx_extensions.clone()
    } else {
        Vec::new()
    };
    if let Some(renderer) = SHELL_RENDERER_V2.get() {
        return renderer(site_options, path, profile, htmx_extensions);
    }
    if let Some(renderer) = SHELL_RENDERER.get() {
        return renderer(site_options, path);
    }
    #[cfg(feature = "leptos-hydration")]
    {
        use leptos::prelude::*;
        let owner = Owner::new();
        return owner
            .with(|| shell_with_path(site_options, path, profile, htmx_extensions).to_html());
    }
    #[cfg(not(feature = "leptos-hydration"))]
    {
        tracing::warn!(
            "render_shell called with no registered renderer and leptos-hydration disabled — \
             returning empty string"
        );
        String::new()
    }
}

/// Helper function to render SSR with proper reactive context (Owner needed for
/// / route). Uses the workspace-default [`RenderingProfile`].
pub fn render_ssr_with_context(site_options: SiteOptions, path: Option<String>) -> String {
    render_shell(site_options, path, workspace_rendering_profile())
}

/// Render SSR with an explicit [`RenderingProfile`] override.
pub fn render_ssr_with_profile(
    site_options: SiteOptions,
    path: Option<String>,
    profile: RenderingProfile,
) -> String {
    render_shell(site_options, path, profile)
}

/// Helper function to render SSR using routing engine's resolution.
///
/// The effective [`RenderingProfile`] is taken from the resolution itself
/// (already resolved via route -> workspace -> default precedence).
pub fn render_ssr_with_route_resolution(
    site_options: SiteOptions,
    resolution: &rustelo_core_lib::routing::engine::RouteResolution,
) -> String {
    let path = resolution.path.clone();
    let profile = resolution.rendering_profile;

    tracing::info!(
        "🔧 render_ssr_with_route_resolution using resolved path: '{}' profile: {}",
        path,
        profile
    );

    render_shell(site_options, Some(path), profile)
}

/// Render only the page body without the HTML shell, for HTMX fragment swaps.
///
/// Delegates to the [`FRAGMENT_RENDERER`] registered by the implementation;
/// when none is registered, returns `None` and the caller must fall back.
pub fn render_fragment_with_route_resolution(
    site_options: SiteOptions,
    resolution: &rustelo_core_lib::routing::engine::RouteResolution,
) -> Option<String> {
    FRAGMENT_RENDERER
        .get()
        .map(|renderer| renderer(site_options, resolution))
}

/// Decide between full-page, fragment, and template-engine rendering based on
/// request headers and the route resolution.
///
/// Decision matrix:
///
/// | resolution.template | profile          | HX-Request | HX-Boosted | response                  |
/// |---------------------|------------------|-----------:|-----------:|---------------------------|
/// | `Some(_)`           | any              |     false  |       any  | full page via `TemplateEngine` |
/// | `Some(_)`           | any              |      true  |      false | block fragment via `TemplateEngine` |
/// | `Some(_)`           | any              |      true  |       true | full page (boosted nav)   |
/// | `None`              | LeptosHydration  |       any  |       any  | full shell (hydration)    |
/// | `None`              | HtmxSsr          |     false  |       any  | full shell (no WASM)      |
/// | `None`              | HtmxSsr          |      true  |       true | full shell (boosted nav)  |
/// | `None`              | HtmxSsr          |      true  |      false | body fragment             |
pub fn render_route_response(
    site_options: SiteOptions,
    resolution: &rustelo_core_lib::routing::engine::RouteResolution,
    headers: &axum::http::HeaderMap,
) -> axum::response::Response {
    // Make request headers available to the registered shell/fragment
    // renderers via `current_request_headers()`. The task_local lives only
    // for the synchronous extent of this call; sync_scope unsets it on exit.
    let headers_clone = headers.clone();
    REQUEST_HEADERS.sync_scope(headers_clone, move || {
        render_route_response_inner(site_options, resolution, headers)
    })
}

fn render_route_response_inner(
    site_options: SiteOptions,
    resolution: &rustelo_core_lib::routing::engine::RouteResolution,
    headers: &axum::http::HeaderMap,
) -> axum::response::Response {
    use axum::http::StatusCode;
    use axum::response::IntoResponse;

    use crate::utils::htmx::{htmx_error_response, htmx_target, is_htmx_boosted, is_htmx_request};

    let htmx_request = is_htmx_request(headers);
    let boosted = is_htmx_boosted(headers);

    // Template-engine routes bypass Leptos entirely.
    if let Some(template_name) = resolution.template.as_deref() {
        let Some(engine) = template_engine() else {
            tracing::warn!(
                template = %template_name,
                "kind=template route requested but TemplateEngine not registered"
            );
            return htmx_error_response(
                headers,
                StatusCode::SERVICE_UNAVAILABLE,
                "template engine not registered",
            );
        };

        let ctx = build_template_context(resolution);
        let result = if htmx_request && !boosted {
            // Block name is derived from the HX-Target header (CSS id minus leading `#`).
            let block = htmx_target(headers)
                .map(|t| t.trim_start_matches('#').to_string())
                .unwrap_or_else(|| "content".to_string());
            engine.render_block(template_name, &block, ctx)
        } else {
            engine.render_page(template_name, ctx)
        };

        return match result {
            Ok(body) => axum::response::Html(body).into_response(),
            Err(e) => {
                tracing::error!(
                    template = %template_name,
                    error = %e,
                    "template render failed"
                );
                htmx_error_response(headers, StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
            }
        };
    }

    // Component routes: existing profile dispatch.
    if matches!(resolution.rendering_profile, RenderingProfile::HtmxSsr) && htmx_request && !boosted
    {
        if let Some(body) = render_fragment_with_route_resolution(site_options.clone(), resolution)
        {
            return axum::response::Html(body).into_response();
        }
        tracing::warn!(
            path = %resolution.path,
            "htmx fragment request received but no fragment renderer registered; falling back to full shell",
        );
    }

    axum::response::Html(render_ssr_with_route_resolution(site_options, resolution)).into_response()
}

/// Build the default Minijinja context exposed to template routes.
fn build_template_context(
    resolution: &rustelo_core_lib::routing::engine::RouteResolution,
) -> minijinja::Value {
    minijinja::context! {
        path => resolution.path.clone(),
        canonical_path => resolution.canonical_path.clone(),
        language => resolution.language.clone(),
        parameters => resolution.parameters.clone(),
        rendering_profile => resolution.rendering_profile.as_str(),
    }
}

// Import AppState from the dedicated app_state module
use crate::app_state::AppState;
#[cfg(feature = "content-db")]
use crate::content::{create_content_routes, ContentRepository, ContentService, ContentSource};
#[cfg(feature = "metrics")]
use crate::metrics::{create_metrics_routes, metrics_middleware, MetricsService};

/// Run the Axum server with all features and routes properly configured
///
/// This refactored function uses modular startup functions for better
/// maintainability. Each aspect of server initialization is handled by focused,
/// single-responsibility functions located in the startup module.
pub async fn run_server() -> Result<(), Box<dyn std::error::Error>> {
    use crate::startup::*;

    // 1. Environment setup
    setup_environment().await?;

    // 2. Load and validate configuration
    let config = load_and_validate_config().await?;

    // 3. Setup logging and panic handling
    setup_logging_and_panic_handling(&config)?;

    // 4. Initialize navigation tracking system
    initialize_navigation_tracking(&config)?;

    // Initialize any_spawner executor for Leptos reactive graph SSR. Only
    // needed when leptos-hydration is active — htmx-ssr has no reactive signals.
    #[cfg(feature = "leptos-hydration")]
    let _ = any_spawner::Executor::init_tokio();

    // 4b. Initialize meta configuration preloader for SSR (after logging is set up)
    rustelo_core_lib::init_meta_preloader();

    // 5. Initialize cache system
    let (_cache_dir, _shared_cache_config, content_cache) =
        initialize_cache_system(&config).await?;

    // 6. Setup site options (addr + root; leptos-hydration converts to
    //    LeptosOptions internally)
    let site_options = setup_site_options(&config)?;

    // 7. Initialize security components
    let (csrf_state, rate_limiter, security_headers_config) =
        initialize_security_components(&config);

    // 8. Initialize database if needed
    #[cfg(any(feature = "auth", feature = "content-db", feature = "post-engagement"))]
    let (_database, _pool, database_pool) = initialize_database(&config).await?;

    // Per-post engagement repository (views/ratings/messages). Shares the same
    // database-agnostic pool; mirrors the auth repository wiring.
    #[cfg(feature = "post-engagement")]
    let engagement_repository = std::sync::Arc::new(
        crate::database::engagement::EngagementRepository::from_pool(&database_pool),
    );

    // 9. Initialize services based on enabled features
    #[cfg(feature = "email")]
    let email_service = initialize_email_service(&config).await?;

    #[cfg(feature = "auth")]
    let (auth_service, auth_repository, jwt_service) = initialize_auth_services(
        &database_pool,
        &config,
        #[cfg(feature = "email")]
        Some(email_service.clone()),
    )
    .await?;

    #[cfg(feature = "content-db")]
    let content_service = initialize_content_services(&database_pool);

    #[cfg(feature = "metrics")]
    let metrics_result = initialize_metrics_service(&config)?;
    #[cfg(feature = "metrics")]
    let metrics_registry = metrics_result
        .as_ref()
        .map(|(_, registry)| registry.clone())
        .flatten();

    // 9b. Initialize file-based RBAC config (loads rbac.ncl; falls back to empty
    // policy on error)
    #[cfg(feature = "rbac")]
    let file_rbac_config = {
        use std::sync::Arc;

        use tokio::sync::RwLock;

        use crate::rbac::file_config::FileRbacConfig;
        let path = std::path::PathBuf::from(&config.rbac.ncl_path);
        let cfg = match FileRbacConfig::load(&path) {
            Ok(cfg) => {
                tracing::info!(
                    path = %path.display(),
                    groups = cfg.groups.len(),
                    user_overrides = cfg.user_overrides.len(),
                    "rbac.ncl loaded"
                );
                cfg
            }
            Err(e) => {
                tracing::warn!(
                    path = %path.display(),
                    error = %e,
                    "rbac.ncl not found or invalid, using empty policy (all requests allowed)"
                );
                FileRbacConfig::default()
            }
        };
        Arc::new(RwLock::new(cfg))
    };

    // 9c. Build WebSocket server with RBAC deny-path sync when rbac-watcher is
    // enabled. The deny_paths cache bridges the watcher and new WS connections
    // for initial sync.
    use crate::content::websocket::{WebSocketConfig, WebSocketServer};
    let ws_server = {
        use std::sync::Arc;

        #[allow(unused_imports)]
        use tokio::sync::RwLock;

        #[cfg(feature = "rbac")]
        {
            let initial_deny_paths =
                crate::rbac::watcher::extract_anonymous_deny_paths(&*file_rbac_config.read().await);
            let deny_cache = Arc::new(RwLock::new(initial_deny_paths));
            Arc::new(WebSocketServer::with_deny_paths(
                WebSocketConfig::default(),
                Arc::clone(&deny_cache),
            ))
        }
        #[cfg(not(feature = "rbac"))]
        Arc::new(WebSocketServer::new(WebSocketConfig::default()))
    };

    // 9d. Spawn hot-reload watcher for rbac.ncl — broadcasts RbacPolicyUpdated on
    // reload
    #[cfg(feature = "rbac-watcher")]
    {
        use crate::rbac::watcher::watch_file_rbac_with_cache;
        let watcher_path = std::path::PathBuf::from(&config.rbac.ncl_path);
        let watcher_config = std::sync::Arc::clone(&file_rbac_config);
        let policy_tx = ws_server.broadcast_tx.clone();
        let deny_cache = ws_server.current_deny_paths.clone();
        tracing::info!(path = %watcher_path.display(), "rbac file watcher started");
        tokio::spawn(async move {
            // Wrap the watcher to also update the deny_cache on reload
            if let Err(e) =
                watch_file_rbac_with_cache(watcher_path, watcher_config, policy_tx, deny_cache)
                    .await
            {
                tracing::error!(error = %e, "rbac file watcher terminated unexpectedly");
            }
        });
    }

    // 9e. Connect NATS admin client and spawn subscriber tasks
    #[cfg(feature = "nats-admin")]
    let nats_client = {
        use std::sync::Arc;

        use crate::nats::executor::PipelineContext;
        #[cfg(not(feature = "pipeline-watcher"))]
        use crate::nats::pipeline::load_pipeline_config;
        #[cfg(not(feature = "pipeline-watcher"))]
        use crate::startup::nats::spawn_pipeline_subscribers;
        use crate::startup::nats::{initialize_nats_admin, spawn_nats_admin_subscribers};

        match &config.external_services.nats_admin {
            Some(nats_cfg) => match initialize_nats_admin(nats_cfg).await {
                Ok(Some((client, log_buffer))) => {
                    spawn_nats_admin_subscribers(client.clone(), log_buffer);

                    // Pipeline path comes from config (defaults to
                    // PIPELINES_NCL_PATH or ./site/pipelines/index.ncl).
                    let pipeline_path = std::path::PathBuf::from(&config.pipelines.ncl_path);
                    let scripts_root = config.pipelines.scripts_root.clone();

                    let ctx = Arc::new(PipelineContext {
                        ws: ws_server.clone(),
                        cache: content_cache.clone(),
                        #[cfg(feature = "auth")]
                        auth_repo: auth_repository.clone(),
                        scripts_root,
                    });

                    // Without pipeline-watcher: load once at startup.
                    #[cfg(not(feature = "pipeline-watcher"))]
                    match load_pipeline_config(&pipeline_path) {
                        Ok(cfg) if !cfg.pipelines.is_empty() => {
                            spawn_pipeline_subscribers(client.clone(), cfg.pipelines, ctx);
                        }
                        Ok(_) => {}
                        Err(e) => {
                            tracing::warn!(
                                error = %e,
                                "pipelines: config load failed, subscriptions disabled"
                            );
                        }
                    }

                    // With pipeline-watcher: the watcher handles initial load
                    // AND reloads; no separate spawn_pipeline_subscribers call.
                    #[cfg(feature = "pipeline-watcher")]
                    {
                        let watcher_client = client.clone();
                        let watcher_ctx = Arc::clone(&ctx);
                        tracing::info!(
                            path = %pipeline_path.display(),
                            "pipeline watcher starting"
                        );
                        tokio::spawn(async move {
                            if let Err(e) = crate::nats::watcher::watch_pipeline_config(
                                pipeline_path,
                                watcher_client,
                                watcher_ctx,
                            )
                            .await
                            {
                                tracing::error!(
                                    error = %e,
                                    "pipeline watcher terminated unexpectedly"
                                );
                            }
                        });
                    }

                    Some(Arc::new(client))
                }
                Ok(None) => None,
                Err(e) => {
                    tracing::warn!(error = %e, "nats-admin: connection failed, continuing without NATS");
                    None
                }
            },
            None => {
                tracing::debug!("nats-admin: not configured in external_services");
                None
            }
        }
    };

    // 10. Create application state
    let app_state = AppState {
        site_options: site_options.clone(),
        csrf_state,
        rate_limiter,
        #[cfg(feature = "auth")]
        auth_service: auth_service.clone(),
        #[cfg(feature = "auth")]
        jwt_service: jwt_service.clone(),
        #[cfg(feature = "auth")]
        auth_repository: auth_repository.clone(),
        #[cfg(feature = "post-engagement")]
        engagement_repository,
        #[cfg(feature = "content-db")]
        content_service: content_service.clone(),
        #[cfg(feature = "email")]
        email_service: email_service.clone(),
        #[cfg(feature = "metrics")]
        metrics_registry,
        #[cfg(feature = "rbac")]
        file_rbac_config,
        ws_server: ws_server.clone(),
        #[cfg(feature = "nats-admin")]
        nats_client,
    };

    // 11. Create main application router
    let main_app = create_main_router(&config, site_options.clone(), app_state.clone())?;

    // 12. Create API router with all service routes
    let api_router = create_api_router(
        &config,
        &app_state,
        content_cache.clone(),
        #[cfg(feature = "auth")]
        auth_service.clone(),
        #[cfg(feature = "content-db")]
        content_service.clone(),
        #[cfg(feature = "email")]
        email_service.clone(),
        ws_server.clone(),
    )?;

    // 13. Combine routers
    // Leptos server functions are registered with prefix "/api" but
    // handle_server_fns must receive the full path (including "/api") for
    // dispatch to work. Mount it here, outside the nested "/api" router, so
    // Axum does not strip the prefix.
    // Leptos server-function dispatcher: only needed when leptos-hydration is
    // active. htmx-ssr uses conventional Axum routes for all form submissions.
    #[cfg(feature = "leptos-hydration")]
    let app_with_api = main_app.nest("/api", api_router).route(
        "/api/{*fn_name}",
        axum::routing::post(leptos_axum::handle_server_fns),
    );
    #[cfg(not(feature = "leptos-hydration"))]
    let app_with_api = main_app.nest("/api", api_router);

    // 14. Apply all middleware layers
    let final_app = apply_middleware_layers(
        app_with_api,
        &config,
        &app_state,
        security_headers_config,
        #[cfg(feature = "auth")]
        jwt_service.clone(),
        #[cfg(feature = "auth")]
        auth_repository.clone(),
    );

    // 15. Start the server
    start_server(final_app, &config).await?;

    Ok(())
}

/// Main entry point for the Axum/Leptos server.
///
/// Uses `tokio::runtime::LocalRuntime` (tokio_unstable) so that `spawn_local`
/// is valid from *any* task context — including connection tasks that
/// `axum::serve` spawns internally via `tokio::spawn`.
///
/// The previous `current_thread + LocalSet::run_until` pattern broke because
/// `LocalSet` only activates its local context while *its own* future is being
/// polled; tasks spawned by `tokio::spawn` inside `axum::serve` escape that
/// scope and call `spawn_local` → panic.  `LocalRuntime` makes the entire
/// runtime "local", eliminating the scope gap.
pub fn run_main() -> Result<(), Box<dyn std::error::Error>> {
    crate::utils::init();
    let rt = tokio::runtime::LocalRuntime::new()?;
    rt.block_on(run_server())
}
