//! API router creation and service integration
//!
//! This module handles the creation of the API router with all feature-gated
//! service routes including auth, content, email, metrics, navigation testing,
//! and health endpoints.

use std::sync::Arc;

use axum::{
    extract::{Path, State},
    http::StatusCode,
    routing::{get, post},
    Json, Router,
};
use rustelo_core_lib::cache::content::ContentCache;
use serde::Serialize;

#[cfg(feature = "leptos-hydration")]
use crate::api::activity_routes;
use crate::api::{create_i18n_routes, create_menu_routes, htmx_routes, nav_test_router};
#[cfg(feature = "auth")]
use crate::auth::routes::create_auth_routes;
use crate::config::Config;
#[cfg(feature = "content-db")]
use crate::content::routes::create_content_routes;
use crate::content_loader::{create_content_loader_routes, ContentLoader};
#[cfg(feature = "email")]
use crate::handlers::email::create_email_routes;
use crate::health::create_health_routes;
#[cfg(feature = "metrics")]
use crate::metrics::routes::create_metrics_routes;

/// Create API router with all feature-gated service routes
#[allow(unused_variables)]
pub fn create_api_router(
    config: &Config,
    app_state: &crate::app_state::AppState,
    content_cache: Arc<ContentCache>,
    #[cfg(feature = "auth")] auth_service: Arc<crate::auth::AuthService>,
    #[cfg(feature = "content-db")] content_service: Arc<crate::content::ContentService>,
    #[cfg(feature = "email")] email_service: Arc<crate::email::EmailService>,
    ws_server: Arc<crate::content::websocket::WebSocketServer>,
) -> Result<axum::Router, Box<dyn std::error::Error>> {
    let mut api_router = Router::new();

    // Add menu routes (always available)
    let menu_routes = create_menu_routes();
    api_router = api_router.merge(menu_routes);

    // Add i18n routes (always available — progressive FTL loading for WASM clients)
    let i18n_routes = create_i18n_routes();
    api_router = api_router.merge(i18n_routes);

    // Activity download + HMAC callback — only available with leptos-hydration
    // (uses OtpServiceExtension from rustelo_components_leptos).
    #[cfg(feature = "leptos-hydration")]
    {
        api_router = api_router.merge(activity_routes());
    }

    // HTMX endpoints for theme toggle and language switch (htmx-ssr profile).
    // Mounted unconditionally — the framework decides at request time whether
    // the response is consumed by an htmx client.
    api_router = api_router.nest("/htmx", htmx_routes());

    // Cookie consent acceptance endpoint.
    api_router = api_router.nest("/htmx/cookies", crate::api::htmx_cookies_routes());

    // Per-field form validation endpoints.
    api_router = api_router.nest("/htmx/validate", crate::api::htmx_validate_routes());

    // Content grid fragment endpoint (tag filter, instant search, pagination).
    api_router = api_router.nest("/htmx/content", crate::api::htmx_content_routes());

    // SSE channel for toasts and live notifications. Publishers anywhere in
    // the process call api::publish_toast(); subscribers connect via
    // /api/htmx/notifications/sse.
    api_router = api_router.nest("/htmx/notifications", crate::api::htmx_sse_routes());

    // Per-post engagement fragments (views/ratings/messages). Stateless router;
    // handlers read the engagement repository + rate limiter from the AppState
    // request extension (layered in startup::middleware).
    #[cfg(feature = "post-engagement")]
    {
        api_router = api_router.nest("/htmx/post", crate::api::htmx_post_routes());
    }

    // HTMX contact form (form-encoded → result fragment, honeypot + rate limit +
    // best-effort email). Replaces the previously dead POST /api/contact target.
    #[cfg(feature = "email")]
    {
        // Newsletter subscription, relayed server-side to the provider in
        // newsletter.ncl. Gated on `email` like the other outbound-delivery
        // endpoints: a build without it has no business posting a visitor's
        // address anywhere.
        api_router = api_router.nest("/htmx/newsletter", crate::api::htmx_newsletter_routes());
        api_router = api_router.nest("/htmx/contact", crate::api::htmx_contact_routes());
        // One endpoint for every declared form: POST /api/htmx/form/{id}, spec read at
        // request time from site/config/forms/{id}.ncl. A new form is a .ncl
        // and a template — no build.
        api_router = api_router.nest("/htmx/form", crate::api::htmx_form_routes());
    }

    // Add auth routes if feature is enabled
    #[cfg(feature = "auth")]
    {
        let auth_routes = create_auth_routes().with_state(auth_service.clone());
        api_router = api_router.nest("/auth", auth_routes);

        // HTMX auth fragments. Shares the same AuthService so a single user
        // model backs both surfaces.
        let htmx_auth_routes = crate::api::htmx_auth_routes().with_state(auth_service.clone());
        api_router = api_router.nest("/htmx/auth", htmx_auth_routes);

        // HTMX user dashboard tabs + bookmarks/notes CRUD.
        let htmx_user_routes = crate::api::htmx_user_routes().with_state(auth_service);
        api_router = api_router.nest("/htmx/user", htmx_user_routes);
    }

    // Add content routes if feature is enabled
    #[cfg(feature = "content-db")]
    {
        let content_routes = create_content_routes().with_state(content_service);
        api_router = api_router.nest("/content", content_routes);
    }

    // Add email routes if feature is enabled
    #[cfg(feature = "email")]
    {
        let email_routes = create_email_routes().with_state(email_service);
        api_router = api_router.nest("/email", email_routes);
    }

    // Add content management API routes if content-db feature is enabled
    #[cfg(feature = "content-db")]
    {
        use crate::content::{routes::create_content_api_routes, ContentApiState};

        // Initialize content watcher if enabled
        #[cfg(feature = "content-watcher")]
        let content_api_state = {
            use crate::content::ContentWatcher;

            // Create content watcher - use unified path resolution
            let content_build_path = rustelo_core_lib::get_content_path();
            let watcher = ContentWatcher::new(
                &content_build_path,
                "scripts/content/build-localized-content.sh",
            )
            .expect("Failed to create content watcher");

            // Start watching in background
            let watcher_arc = Arc::new(watcher);
            let watcher_clone = watcher_arc.clone();
            tokio::spawn(async move {
                if let Err(e) = watcher_clone.start_watching().await {
                    tracing::error!("Failed to start content watcher: {}", e);
                } else {
                    tracing::info!("Content watcher started successfully");
                }
            });

            ContentApiState::with_watcher(watcher_arc)
        };

        #[cfg(not(feature = "content-watcher"))]
        let content_api_state = ContentApiState::new();

        let content_api_routes =
            create_content_api_routes().with_state(Arc::new(content_api_state));

        api_router = api_router.nest("/content", content_api_routes);
    }

    // Add static content meta API routes if content-static feature is enabled
    #[cfg(feature = "content-static")]
    {
        use axum::{extract::Path, http::StatusCode, response::Json, routing::get};

        let static_meta_routes = Router::new().route(
            "/meta/{content_type}/{language}",
            get(
                |Path((content_type, language)): Path<(String, String)>| async move {
                    match rustelo_core_lib::categories::load_meta_config(&content_type, &language) {
                        Ok(config) => Ok(Json(config)),
                        Err(e) => {
                            tracing::error!(
                                "Failed to load meta config for {}/{}: {}",
                                content_type,
                                language,
                                e
                            );
                            Err(StatusCode::NOT_FOUND)
                        }
                    }
                },
            ),
        );

        api_router = api_router.nest("/content", static_meta_routes);
    }

    // Add health check routes
    let health_routes = create_health_routes().with_state(app_state.clone());
    api_router = api_router.merge(health_routes);

    // Add content loader routes (always available for static content) - use unified
    // path resolution
    let content_root = rustelo_core_lib::path_resolution::get_content_path();
    let content_loader = ContentLoader::with_cache(content_root.into(), content_cache.clone());
    let content_loader_routes = create_content_loader_routes().with_state(content_loader);
    api_router = api_router.nest("/content", content_loader_routes);

    // Add navigation test API routes (if enabled via environment variable)
    if std::env::var("ENABLE_NAV_TEST_API").unwrap_or_default() == "true" {
        let nav_test_routes = nav_test_router();
        api_router = api_router.nest("/nav-test", nav_test_routes);
        tracing::info!("Navigation test API enabled at /api/nav-test");
    }

    // Add metrics routes if feature is enabled
    #[cfg(feature = "metrics")]
    {
        if config.app.enable_metrics {
            let metrics_routes = create_metrics_routes().with_state(app_state.clone());
            api_router = api_router.merge(metrics_routes);
        }
    }

    // Rendered HTML endpoint for WASM post viewer (no ContentLoader state needed)
    api_router = api_router.route(
        "/content-render/{content_type}/{language}/{slug}",
        get(render_content_for_wasm),
    );

    // WebSocket endpoint + toast broadcast endpoint, both sharing ws_server state.
    let ws_route = Router::new()
        .route(
            "/ws",
            get(crate::content::websocket::WebSocketServer::handle_upgrade),
        )
        .route("/notify", post(notify_broadcast))
        .with_state(ws_server);
    api_router = api_router.merge(ws_route);

    Ok(api_router)
}

