//! JWT token handling

use anyhow::{anyhow, Result};
use chrono::{Duration, Utc};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// JWT claims
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
    /// Subject (user ID)
    pub sub: String,
    /// Username
    pub username: String,
    /// Role (admin, operator, viewer)
    pub role: String,
    /// Permissions list
    pub permissions: Vec<String>,
    /// Expiration time (unix timestamp)
    pub exp: i64,
    /// Issued at (unix timestamp)
    pub iat: i64,
}

impl Claims {
    /// Check if user has a specific permission
    pub fn has_permission(&self, permission: &str) -> bool {
        // Admins have all permissions
        if self.role == "admin" {
            return true;
        }
        self.permissions.contains(&permission.to_string())
    }

    /// Check if user is admin
    pub fn is_admin(&self) -> bool {
        self.role == "admin"
    }

    /// Get user ID as UUID
    pub fn user_id(&self) -> Result<Uuid> {
        Uuid::parse_str(&self.sub).map_err(|e| anyhow!("Invalid user ID in token: {}", e))
    }
}

/// The fixed `purpose` discriminator carried by a viewer token.
///
/// A login [`Claims`] never carries this field; a [`ViewerClaims`] always does
/// and it is checked on validation. This makes the two token types
/// non-interchangeable even before structural deserialization differences are
/// considered.
pub const VIEWER_TOKEN_PURPOSE: &str = "viewer";

/// Default lifetime of a minted viewer token, in seconds (5 minutes).
pub const VIEWER_TOKEN_TTL_SECS: i64 = 300;

/// Access mode carried inside a viewer token's signed claims.
///
/// This is the authz-strength split that fully closes audit CRITICAL #1. A
/// `view`-permission-only user (e.g. the `viewer` role) is minted a
/// [`ViewerAccess::ViewOnly`] token; an admin or a `control`-permission user
/// (admin/operator) is minted a [`ViewerAccess::Control`] token. The mode lives
/// INSIDE the signed token (it cannot be forged client-side) and is enforced at
/// the relay: for a view-only token the relay refuses to forward ANY input event
/// to the agent while still streaming video out to the viewer.
///
/// Serialized as a lowercase string (`"view_only"` / `"control"`) so the wire
/// form is stable and human-readable in logs/debugging. Mirrors the proto's
/// `SessionType.VIEW_ONLY` vs `SCREEN_CONTROL` distinction at the token layer;
/// tying the agent-side capture mode to it is a deeper Phase-2 refinement (for
/// now the relay simply refuses to forward input for a view-only token — the
/// agent still performs full capture).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ViewerAccess {
    /// Watch-only. The relay drops all viewer→agent input events; video still
    /// streams out to the viewer.
    #[serde(rename = "view_only")]
    ViewOnly,
    /// Full control. The relay forwards input (subject to the existing Task-3
    /// rate throttle).
    #[serde(rename = "control")]
    Control,
}

impl ViewerAccess {
    /// True if this token may forward input to the agent.
    pub fn can_control(&self) -> bool {
        matches!(self, ViewerAccess::Control)
    }

    /// Stable lowercase wire/log string.
    pub fn as_str(&self) -> &'static str {
        match self {
            ViewerAccess::ViewOnly => "view_only",
            ViewerAccess::Control => "control",
        }
    }
}

/// Claims for a session-scoped viewer token.
///
/// Minted by an authenticated + authorized dashboard user for ONE specific
/// session (`POST /api/sessions/:id/viewer-token`) and consumed by the viewer
/// WebSocket (Task 3), which verifies signature + expiry + blacklist + that
/// `session_id` matches the requested session before admitting the viewer.
///
/// The `purpose` field is fixed to [`VIEWER_TOKEN_PURPOSE`]; a normal login JWT
/// has no such field and cannot be deserialized into this struct, and this
/// struct cannot be deserialized into a login [`Claims`] (it lacks the
/// login-only fields). The discriminator is also re-checked explicitly on
/// validation so the two are unambiguously separate credential planes.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ViewerClaims {
    /// Subject — the dashboard user id that minted/owns this token.
    pub sub: String,
    /// The session this token authorizes viewing of. The WS layer (Task 3)
    /// rejects the token if this does not equal the requested session.
    pub session_id: String,
    /// Tenancy scope (Phase-4-ready). Resolved via `db::tenancy` at mint time.
    pub tenant_id: String,
    /// Access mode (authz-strength split, audit CRITICAL #1). Determined at mint
    /// time by the requester's permission: `control`/admin → [`ViewerAccess::Control`],
    /// `view`-only → [`ViewerAccess::ViewOnly`]. The relay reads this from the
    /// VERIFIED token and refuses to forward input for a view-only token. Being
    /// inside the signed claims, it cannot be forged or upgraded client-side.
    pub access: ViewerAccess,
    /// Fixed discriminator — always [`VIEWER_TOKEN_PURPOSE`].
    pub purpose: String,
    /// Expiration time (unix timestamp).
    pub exp: i64,
    /// Issued at (unix timestamp).
    pub iat: i64,
}

