<?php
/**
 * JWT Authentication Token Generator
 *
 * Generates and verifies JWT tokens for WebRTC widget authentication.
 * Used to authenticate WebSocket connections between WordPress and middleware.
 *
 * @link       https://github.com/sitestaffr/sitestaffr-plugin
 * @since      0.11.75
 *
 * @package    SiteStaffr
 * @subpackage SiteStaffr/includes
 */

/**
 * JWT Authentication Token Generator class.
 *
 * Generates JWT tokens using WordPress secret keys for signing.
 * Tokens are used to authenticate WebSocket connections to middleware.
 *
 * @since      0.11.75
 * @package    SiteStaffr
 * @subpackage SiteStaffr/includes
 * @author     SiteStaffr
 */
class SiteStaffr_Auth_Token {

	/**
	 * Generate a JWT token for WebSocket authentication.
	 *
	 * Creates a JWT token containing site_id, user_id, expiration, and issuer.
	 * Token is signed using HS256 algorithm with WordPress auth salt.
	 *
	 * @since    0.11.75
	 * @param    string    $site_id    WordPress site ID or URL.
	 * @param    int       $user_id    WordPress user ID (0 for anonymous).
	 * @param    int       $expiry     Token expiry in seconds (default: 3600 = 1 hour).
	 * @return   string                JWT token string.
	 */
	public static function generate_token( $site_id, $user_id = 0, $expiry = 3600 ) {
		// Sanitize inputs
		$site_id = sanitize_text_field( $site_id );
		$user_id = absint( $user_id );
		$expiry  = absint( $expiry );

		// Build token header
		$header = array(
			'alg' => 'HS256',
			'typ' => 'JWT',
		);

		// Build token payload
		$now     = time();
		$payload = array(
			'iss'     => get_site_url(),           // Issuer (WordPress site URL)
			'site_id' => $site_id,                 // Site identifier
			'user_id' => $user_id,                 // WordPress user ID (0 = anonymous/guest)
			'iat'     => $now,                     // Issued at
			'exp'     => $now + $expiry,           // Expiration time
		);

		// Log token generation for debugging (only in development)
		if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			SiteStaffr_Logger::legacy( sprintf(
				'SiteStaffr Auth Token: Generated token for site=%s, user_id=%d (guest=%s), expires_in=%d seconds',
				get_site_url(),
				$user_id,
				$user_id === 0 ? 'YES' : 'NO',
				$expiry
			) );
		}

		// Encode header and payload
		$encoded_header  = self::base64_url_encode( wp_json_encode( $header ) );
		$encoded_payload = self::base64_url_encode( wp_json_encode( $payload ) );

		// Create signature base
		$signature_base = $encoded_header . '.' . $encoded_payload;

		// Generate signature using WordPress auth salt
		$signature = self::generate_signature( $signature_base );

		// Build final token
		$token = $signature_base . '.' . $signature;

