use std::convert::Infallible;
use std::time::Duration;

use axum::Json;
use axum::extract::{Query, State};
use axum::response::sse::{Event, KeepAlive, Sse};
use futures::stream::Stream;
use serde::{Deserialize, Serialize};
use tokio_stream::StreamExt;
use tokio_stream::wrappers::BroadcastStream;
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;

use crate::auth::middleware::AuthUser;
use crate::error::AppError;
use crate::models::session::Session;
use crate::services::sse_tickets::TicketKind;
use crate::state::AppState;

#[derive(Deserialize)]
pub struct SseQuery {
    pub ticket: String,
}

#[derive(Serialize)]
pub struct StreamTicketResponse {
    pub ticket: String,
    /// Server clock at mint time — the client seeds its SSE reconnect cursor from this
    /// instead of `new Date()`, so a skewed browser clock can't drop uploads. See
    /// `DeltaResponse::server_time`.
    pub server_time: chrono::DateTime<chrono::Utc>,
}

/// Mint a short-lived single-use SSE ticket. The browser's `EventSource` cannot
/// send an `Authorization` header, so the alternative used to be passing the JWT
/// as `?token=...` — which leaks the bearer token into access logs, referer
/// headers, and browser history. The client now exchanges its Bearer token for
/// an opaque ticket via this endpoint and passes that on the stream open.
pub async fn issue_ticket(
    State(state): State<AppState>,
    auth: AuthUser,
) -> Result<Json<StreamTicketResponse>, AppError> {
    // The endpoint had no rate limit at all. Authentication is not a bound here: one valid
    // session could loop it freely. 60/min is far above a real client (one ticket per SSE
    // (re)connect, and reconnects are backed off) while capping a loop.
    if let Err(retry_after_secs) = state.rate_limiter.check_with_retry(
        format!("sse_ticket:{}", auth.user_id),
        60,
        Duration::from_secs(60),
    ) {
        return Err(AppError::TooManyRequests(
            "Zu viele Verbindungsversuche. Bitte warte kurz.".into(),
            Some(retry_after_secs),
        ));
    }

    let ticket = state
        .sse_tickets
        .issue(auth.token_hash, TicketKind::Sse)
        .ok_or_else(|| {
            AppError::ServiceUnavailable(
                "Server ist gerade ausgelastet. Live-Updates folgen in Kürze.".into(),
                Some(30),
            )
        })?;
    let server_time = sqlx::query_scalar("SELECT NOW()")
        .fetch_one(&state.pool)
        .await?;
    Ok(Json(StreamTicketResponse {
        ticket,
        server_time,
    }))
}

/// Live SSE streams one session may hold OPEN at once.
///
/// The ticket store's `MAX_TICKETS_PER_SESSION` bounds UNCONSUMED tickets, not open streams — so it
/// never bounded this at all: mint a ticket, redeem it (freeing the slot), repeat. At the 60/min
/// ticket ceiling one guest could accumulate 60 new live streams per minute indefinitely, each
/// holding a broadcast receiver, a tokio task and a 60-second DB revalidation ticker.
///
/// 6 rather than 2: a guest legitimately has the feed in one tab, the diashow on a laptop, and both
/// may briefly double during a reconnect before the old socket's `Drop` lands. Well above real use,
/// far below anything that hurts.
const MAX_OPEN_STREAMS_PER_SESSION: usize = 6;

/// Open stream count per session token hash.
type OpenStreams = std::collections::HashMap<String, usize>;
static OPEN_STREAMS: std::sync::LazyLock<std::sync::Mutex<OpenStreams>> =
    std::sync::LazyLock::new(|| std::sync::Mutex::new(OpenStreams::new()));

/// Decrements the open-stream count for its session when the stream is dropped.
///
/// A `Drop` guard is the only thing that works here: a client vanishing off wifi never runs any
/// cleanup path we write, but dropping the response future is exactly what happens.
struct StreamSlot(String);

impl Drop for StreamSlot {
    fn drop(&mut self) {
        if let Ok(mut map) = OPEN_STREAMS.lock()
            && let Some(n) = map.get_mut(&self.0)
        {
            *n = n.saturating_sub(1);
            if *n == 0 {
                map.remove(&self.0);
            }
        }
    }
}