impl ViewerClaims {
    /// Parse the session id claim as a UUID.
    pub fn session_uuid(&self) -> Result<Uuid> {
        Uuid::parse_str(&self.session_id)
            .map_err(|e| anyhow!("Invalid session_id in viewer token: {}", e))
    }
}

/// JWT configuration
#[derive(Clone)]
pub struct JwtConfig {
    secret: String,
    expiry_hours: i64,
}

impl JwtConfig {
    /// Create new JWT config
    pub fn new(secret: String, expiry_hours: i64) -> Self {
        Self {
            secret,
            expiry_hours,
        }
    }

    /// Create a JWT token for a user
    pub fn create_token(
        &self,
        user_id: Uuid,
        username: &str,
        role: &str,
        permissions: Vec<String>,
    ) -> Result<String> {
        let now = Utc::now();
        let exp = now + Duration::hours(self.expiry_hours);

        let claims = Claims {
            sub: user_id.to_string(),
            username: username.to_string(),
            role: role.to_string(),
            permissions,
            exp: exp.timestamp(),
            iat: now.timestamp(),
        };

        let token = encode(
            &Header::default(),
            &claims,
            &EncodingKey::from_secret(self.secret.as_bytes()),
        )
        .map_err(|e| anyhow!("Failed to create token: {}", e))?;

        Ok(token)
    }

    /// Validate and decode a JWT token
    ///
    /// SEC-13: Explicitly enforces token expiration
    /// - Validates signature against secret
    /// - Checks exp claim (expiration time)
    /// - Checks iat claim (issued at time)
    /// - Rejects expired tokens
    pub fn validate_token(&self, token: &str) -> Result<Claims> {
        // SEC-13: Explicit validation configuration
        let mut validation = Validation::default();
        validation.validate_exp = true; // Enforce expiration check
        validation.validate_nbf = false; // Not using "not before" claim
        validation.leeway = 0; // No clock skew tolerance

        let token_data = decode::<Claims>(
            token,
            &DecodingKey::from_secret(self.secret.as_bytes()),
            &validation,
        )
        .map_err(|e| anyhow!("Invalid token: {}", e))?;

        // Additional check: Ensure token hasn't expired (redundant but explicit)
        let now = Utc::now().timestamp();
        if token_data.claims.exp < now {
            return Err(anyhow!("Token has expired"));
        }

        Ok(token_data.claims)
    }

    /// Mint a short-lived, session-scoped viewer token with an explicit access
    /// mode.
    ///
    /// Signed with the same secret as login tokens but carrying a distinct
    /// [`ViewerClaims`] shape and a fixed `purpose`. TTL defaults to
    /// [`VIEWER_TOKEN_TTL_SECS`] (5 minutes) — short by design so a leaked
    /// viewer token has a tiny window and no standing access is granted.
    ///
    /// `access` is decided by the caller from the requester's permission
    /// (`mint_viewer_token`): a `view`-only user gets [`ViewerAccess::ViewOnly`]
    /// (the relay drops their input), an admin/`control` user gets
    /// [`ViewerAccess::Control`]. The mode is stamped into the signed claims and
    /// therefore cannot be forged or upgraded by the client.
    pub fn create_viewer_token(
        &self,
        user_id: &str,
        session_id: Uuid,
        tenant_id: Uuid,
        access: ViewerAccess,
    ) -> Result<String> {
        let now = Utc::now();
        let exp = now + Duration::seconds(VIEWER_TOKEN_TTL_SECS);

        let claims = ViewerClaims {
            sub: user_id.to_string(),
            session_id: session_id.to_string(),
            tenant_id: tenant_id.to_string(),
            access,
            purpose: VIEWER_TOKEN_PURPOSE.to_string(),
            exp: exp.timestamp(),
            iat: now.timestamp(),
        };

        encode(
            &Header::default(),
            &claims,
            &EncodingKey::from_secret(self.secret.as_bytes()),
        )
        .map_err(|e| anyhow!("Failed to create viewer token: {}", e))
    }