		return $token;
	}

	/**
	 * Verify a JWT token.
	 *
	 * Validates token signature and checks expiration.
	 * Returns decoded payload if valid, false otherwise.
	 *
	 * @since    0.11.75
	 * @param    string    $token    JWT token to verify.
	 * @return   array|bool          Decoded payload array on success, false on failure.
	 */
	public static function verify_token( $token ) {
		// Sanitize input
		$token = sanitize_text_field( $token );

		// Split token into parts
		$parts = explode( '.', $token );

		// JWT must have exactly 3 parts (header.payload.signature)
		if ( count( $parts ) !== 3 ) {
			SiteStaffr_Logger::legacy( 'SiteStaffr Auth: Invalid token format - expected 3 parts, got ' . count( $parts ) );
			return false;
		}

		list( $encoded_header, $encoded_payload, $provided_signature ) = $parts;

		// Validate header: must be JSON with alg=HS256
		$header = json_decode( self::base64_url_decode( $encoded_header ), true );
		if ( ! is_array( $header ) || ! isset( $header['alg'] ) || 'HS256' !== $header['alg'] ) {
			SiteStaffr_Logger::legacy( 'SiteStaffr Auth: Invalid or unsupported token header' );
			return false;
		}

		// Verify signature (timing-safe)
		$signature_base      = $encoded_header . '.' . $encoded_payload;
		$calculated_signature = self::generate_signature( $signature_base );

		if ( ! hash_equals( $calculated_signature, $provided_signature ) ) {
			SiteStaffr_Logger::legacy( 'SiteStaffr Auth: Token signature verification failed' );
			return false;
		}

		// Decode payload
		$payload = json_decode( self::base64_url_decode( $encoded_payload ), true );

		if ( ! is_array( $payload ) || empty( $payload ) ) {
			SiteStaffr_Logger::legacy( 'SiteStaffr Auth: Failed to decode token payload' );
			return false;
		}

		// Require expiration claim
		if ( ! isset( $payload['exp'] ) || ! is_numeric( $payload['exp'] ) ) {
			SiteStaffr_Logger::legacy( 'SiteStaffr Auth: Token missing required exp claim' );
			return false;
		}

		if ( time() > $payload['exp'] ) {
			SiteStaffr_Logger::legacy( 'SiteStaffr Auth: Token expired' );
			return false;
		}

		// Require issuer claim and verify it matches current site
		if ( ! isset( $payload['iss'] ) || $payload['iss'] !== get_site_url() ) {
			SiteStaffr_Logger::legacy( 'SiteStaffr Auth: Token issuer missing or mismatched' );
			return false;
		}

		return $payload;
	}

	/**
	 * Generate HMAC signature for token.
	 *
	 * Uses HS256 algorithm (HMAC with SHA-256) and site-specific secret.
	 *
	 * @since    0.11.75
	 * @param    string    $data    Data to sign.
	 * @return   string             Base64-URL-encoded signature.
	 */
	private static function generate_signature( $data ) {
		// Get site-specific secret key
		$secret = self::get_site_secret();

		// Generate HMAC signature
		$signature = hash_hmac( 'sha256', $data, $secret, true );

		// Return base64-URL-encoded signature
		return self::base64_url_encode( $signature );
	}

	/**
	 * Get or create site-specific secret for JWT signing.
	 *
	 * This secret is unique per WordPress installation and used to sign JWT tokens.
	 * The middleware stores a hash of this secret for verification.
	 *
	 * @since    0.11.77
	 * @return   string    Site-specific secret (64 hex characters = 32 bytes).
	 */
	private static function get_site_secret() {
		$secret = get_option( 'sitestaffr_site_secret' );
		if ( ! $secret ) {
			// Generate new cryptographically secure secret if none exists
			$secret = bin2hex( random_bytes( 32 ) );
			update_option( 'sitestaffr_site_secret', $secret, false ); // Don't autoload
			SiteStaffr_Logger::legacy( 'SiteStaffr Auth: Generated new site secret' );
		}
		return $secret;
	}

	/**
	 * Base64-URL encode data.
	 *
	 * Standard base64 encoding with URL-safe character substitutions.
	 *
	 * @since    0.11.75
	 * @param    string    $data    Data to encode.
	 * @return   string             Base64-URL-encoded string.
	 */
	private static function base64_url_encode( $data ) {
		// Standard base64 encode
		$encoded = base64_encode( $data );

		// Make URL-safe
		$encoded = strtr( $encoded, '+/', '-_' );

		// Remove padding
		$encoded = rtrim( $encoded, '=' );

		return $encoded;
	}

	/**
	 * Base64-URL decode data.
	 *
	 * Reverse of base64_url_encode().
	 *
	 * @since    0.11.75
	 * @param    string    $data    Base64-URL-encoded string.
	 * @return   string             Decoded data.
	 */
	private static function base64_url_decode( $data ) {
		// Restore URL-unsafe characters
		$data = strtr( $data, '-_', '+/' );

		// Restore padding
		$remainder = strlen( $data ) % 4;
		if ( $remainder ) {
			$data .= str_repeat( '=', 4 - $remainder );
		}

		// Standard base64 decode (strict mode rejects invalid characters)
		return base64_decode( $data, true );
	}
}


