//! Session API endpoints — viewer-token minting.
//!
//! `POST /api/sessions/:id/viewer-token` mints a short-lived, session-scoped
//! viewer JWT for an authenticated dashboard user. This replaces the v1 model
//! where any dashboard JWT could join any session at the viewer WebSocket:
//! a viewer token is now bound to one `session_id`, and the WS layer (Task 3)
//! verifies signature + expiry + blacklist + that the token's `session_id`
//! matches the requested session.
//!
//! Authorization (Phase 1) — TIERED by permission (the authz-strength split that
//! fully closes audit CRITICAL #1). The requester must present a valid dashboard
//! JWT (the [`AuthenticatedUser`] extractor), the target session must exist in
//! the live session manager, AND the access mode of the minted token is decided
//! by the requester's permission:
//!   - `is_admin()` OR the `control` permission (see [`SESSION_CONTROL_PERMISSION`])
//!     → a CONTROL token (input is forwarded to the agent).
//!   - else the `view` permission (see [`SESSION_VIEW_PERMISSION`])
//!     → a VIEW-ONLY token (the relay refuses to forward this viewer's input;
//!     video still streams).
//!   - else → 403 (standard envelope).
//!
//! This is why the gate is no longer a single `view` check: `view` is held by
//! EVERY default role (incl. `viewer`), so a `view`-only token still granting
//! input control was an intra-tenant privilege escalation. The access mode is
//! stamped INSIDE the signed token; the relay enforces it (it cannot be forged
//! or upgraded client-side). This minting endpoint is the authorization decision
//! point. Per-tenant / per-machine ACL narrowing is a Phase-4 concern; the
//! tenancy claim is already carried so the WS and future phases can enforce it.

use axum::{
    extract::{Path, State},
    http::StatusCode,
    Json,
};
use serde::Serialize;
use uuid::Uuid;

use crate::auth::{AuthenticatedUser, ViewerAccess};
use crate::db;
use crate::AppState;

use super::machine_keys::ApiError;

type ApiResult<T> = Result<T, (StatusCode, Json<ApiError>)>;

/// Permission (in GC's existing catalog — see `api::users` `valid_permissions`:
/// `view`, `control`, `transfer`, `manage_users`, `manage_clients`) that grants
/// a CONTROL viewer token (input is forwarded to the agent). Held by the default
/// `admin` (implicitly, via `is_admin()`) and `operator` roles, NOT by `viewer`.
/// This is the discriminator that closes the authz-strength gap: a viewer token
/// no longer implies input control.
const SESSION_CONTROL_PERMISSION: &str = "control";

/// Permission that grants a VIEW-ONLY viewer token (watch only; the relay drops
/// this viewer's input). `view` is held by every default role (admin/operator/
/// viewer), so it is the floor for obtaining ANY viewer token. A user with
/// neither `control` nor `view` cannot mint a token at all (403).
const SESSION_VIEW_PERMISSION: &str = "view";

/// Response carrying a freshly minted viewer token.
#[derive(Debug, Serialize)]
pub struct ViewerTokenResponse {
    /// The short-lived viewer JWT. Use as the `token` query param on
    /// `/ws/viewer` for this session only.
    pub token: String,
    /// The session the token authorizes (echoed for client convenience).
    pub session_id: String,
    /// Token lifetime in seconds (for client-side refresh scheduling).
    pub expires_in_secs: i64,
    /// Access mode granted by this token: `"control"` (input forwarded) or
    /// `"view_only"` (watch only — the relay drops this viewer's input). Decided
    /// server-side by the requester's permission; echoed so the (future) UI can
    /// reflect the mode. Authoritative enforcement is the signed claim, not this
    /// field.
    pub access: String,
}

/// Build a standard error envelope (mirrors `machine_keys`).
fn err(status: StatusCode, code: &str, detail: &str) -> (StatusCode, Json<ApiError>) {
    (
        status,
        Json(ApiError {
            detail: detail.to_string(),
            error_code: code.to_string(),
            status_code: status.as_u16(),
        }),
    )
}

/// POST /api/sessions/:id/viewer-token
///
/// Mints a ~5-minute, session-scoped viewer token for the authenticated user.
pub async fn mint_viewer_token(
    user: AuthenticatedUser,
    State(state): State<AppState>,
    Path(id): Path<String>,
) -> ApiResult<Json<ViewerTokenResponse>> {
    let session_id = Uuid::parse_str(&id).map_err(|_| {
        err(
            StatusCode::BAD_REQUEST,
            "INVALID_SESSION_ID",
            "Invalid session ID",
        )
    })?;

    // TIERED AUTHORIZATION GATE (closes audit CRITICAL #1 — authz-strength split).
    // Authentication alone is not enough, and a single `view` check is too coarse
    // because `view` is held by EVERY default role: granting a `viewer`-role user
    // an input-capable token was intra-tenant privilege escalation. So the access
    // mode is decided by permission and stamped into the signed token:
    //   - admin OR `control`            → CONTROL token (relay forwards input)
    //   - else `view`                   → VIEW-ONLY token (relay drops input)
    //   - else                          → 403
    let access = if user.is_admin() || user.has_permission(SESSION_CONTROL_PERMISSION) {
        ViewerAccess::Control
    } else if user.has_permission(SESSION_VIEW_PERMISSION) {
        ViewerAccess::ViewOnly
    } else {
        tracing::warn!(
            "User {} denied viewer-token mint for session {} (lacks '{}'/'{}' permission)",
            user.username,
            session_id,
            SESSION_CONTROL_PERMISSION,
            SESSION_VIEW_PERMISSION
        );
        return Err(err(
            StatusCode::FORBIDDEN,
            "FORBIDDEN",
            "You do not have permission to view sessions",
        ));
    };

    // The session must exist (live session manager is the
    // source of truth for joinable sessions, matching GET /api/sessions/:id).
    let session = state
        .sessions
        .get_session(session_id)
        .await
        .ok_or_else(|| {
            err(
                StatusCode::NOT_FOUND,
                "SESSION_NOT_FOUND",
                "Session not found",
            )
        })?;

    // Resolve tenancy (Phase-1: always the default tenant). Carried in the
    // claim so the WS and Phase-4 isolation can enforce it.
    let tenant_id = db::tenancy::current_tenant_id();

    let token = state
        .jwt_config
        .create_viewer_token(&user.user_id, session_id, tenant_id, access)
        .map_err(|e| {
            tracing::error!("Failed to mint viewer token: {}", e);
            err(
                StatusCode::INTERNAL_SERVER_ERROR,
                "INTERNAL_ERROR",
                "Failed to mint viewer token",
            )
        })?;

    // Register the minted token under the authenticated user's `sub` so logout
    // can revoke it (CRITICAL #2): a viewer token minted before logout would
    // otherwise stay valid for the rest of its 5-minute TTL. The registry prunes
    // expired entries on insert, so it cannot grow unbounded. The token string
    // is held transiently in memory (already handled server-side) and never
    // logged. `user.user_id` is the `sub` claim stamped into the viewer token.
    state.viewer_tokens.register(
        &user.user_id,
        &token,
        std::time::Duration::from_secs(crate::auth::jwt::VIEWER_TOKEN_TTL_SECS as u64),
    );

    tracing::info!(
        "User {} minted a {} viewer token for session {} (agent {})",
        user.username,
        access.as_str(),
        session_id,
        session.agent_id
    );

    Ok(Json(ViewerTokenResponse {
        token,
        session_id: session_id.to_string(),
        expires_in_secs: crate::auth::jwt::VIEWER_TOKEN_TTL_SECS,
        access: access.as_str().to_string(),
    }))
}