// ── /api/notify ──────────────────────────────────────────────────────────────

#[derive(serde::Deserialize)]
struct NotifyPayload {
    kind: String,
    message: String,
    /// Optional target user UUID.  When set the message is persisted to
    /// `user_messages` in addition to being broadcast over the WebSocket.
    #[allow(dead_code)]
    user_id: Option<String>,
    /// Subject line for persistent messages (defaults to `kind` when omitted).
    #[allow(dead_code)]
    subject: Option<String>,
    /// Sender label shown in the user's inbox (defaults to "system").
    #[allow(dead_code)]
    sender_email: Option<String>,
}

/// POST /api/notify — broadcast a toast to all connected WebSocket clients.
///
/// Two authentication paths are accepted:
/// 1. **JWT** (`Authorization: Bearer <jwt>`) — role must be `admin` or
///    `moderator` (enforced by RBAC before this handler runs, verified again
///    in-handler as belt-and-suspenders).
/// 2. **Service token** (`X-Service-Token: <raw-token>`) — validated against
///    the `service_tokens` table.  Allows external services (CI/CD, monitoring)
///    to send notifications without a full user account.
///
/// When `user_id` is present the message is also persisted to `user_messages`
/// so it survives the WebSocket session.
///
/// ```text
/// # JWT path
/// curl -s -X POST http://localhost:3000/api/notify \
///   -H "Authorization: Bearer <jwt>" \
///   -H "Content-Type: application/json" \
///   -d '{"kind":"info","message":"Hello","user_id":"<uuid>"}'
///
/// # Service token path
/// curl -s -X POST http://localhost:3000/api/notify \
///   -H "X-Service-Token: <raw-token>" \
///   -H "Content-Type: application/json" \
///   -d '{"kind":"warning","message":"Deploy started"}'
/// ```
#[cfg(feature = "auth")]
async fn notify_broadcast(
    headers: axum::http::HeaderMap,
    axum::Extension(auth_ctx): axum::Extension<crate::auth::middleware::AuthContext>,
    axum::Extension(app_state): axum::Extension<crate::app_state::AppState>,
    State(ws): State<Arc<crate::content::websocket::WebSocketServer>>,
    Json(payload): Json<NotifyPayload>,
) -> StatusCode {
    use rustelo_core_lib::auth::Role;
    use sha2::{Digest, Sha256};

    // ── Authorisation ─────────────────────────────────────────────────────────
    // Path 1: JWT — admin, moderator, or supervisor role.
    let jwt_authorised = auth_ctx.user.as_ref().map_or(false, |u| {
        u.has_role(&Role::Admin)
            || u.has_role(&Role::Moderator)
            || u.has_role(&Role::Custom("supervisor".to_string()))
    });

    // Path 2: service token via X-Service-Token header.
    let service_authorised = if !jwt_authorised {
        if let Some(raw) = headers.get("X-Service-Token").and_then(|v| v.to_str().ok()) {
            let hash = Sha256::digest(raw.as_bytes())
                .iter()
                .fold(String::new(), |mut s, b| {
                    use std::fmt::Write;
                    let _ = write!(s, "{b:02x}");
                    s
                });
            app_state
                .auth_repository
                .validate_service_token(&hash, "notify")
                .await
                .ok()
                .flatten()
                .is_some()
        } else {
            false
        }
    } else {
        false
    };

    if !jwt_authorised && !service_authorised {
        return StatusCode::UNAUTHORIZED;
    }

    // ── Persistence (optional) ────────────────────────────────────────────────
    if let Some(ref uid_str) = payload.user_id {
        if let Ok(user_uuid) = uuid::Uuid::parse_str(uid_str) {
            let sender = payload
                .sender_email
                .as_deref()
                .unwrap_or("system")
                .to_owned();
            let subject = payload
                .subject
                .as_deref()
                .unwrap_or(payload.kind.as_str())
                .to_owned();
            if let Err(e) = app_state
                .auth_repository
                .insert_message(user_uuid, &sender, &subject, &payload.message)
                .await
            {
                tracing::warn!(error = %e, "notify_broadcast: failed to persist message");
            }
        }
    }

    // ── Broadcast ─────────────────────────────────────────────────────────────
    ws.broadcast_notification(
        crate::content::websocket::ContentNotification::ToastBroadcast {
            kind: payload.kind,
            message: payload.message,
        },
    )
    .await;
    StatusCode::OK
}

