<?php
/**
 * [1.5.215] App Token Revocation — invalidação imediata de access tokens.
 *
 * PROBLEMA:
 *   Access tokens são JWT auto-contidos (validação sem BD). Quando o admin
 *   revoga o refresh token, o access token continua válido até `exp`
 *   (1 hora). User pode continuar a usar a app durante esse tempo.
 *
 * SOLUÇÃO:
 *   Mantemos um mapa { telemovel => unix_timestamp } numa option WP.
 *   Sempre que admin revoga, gravamos NOW(). No middleware de auth,
 *   verificamos se `JWT.iat < cutoff`. Se sim, rejeita.
 *
 *   Custo: 1x get_option por request — ~O(1) com cache nativo do WP.
 *   wp_options sempre carregadas pelo WP em arranque (object cache).
 *
 * LIMPEZA:
 *   Entradas mais antigas que ACCESS_TOKEN_TTL podem ser removidas
 *   (porque qualquer JWT com iat anterior já expirou via 'exp' natural).
 *   Job de limpeza corre 1x/dia via cron.
 */

if ( ! defined( 'ABSPATH' ) ) exit;

const RMBZ_APP_TOKEN_REVOKE_OPT = 'rmbz_app_token_revoke_map';

/**
 * Marca todos os access tokens deste telemóvel como revogados.
 * Tokens emitidos antes de NOW() ficam imediatamente inválidos.
 *
 * @param string $telemovel
 * @param string $reason   Razão. 'admin' = revogado pelo admin,
 *                          'taken_over' = login noutro device.
 */
function rmbz_app_token_mark_revoked( $telemovel, $reason = 'admin' ) {
    if ( ! $telemovel ) return;
    $map = get_option( RMBZ_APP_TOKEN_REVOKE_OPT, [] );
    if ( ! is_array( $map ) ) $map = [];
    // Storage formato: { telemovel => [ts, reason] }
    // Compatível com formato antigo (só int) — se for int, normaliza.
    $map[ (string) $telemovel ] = [ time(), $reason ];
    update_option( RMBZ_APP_TOKEN_REVOKE_OPT, $map, false );
}

/**
 * Marca múltiplos telemóveis de uma vez (mais eficiente que loop).
 *
 * @param array $telemoveis array de strings
 * @param string $reason
 */
function rmbz_app_token_mark_revoked_bulk( array $telemoveis, $reason = 'admin' ) {
    if ( empty( $telemoveis ) ) return;
    $map = get_option( RMBZ_APP_TOKEN_REVOKE_OPT, [] );
    if ( ! is_array( $map ) ) $map = [];
    $now = time();
    foreach ( $telemoveis as $t ) {
        if ( $t ) $map[ (string) $t ] = [ $now, $reason ];
    }
    update_option( RMBZ_APP_TOKEN_REVOKE_OPT, $map, false );
}

/**
 * Verifica se um JWT (identificado por sub=telemovel + iat) foi revogado.
 *
 * REGRA: JWT é revogado se foi emitido ANTES do cutoff (estritamente).
 *   iat <  cutoff → revogado
 *   iat >= cutoff → OK (emitido no mesmo segundo ou depois → válido)
 *
 * Isto é IMPORTANTE: quando issue_tokens_for_user faz mark_revoked()
 * e LOGO A SEGUIR cria um JWT novo, ambos os timestamps podem ser
 * exactamente iguais (mesmo segundo). O JWT novo TEM de ser aceite —
 * caso contrário o próprio user que acabou de fazer login é deslogado.
 *
 * @param string $telemovel
 * @param int    $iat        timestamp do JWT (campo 'iat')
 * @return bool|string false = OK, 'admin'|'taken_over' = revogado (com razão)
 */
function rmbz_app_token_is_revoked( $telemovel, $iat ) {
    if ( ! $telemovel || $iat <= 0 ) return false;
    $map = get_option( RMBZ_APP_TOKEN_REVOKE_OPT, [] );
    if ( ! is_array( $map ) || empty( $map ) ) return false;
    $entry = $map[ (string) $telemovel ] ?? null;
    if ( $entry === null ) return false;

    // Suporte formato antigo (int) e novo ([ts, reason])
    if ( is_array( $entry ) ) {
        $cutoff = (int) ( $entry[0] ?? 0 );
        $reason = (string) ( $entry[1] ?? 'admin' );
    } else {
        $cutoff = (int) $entry;
        $reason = 'admin';
    }

    if ( $cutoff <= 0 ) return false;

    // [1.5.217] FIX: usar >= em vez de >.
    //   Cenário: dispositivo B faz login → mark_revoked() grava cutoff=T
    //                                    → make_access_token() gera JWT com iat=T
    //   Sem este fix, o JWT recém-criado é rejeitado (T <= T = true).
    //   Resultado: o user que acabou de fazer login é desligado.
    //   Correcto: JWTs emitidos no mesmo segundo do cutoff são VÁLIDOS,
    //             porque foram criados a SEGUIR ao mark_revoked.
    if ( $iat >= $cutoff ) return false;

    return $reason; // string truthy
}

/**
 * Limpeza periódica — remove entradas mais antigas que ACCESS_TOKEN_TTL
 * (já não fazem efeito porque os JWTs respectivos expiraram via 'exp').
 *
 * Mantém a option pequena ao longo do tempo.
 */
function rmbz_app_token_cleanup_old() {
    $map = get_option( RMBZ_APP_TOKEN_REVOKE_OPT, [] );
    if ( ! is_array( $map ) || empty( $map ) ) return;

    $ttl    = defined( 'RMBZ_API_ACCESS_TOKEN_TTL' ) ? (int) RMBZ_API_ACCESS_TOKEN_TTL : 3600;
    $cutoff = time() - $ttl - 60;
    $changed = false;
    foreach ( $map as $tel => $entry ) {
        $ts = is_array( $entry ) ? (int) ( $entry[0] ?? 0 ) : (int) $entry;
        if ( $ts < $cutoff ) {
            unset( $map[ $tel ] );
            $changed = true;
        }
    }
    if ( $changed ) {
        update_option( RMBZ_APP_TOKEN_REVOKE_OPT, $map, false );
    }
}
add_action( 'rmbz_app_token_cleanup_daily', 'rmbz_app_token_cleanup_old' );

// Agendar cron diário (idempotente)
add_action( 'init', function () {
    if ( ! wp_next_scheduled( 'rmbz_app_token_cleanup_daily' ) ) {
        wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'rmbz_app_token_cleanup_daily' );
    }
} );
