<?php

/**
 * Learner identity resolution for REST endpoints.
 *
 * Identity is derived from trusted sources only — never from a raw
 * client-supplied learnerId/sessionId. This closes the IDOR where a caller
 * with a valid wp_rest nonce could read/write another learner's data by
 * sending a different learnerId/sessionId in the request body.
 *
 *   - Logged-in users: identity = get_current_user_id() (body learnerId ignored).
 *   - Guests: identity = the HMAC-signed sessionId issued by /bootstrap,
 *     verified against the request's sessionToken.
 *   - Premium (external LMS bridge): gated behind the premium-only flag and
 *     stripped from the free zip. Reserved for the Phase 3 HMAC bridge.
 *
 * @package SlideMind\Core
 * @since   0.4.0
 */
namespace SlideMind\Core;

if ( !defined( 'ABSPATH' ) ) {
    exit;
}
class LearnerIdentity {
    /**
     * HMAC a session ID (optionally scoped to a configuration) with a
     * site-stable key so the server can prove it issued the session for
     * that configuration. Uses wp_salt('auth') — rotating salts invalidates
     * outstanding session tokens, which is acceptable (clients re-bootstrap).
     *
     * @param string $session_id UUID v4 session identifier.
     * @param string $config_uid Configuration UID the session is scoped to.
     * @return string Hex HMAC-SHA256 signature.
     */
    public static function sign_session_id( string $session_id, string $config_uid = '' ) : string {
        $payload = ( $config_uid === '' ? $session_id : $session_id . ':' . $config_uid );
        return hash_hmac( 'sha256', $payload, wp_salt( 'auth' ) );
    }

    /**
     * Constant-time verification of a session token against a session ID
     * and the configuration it was issued for. A token issued for one
     * configuration does not validate for another.
     *
     * @param string $session_id UUID v4 session identifier.
     * @param string $token      Candidate signature from the client.
     * @param string $config_uid Configuration UID from the current request.
     * @return bool
     */
    public static function verify_session_token( string $session_id, string $token, string $config_uid = '' ) : bool {
        if ( $session_id === '' || $token === '' ) {
            return false;
        }
        if ( !LearnerMemory::is_valid_session_id( $session_id ) ) {
            return false;
        }
        return hash_equals( self::sign_session_id( $session_id, $config_uid ), $token );
    }

    /**
     * Resolve the caller's identity from trusted sources.
     *
     * @param \WP_REST_Request $request
     * @param bool             $require When true, a missing/invalid identity
     *                                  returns WP_Error. When false (chat),
     *                                  returns an anonymous-no-memory identity.
     * @return array|\WP_Error ['user_id' => int, 'session_id' => string] or error.
     */
    public static function resolve( $request, bool $require = true ) {
        // 1. Logged-in user — cookie-authenticated. Body learnerId is ignored.
        $current_uid = get_current_user_id();
        if ( $current_uid > 0 ) {
            return [
                'user_id'    => $current_uid,
                'session_id' => '',
            ];
        }
        // 3. Guest — verify the HMAC-signed session issued by /bootstrap
        //    for this same configuration.
        $session_id = (string) $request->get_param( 'sessionId' );
        $session_token = (string) $request->get_param( 'sessionToken' );
        $config_uid = (string) $request->get_param( 'configId' );
        if ( self::verify_session_token( $session_id, $session_token, $config_uid ) ) {
            return [
                'user_id'    => 0,
                'session_id' => $session_id,
            ];
        }
        if ( $require ) {
            return new \WP_Error('identity_required', __( 'A valid identity is required. Call /bootstrap first.', 'slidemind' ), [
                'status' => 401,
            ]);
        }
        // Soft mode (chat): anonymous with no memory binding.
        return [
            'user_id'    => 0,
            'session_id' => '',
        ];
    }

}