#[cfg(not(feature = "auth"))]
async fn notify_broadcast(
    State(ws): State<Arc<crate::content::websocket::WebSocketServer>>,
    Json(payload): Json<NotifyPayload>,
) -> StatusCode {
    ws.broadcast_notification(
        crate::content::websocket::ContentNotification::ToastBroadcast {
            kind: payload.kind,
            message: payload.message,
        },
    )
    .await;
    StatusCode::OK
}

/// Rendered content response for WASM post viewer.
/// `id` is the stable filesystem identifier (same across all languages).
/// `category` is the primary category, invariant across languages.
/// `translation_slugs` maps language code → localized URL slug of the
/// translation. `translation_ids` maps language code → stable id of the
/// translation. The WASM client uses these for language switching without
/// additional fetches.
#[derive(Debug, Serialize)]
struct RenderedContent {
    title: String,
    subtitle: String,
    html: String,
    /// Stable content identifier — same filename stem across all languages.
    id: String,
    /// Primary category — invariant across languages.
    category: String,
    /// lang → localized slug for the translation.
    translation_slugs: std::collections::HashMap<String, String>,
    /// lang → stable id for the translation (same as `id` in most cases).
    translation_ids: std::collections::HashMap<String, String>,
}

/// Handler: reads markdown by slug, returns rendered HTML + metadata as JSON.
/// Used by the WASM PostViewer via `create_resource` instead of DOM
/// manipulation.
async fn render_content_for_wasm(
    Path((content_type, language, slug)): Path<(String, String, String)>,
) -> Result<Json<RenderedContent>, (StatusCode, String)> {
    use rustelo_core_lib::fluent::{
        load_content_by_slug, load_content_index, process_markdown_file,
        rewrite_content_image_paths,
    };

    // Use the registered ContentKindRegistry (populated from content-kinds.toml at
    // startup), not get_enabled_content_types() which always returns the
    // compile-time fallback list.
    let registry = rustelo_core_lib::create_content_kind_registry();
    let valid_types: Vec<String> = if registry.kinds.is_empty() {
        rustelo_core_lib::get_enabled_content_types()
    } else {
        registry.kinds.keys().cloned().collect()
    };
    if !valid_types.contains(&content_type) {
        return Err((
            StatusCode::BAD_REQUEST,
            format!(
                "Invalid content type '{}'. Valid: {}",
                content_type,
                valid_types.join(", ")
            ),
        ));
    }

    // Load index to extract the stable item id and primary category.
    // id is the same across all languages (filename stem).
    // category is invariant: the WASM client uses these to switch languages without
    // extra fetches.
    //
    // Cross-language resolution: if slug is not found in the requested language's
    // index (e.g., the URL still carries the previous language's slug after a
    // fast language switch), search all other language indexes.  Once the
    // stable `id` is found, use it to load the correct file in the target
    // language.
    let local_item = load_content_index(&content_type, &language)
        .ok()
        .and_then(|idx| {
            idx.items
                .into_iter()
                .find(|item| item.slug == slug || item.id == slug)
        });

    let (item_id, item_category, item_translation_slugs, item_translation_ids) =
        if let Some(ref item) = local_item {
            let cat = item.categories.first().cloned().unwrap_or_default();
            (
                item.id.clone(),
                cat,
                item.translation_slugs.clone(),
                item.translation_ids.clone(),
            )
        } else {
            // Slug not found locally — walk other language indexes to resolve the stable
            // id.
            let resolved = rustelo_core_lib::i18n::discover_available_languages()
                .iter()
                .filter(|l| l.as_str() != language)
                .find_map(|lang| {
                    let idx = load_content_index(&content_type, lang).ok()?;
                    idx.items
                        .into_iter()
                        .find(|item| item.slug == slug || item.id == slug)
                        .map(|item| item.id)
                });
            match resolved {
                Some(id) => {
                    // Re-query the target-language index with the stable id to get category
                    // and translations.
                    let (cat, t_slugs, t_ids) = load_content_index(&content_type, &language)
                        .ok()
                        .and_then(|idx| {
                            idx.items
                                .into_iter()
                                .find(|item| item.id == id)
                                .map(|item| {
                                    (
                                        item.categories.into_iter().next().unwrap_or_default(),
                                        item.translation_slugs,
                                        item.translation_ids,
                                    )
                                })
                        })
                        .unwrap_or_default();
                    (id, cat, t_slugs, t_ids)
                }
                None => (
                    slug.clone(),
                    String::new(),
                    std::collections::HashMap::new(),
                    std::collections::HashMap::new(),
                ),
            }
        };

    // Use the resolved stable id so load_content_by_slug can locate the file
    // even when the original URL slug belongs to a different language.
    let effective_slug = if item_id != slug { &item_id } else { &slug };
    let markdown = load_content_by_slug(effective_slug, &content_type, &language)
        .map_err(|e| (StatusCode::NOT_FOUND, e.to_string()))?;

    let (metadata, html) = process_markdown_file(&markdown)
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

    let html = rewrite_content_image_paths(&html, &content_type, &language, &slug);

    let title = metadata.title.unwrap_or_else(|| {
        slug.replace('-', " ")
            .split_whitespace()
            .map(|word| {
                let mut chars = word.chars();
                match chars.next() {
                    None => String::new(),
                    Some(first) => {
                        let upper: String = first.to_uppercase().collect();
                        let lower: String = chars.collect::<String>().to_lowercase();
                        upper + lower.as_str()
                    }
                }
            })
            .collect::<Vec<_>>()
            .join(" ")
    });

    Ok(Json(RenderedContent {
        title,
        subtitle: metadata.subtitle.unwrap_or_default(),
        html,
        id: item_id,
        category: item_category,
        translation_slugs: item_translation_slugs,
        translation_ids: item_translation_ids,
    }))
}
