//! WebSocket handlers for CRDT-sync agent registration and event push.

use crate::service::gateway::{self, GatewayState};
use futures::{SinkExt, StreamExt};
use poem::handler;
use poem::http::StatusCode;
use poem::web::websocket::{Message as WsMessage, WebSocket};
use poem::web::{Data, Query};
use serde::Deserialize;
use std::sync::Arc;

// ── CRDT-sync WebSocket handler (agent registration) ─────────────────────────

/// Query parameters accepted on the `/crdt-sync` WebSocket upgrade in gateway mode.
#[derive(Deserialize)]
struct GatewayCrdtSyncParams {
    /// One-time join token from `POST /gateway/tokens`.
    token: Option<String>,
    /// Human-readable label for the connecting agent.
    label: Option<String>,
    /// WebSocket address the agent exposes for mesh connections
    /// (e.g. `ws://0.0.0.0:3002/crdt-sync`).
    address: Option<String>,
}

/// `GET /crdt-sync` — gateway-side WebSocket endpoint for build agent registration.
///
/// # Authentication
///
/// The connecting node must supply a valid one-time join token via the `token`
/// query parameter, obtained from `POST /gateway/tokens`.  The token is
/// validated pre-upgrade and consumed by [`gateway::register_agent`] once the
/// WebSocket is established.
///
/// # Protocol
///
/// 1. Pre-upgrade: token existence is verified (read-only).
/// 2. Post-upgrade: [`gateway::register_agent`] writes the node to the CRDT and
///    consumes the token.
/// 3. Keepalive: the server sends Ping frames every 30 s; each Pong resets the
///    pong deadline and calls [`gateway::heartbeat_agent`].
/// 4. Disconnect: on clean close or pong timeout, [`gateway::remove_agent`]
///    tombstones the node (`alive = false`).
#[handler]
pub async fn gateway_crdt_sync_handler(
    ws: WebSocket,
    state: Data<&Arc<GatewayState>>,
    Query(params): Query<GatewayCrdtSyncParams>,
    remote_addr: &poem::web::RemoteAddr,
) -> poem::Response {
    use crate::crdt_sync::{PING_INTERVAL_SECS, PONG_TIMEOUT_SECS};

    // ── Pre-upgrade: validate token exists ─────────────────────────────
    let token = match params.token {
        Some(t) if !t.is_empty() => t,
        _ => {
            return poem::Response::builder()
                .status(StatusCode::UNAUTHORIZED)
                .body("token query parameter required");
        }
    };

    {
        let tokens = state.pending_tokens.read().await;
        if !tokens.contains_key(&token) {
            return poem::Response::builder()
                .status(StatusCode::UNAUTHORIZED)
                .body("invalid or already-used join token");
        }
    }

    let label = params
        .label
        .filter(|l| !l.is_empty())
        .unwrap_or_else(|| "build-agent".to_string());
    let address = params
        .address
        .filter(|a| !a.is_empty())
        .unwrap_or_else(|| remote_addr.to_string());

    // ── WebSocket upgrade ───────────────────────────────────────────────
    use poem::IntoResponse as _;
    let state = Arc::clone(&state);
    ws.on_upgrade(move |socket| async move {
        // Register the agent — consumes the one-time token.
        let node = match gateway::register_agent(&state, &token, label, address).await {
            Ok(n) => n,
            Err(e) => {
                crate::slog!("[gateway/crdt-sync] Registration failed: {e}");
                return;
            }
        };
        let node_id = node.node_id.clone();
        crate::slog!(
            "[gateway/crdt-sync] Agent '{}' registered, node_id={:.12}…",
            node.label.as_deref().unwrap_or("?"),
            &node_id
        );

        let (mut sink, mut stream) = socket.split();

        let mut pong_deadline =
            tokio::time::Instant::now() + std::time::Duration::from_secs(PONG_TIMEOUT_SECS);
        let mut ping_ticker = tokio::time::interval_at(
            tokio::time::Instant::now() + std::time::Duration::from_secs(PING_INTERVAL_SECS),
            std::time::Duration::from_secs(PING_INTERVAL_SECS),
        );

        loop {
            tokio::select! {
                _ = ping_ticker.tick() => {
                    if tokio::time::Instant::now() >= pong_deadline {
                        crate::slog!(
                            "[gateway/crdt-sync] No pong from {:.12}… in {}s; disconnecting",
                            &node_id, PONG_TIMEOUT_SECS
                        );
                        break;
                    }
                    if sink.send(WsMessage::Ping(vec![])).await.is_err() {
                        break;
                    }
                }
                frame = stream.next() => {
                    match frame {
                        Some(Ok(WsMessage::Pong(_))) => {
                            pong_deadline = tokio::time::Instant::now()
                                + std::time::Duration::from_secs(PONG_TIMEOUT_SECS);
                            gateway::heartbeat_agent(&node_id);
                        }
                        Some(Ok(WsMessage::Ping(data))) => {
                            let _ = sink.send(WsMessage::Pong(data)).await;
                        }
                        Some(Ok(WsMessage::Close(_))) | None => break,
                        _ => {}
                    }
                }
            }
        }

        gateway::remove_agent(&node_id);
        crate::slog!(
            "[gateway/crdt-sync] Agent {:.12}… disconnected and tombstoned",
            &node_id
        );
    })
    .into_response()
}