    /// Validate a viewer token: signature + expiry + correct `purpose`.
    ///
    /// Rejects a login JWT presented as a viewer token (it lacks the viewer
    /// claim shape, and even if forced it would fail the `purpose` check) and,
    /// symmetrically, a viewer token can never satisfy [`Self::validate_token`]
    /// because it lacks the login claim fields. The WS layer (Task 3)
    /// additionally checks the blacklist and the `session_id` match.
    pub fn validate_viewer_token(&self, token: &str) -> Result<ViewerClaims> {
        let mut validation = Validation::default();
        validation.validate_exp = true;
        validation.validate_nbf = false;
        validation.leeway = 0;
        // The login claim's required fields differ from ViewerClaims, so the
        // default required-claims set ("exp") is the only safe intersection.
        validation.set_required_spec_claims(&["exp"]);

        let token_data = decode::<ViewerClaims>(
            token,
            &DecodingKey::from_secret(self.secret.as_bytes()),
            &validation,
        )
        .map_err(|e| anyhow!("Invalid viewer token: {}", e))?;

        if token_data.claims.purpose != VIEWER_TOKEN_PURPOSE {
            return Err(anyhow!("Token is not a viewer token"));
        }

        let now = Utc::now().timestamp();
        if token_data.claims.exp < now {
            return Err(anyhow!("Viewer token has expired"));
        }

        Ok(token_data.claims)
    }
}

// Removed insecure default_jwt_secret() function - JWT_SECRET must be set via environment variable

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_create_and_validate_token() {
        let config = JwtConfig::new("test-secret".to_string(), 24);
        let user_id = Uuid::new_v4();

        let token = config
            .create_token(
                user_id,
                "testuser",
                "admin",
                vec!["view".to_string(), "control".to_string()],
            )
            .unwrap();

        let claims = config.validate_token(&token).unwrap();
        assert_eq!(claims.username, "testuser");
        assert_eq!(claims.role, "admin");
        assert!(claims.has_permission("view"));
        assert!(claims.has_permission("manage_users")); // admin has all
    }

    #[test]
    fn test_invalid_token() {
        let config = JwtConfig::new("test-secret".to_string(), 24);
        assert!(config.validate_token("invalid.token.here").is_err());
    }

    #[test]
    fn test_viewer_token_carries_access_mode() {
        let config = JwtConfig::new("test-secret".to_string(), 24);
        let session_id = Uuid::new_v4();
        let tenant_id = Uuid::new_v4();

        // A control token round-trips with Control access.
        let control = config
            .create_viewer_token("user-1", session_id, tenant_id, ViewerAccess::Control)
            .unwrap();
        let claims = config.validate_viewer_token(&control).unwrap();
        assert_eq!(claims.access, ViewerAccess::Control);
        assert!(claims.access.can_control());
        assert_eq!(claims.session_id, session_id.to_string());

        // A view-only token round-trips with ViewOnly access and cannot control.
        let view_only = config
            .create_viewer_token("user-2", session_id, tenant_id, ViewerAccess::ViewOnly)
            .unwrap();
        let claims = config.validate_viewer_token(&view_only).unwrap();
        assert_eq!(claims.access, ViewerAccess::ViewOnly);
        assert!(!claims.access.can_control());
    }

    #[test]
    fn test_viewer_access_serializes_lowercase() {
        // Wire form must be the stable lowercase strings the proto mirrors.
        assert_eq!(
            serde_json::to_string(&ViewerAccess::ViewOnly).unwrap(),
            "\"view_only\""
        );
        assert_eq!(
            serde_json::to_string(&ViewerAccess::Control).unwrap(),
            "\"control\""
        );
    }

    #[test]
    fn test_login_token_rejected_as_viewer_token() {
        // A login JWT must never satisfy the viewer-token validator (no viewer
        // claim shape / purpose), so it can never carry a forged access mode.
        let config = JwtConfig::new("test-secret".to_string(), 24);
        let login = config
            .create_token(Uuid::new_v4(), "u", "admin", vec!["view".to_string()])
            .unwrap();
        assert!(config.validate_viewer_token(&login).is_err());
    }
}
