//! Admin overview dashboard aggregates.
//!
//! One composed endpoint feeding the Overview tab's per-section summary
//! cards (Users, Mangas, Analysis). Pure DB aggregates — no system
//! sampling — so it's cheap enough to poll. The System and Crawler cards
//! reuse their own existing endpoints (`/admin/system`, `/admin/crawler`),
//! and the Settings strip reuses the settings endpoints; only these three
//! sections needed new aggregation, so they live together here.
//!
//! Admin-only (`RequireAdmin`, cookie-only).

use std::sync::OnceLock;
use std::time::{Duration, Instant};

use axum::extract::State;
use axum::routing::get;
use axum::{Json, Router};
use chrono::{DateTime, Utc};
use serde::Serialize;
use tokio::sync::Mutex;

use crate::app::AppState;
use crate::auth::extractor::RequireAdmin;
use crate::error::AppResult;
use crate::repo;

pub fn routes() -> Router<AppState> {
    Router::new().route("/admin/overview", get(overview))
}

#[derive(Debug, Clone, Serialize)]
pub struct OverviewStats {
    pub users: UsersOverview,
    pub mangas: repo::admin_view::MangaStats,
    pub analysis: AnalysisOverview,
}

#[derive(Debug, Clone, Serialize)]
pub struct UsersOverview {
    pub total: i64,
    pub admins: i64,
    pub newest_username: Option<String>,
    pub newest_created_at: Option<DateTime<Utc>>,
}

#[derive(Debug, Clone, Serialize)]
pub struct AnalysisOverview {
    pub analyzed_pages: i64,
    pub total_pages: i64,
}

/// Process-wide single-flight cache for the overview aggregate.
///
/// The admin dashboard polls this endpoint on a timer; without coalescing, a
/// slow DB (e.g. a deep crawl/analysis backlog) lets identical requests stack
/// and pin Postgres. Holding the async mutex across the recompute gives BOTH a
/// short-TTL cache and single-flight: concurrent pollers block on the lock,
/// then read the freshly cached value instead of each launching their own
/// aggregate. Resets on process restart, which is fine for a dashboard stat.
///
/// Unlike per-`AppState` shared state (e.g. `auth_limiter`), this is a single
/// production-instance cache, so a process global is adequate. It is
/// test-safe because the cache lives in the handler *body*: the two auth tests
/// are rejected at the `RequireAdmin` extractor before reaching it, and the
/// one aggregate test issues a single request. A future test that mutates data
/// and re-reads within the TTL would need to account for this.
static OVERVIEW_CACHE: OnceLock<Mutex<Option<(Instant, OverviewStats)>>> = OnceLock::new();
const OVERVIEW_TTL: Duration = Duration::from_secs(10);

async fn overview(
    State(state): State<AppState>,
    _admin: RequireAdmin,
) -> AppResult<Json<OverviewStats>> {
    let cache = OVERVIEW_CACHE.get_or_init(|| Mutex::new(None));
    let mut guard = cache.lock().await;
    if let Some((at, cached)) = guard.as_ref() {
        if at.elapsed() < OVERVIEW_TTL {
            return Ok(Json(cached.clone()));
        }
    }

    // Independent reads — run concurrently so latency is the slowest
    // query, not their sum (mirrors the storage handler).
    let (user_counts, user_newest, mangas, coverage) = tokio::try_join!(
        repo::user::counts(&state.db),
        repo::user::newest(&state.db),
        repo::admin_view::manga_stats(&state.db),
        repo::page_analysis::library_coverage(&state.db),
    )?;

    let (total, admins) = user_counts;
    let (newest_username, newest_created_at) = match user_newest {
        Some((username, created_at)) => (Some(username), Some(created_at)),
        None => (None, None),
    };
    let (analyzed_pages, total_pages) = coverage;

    let stats = OverviewStats {
        users: UsersOverview {
            total,
            admins,
            newest_username,
            newest_created_at,
        },
        mangas,
        analysis: AnalysisOverview {
            analyzed_pages,
            total_pages,
        },
    };
    *guard = Some((Instant::now(), stats.clone()));
    Ok(Json(stats))
}