/// Claim one of this session's stream slots, or `None` when it is already at the cap.
fn claim_stream_slot(token_hash: &str) -> Option<StreamSlot> {
    let mut map = match OPEN_STREAMS.lock() {
        Ok(m) => m,
        // Never let a poisoned lock take live updates down for the whole venue.
        Err(e) => e.into_inner(),
    };
    let n = map.entry(token_hash.to_string()).or_insert(0);
    if *n >= MAX_OPEN_STREAMS_PER_SESSION {
        return None;
    }
    *n += 1;
    Some(StreamSlot(token_hash.to_string()))
}

/// SSE stream endpoint. Authenticates via a single-use ticket (see
/// [`issue_ticket`]) — never the raw JWT.
pub async fn stream(
    State(state): State<AppState>,
    Query(q): Query<SseQuery>,
) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, AppError> {
    let token_hash = state
        .sse_tickets
        .consume(&q.ticket, TicketKind::Sse)
        .ok_or_else(|| AppError::Unauthorized("Ticket ungültig oder abgelaufen.".into()))?;

    // NOTE: this authenticates via ticket→session, not the `AuthUser` extractor. The
    // SSE stream is read-only, and under the read-only-ban model (USER_JOURNEYS §10)
    // banned users retain read access — so both minting a ticket and holding a stream
    // open are intentionally allowed for banned users; only writes are blocked.
    Session::find_by_token_hash(&state.pool, &token_hash)
        .await
        .map_err(|e| AppError::Internal(e.into()))?
        .ok_or_else(|| AppError::Unauthorized("Sitzung nicht gefunden.".into()))?;

    // Bound how many streams this session holds open — see MAX_OPEN_STREAMS_PER_SESSION. Refuse
    // rather than evict: closing somebody's live feed to make room for their own reconnect loop
    // reads exactly like the flakiness it would be trying to fix.
    let slot = claim_stream_slot(&token_hash).ok_or_else(|| {
        tracing::warn!("session at its open-SSE-stream cap; refusing another");
        AppError::TooManyRequests(
            "Zu viele offene Verbindungen. Bitte schließe andere Tabs.".into(),
            Some(10),
        )
    })?;

    let rx = state.sse_tx.subscribe();
    let events = BroadcastStream::new(rx).filter_map(|msg| match msg {
        Ok(sse_event) => Some(Ok(Event::default()
            .event(sse_event.event_type)
            .data(sse_event.data))),
        // A consumer that falls behind the broadcast buffer would otherwise silently
        // lose events, leaving its feed permanently stale. Instead of dropping the
        // gap, tell the client to resync — the frontend responds by running a full
        // feed-delta fetch (which reconciles both new uploads and deletions).
        Err(BroadcastStreamRecvError::Lagged(n)) => {
            tracing::warn!("SSE consumer lagged, dropped {n} event(s); emitting resync");
            Some(Ok(Event::default().event("resync").data(n.to_string())))
        }
    });

    // The session is only checked once at open. Re-validate it periodically so a
    // logged-out, expired, OR banned session's stream is closed rather than kept alive
    // until the client happens to disconnect. Bounds a stale stream to ~60s. NOTE: a ban is
    // deliberately read-only and does NOT revoke sessions (see `ban_user`), so the
    // `is_banned` re-read below is LOAD-BEARING — it is the only thing that stops a banned
    // user's live push stream. Do not remove it. Only a *definitive* gone/expired/banned
    // state ends the stream; a transient DB error just retries next tick.
    let pool = state.pool.clone();
    let session_hash = token_hash.clone();
    let session_gone = async move {
        // Owns the slot guard, and this future is owned by the returned stream — so the slot is
        // released exactly when the stream is dropped, including when the client simply walks out
        // of range and no cleanup code of ours ever runs.
        let _slot = slot;
        let mut ticker = tokio::time::interval(Duration::from_secs(60));
        ticker.tick().await; // consume the immediate first tick
        loop {
            ticker.tick().await;
            match Session::find_user_by_token_hash(&pool, &session_hash).await {
                Ok(Some(user)) if !user.is_banned => {} // still valid — keep streaming
                Ok(Some(_)) => break,                   // banned — cut the stream
                Ok(None) => break,                      // logged out or expired — stop
                Err(e) => {
                    tracing::warn!(error = ?e, "SSE session revalidation query failed; retrying");
                }
            }
        }
    };

    let stream = futures::StreamExt::take_until(events, Box::pin(session_gone));

    Ok(Sse::new(stream).keep_alive(
        KeepAlive::new()
            .interval(Duration::from_secs(30))
            .text("ping"),
    ))
}