// ── Event-push WebSocket handler ─────────────────────────────────────────────

/// Query parameters accepted on the `/gateway/events/push` WebSocket upgrade.
#[derive(Deserialize)]
struct EventPushQueryParams {
    /// One-time join token generated by `POST /gateway/tokens`.
    token: Option<String>,
    /// The project name this node represents (e.g. `"huskies"`).
    project: Option<String>,
}

/// `GET /gateway/events/push` — WebSocket endpoint for project nodes to push
/// [`StatusEvent`] frames to the gateway.
///
/// # Authentication
///
/// The connecting node must supply a valid one-time join token via the `token`
/// query parameter, obtained from `POST /gateway/tokens`.  The token is
/// consumed on the first successful upgrade — the connection itself is then
/// kept open indefinitely.
///
/// # Protocol
///
/// Each message from the project node must be a JSON-encoded
/// [`crate::service::events::StoredEvent`].  The gateway fan-outs the event
/// (tagged with the project name) to all current local subscribers.
///
/// The server does not send data back; clients should treat any close frame
/// as a signal to reconnect with exponential back-off (see docs/gateway-protocol.html).
///
/// # Reconnect-with-backoff
///
/// Project nodes MUST reconnect on disconnect.  Recommended policy:
///
/// - Initial retry delay: **1 s**
/// - Back-off multiplier: **2×** per attempt
/// - Max delay cap: **60 s**
/// - Jitter: add ±10 % to the delay to avoid thundering herds
#[handler]
pub async fn gateway_event_push_handler(
    ws: WebSocket,
    state: Data<&Arc<GatewayState>>,
    Query(params): Query<EventPushQueryParams>,
) -> poem::Response {
    // ── Authentication (pre-upgrade) ─────────────────────────────────────
    let token = match params.token {
        Some(t) if !t.is_empty() => t,
        _ => {
            return poem::Response::builder()
                .status(StatusCode::UNAUTHORIZED)
                .body("token query parameter required");
        }
    };

    let project = match params.project {
        Some(p) if !p.is_empty() => p,
        _ => {
            return poem::Response::builder()
                .status(StatusCode::BAD_REQUEST)
                .body("project query parameter required");
        }
    };

    // Validate and consume the one-time token.
    {
        let mut tokens = state.pending_tokens.write().await;
        if !tokens.contains_key(&token) {
            return poem::Response::builder()
                .status(StatusCode::UNAUTHORIZED)
                .body("invalid or already-used join token");
        }
        tokens.remove(&token);
    }

    // ── WebSocket upgrade ────────────────────────────────────────────────
    use poem::IntoResponse as _;
    let state = Arc::clone(&state);
    ws.on_upgrade(move |socket| async move {
        let (_, mut stream) = socket.split();

        crate::slog!(
            "[gateway] Project node '{}' connected to event-push endpoint",
            project
        );

        while let Some(msg) = stream.next().await {
            let text = match msg {
                Ok(WsMessage::Text(t)) => t,
                Ok(WsMessage::Close(_)) | Err(_) => break,
                _ => continue,
            };

            match serde_json::from_str::<crate::service::events::StoredEvent>(&text) {
                Ok(event) => {
                    gateway::broadcast_status_event(&state, project.clone(), event);
                }
                Err(e) => {
                    crate::slog!(
                        "[gateway] event-push: invalid frame from '{}': {e}",
                        project
                    );
                }
            }
        }

        crate::slog!(
            "[gateway] Project node '{}' disconnected from event-push endpoint",
            project
        );
    })
    .into_response()
}
