<?php
/**
 * 1MZ For Artists — Subscription Controller
 *
 * Endpoints:
 *   GET  /1mz/v1/subscription/status         — Estado actual da subscrição + first-deposit gate
 *   GET  /1mz/v1/subscription/plans          — Lista de pacotes VIP disponíveis
 *   POST /1mz/v1/subscription/buy            — Comprar/renovar plano com saldo da carteira
 *   GET  /1mz/v1/subscription/history        — Histórico de pagamentos de subscrição
 *   GET  /1mz/v1/payments/destinations       — Números M-Pesa/E-Mola para depósito
 *   POST /1mz/v1/wallet/deposits             — Criar pedido de depósito (M-Pesa/E-Mola)
 *
 * REGRAS DE NEGÓCIO (ver subscription-functions.php, first-deposit-gate.php,
 * admin-handlers.php → rmbz_process_buy_vip_package, form-handler.php
 * → rmbz_processar_deposito):
 *   - Pacote VIP tem campos { id, nome, preco, dias, perms: { site, social,
 *     distribute, video } }
 *   - Compra com saldo: se já tem subscrição activa, ESTENDE em +dias;
 *     senão, cria nova
 *   - Compra força permissões totais do plano em wallet.extra_data
 *   - Depósito directo: amount ≤ 50000 MT, só 1 pendente por user, requer
 *     transaction_id OU prova de imagem (URL)
 *   - First-deposit gate é visível enquanto: conta existe + tem depósito
 *     pendente + nunca teve aprovado + nunca teve subscrição
 *
 * @package 1MZ_For_Artists
 * @since   1.6.15
 */

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

/* ============================================================================
 * MARCADOR DE VERSÃO
 *
 * Truque defensivo contra OPcache (regra 12.11 do handoff). Se a app pedir
 * /subscription/status e este número não vier no payload, o servidor está
 * a servir versão antiga em cache — basta reiniciar PHP no cPanel.
 * ============================================================================ */
if ( ! defined( 'RMBZ_API_SUBSCRIPTION_VERSION' ) ) {
    define( 'RMBZ_API_SUBSCRIPTION_VERSION', '1.6.25.3' );
}


add_action( 'rest_api_init', 'rmbz_api_register_subscription_routes' );

/**
 * [1.6.118.126] As subscrições são guardadas em hora LOCAL do site (Maputo),
 * via current_time(). Mas mysql_to_rfc3339() assume UTC e marca "Z", o que faz
 * o app/site somar o offset outra vez (bug das +2h na expiração do VIP).
 *
 * Este helper recebe uma data MySQL em hora local e devolve o ISO RFC3339 com
 * o instante UTC correcto (recuando o offset do site), para o app converter
 * certo. Se a data for vazia, devolve null.
 */
function rmbz_sub_local_mysql_to_utc_rfc3339( $mysql_local ) {
    if ( empty( $mysql_local ) ) { return null; }
    // Offset do site, derivado do WordPress (local − UTC).
    $local = (int) current_time( 'timestamp' );
    $utc   = (int) current_time( 'timestamp', true );
    $offset = $local - $utc;
    if ( $offset < -12 * HOUR_IN_SECONDS || $offset > 14 * HOUR_IN_SECONDS ) {
        $gmt = get_option( 'gmt_offset', '' );
        $offset = ( $gmt !== '' && $gmt !== null ) ? (int) round( (float) $gmt * HOUR_IN_SECONDS ) : 0;
    }
    // A data está em parede-relógio local. Interpretá-la como UTC dá o
    // timestamp "local"; recuar o offset dá o instante UTC verdadeiro.
    $wall = strtotime( $mysql_local . ' UTC' );
    if ( $wall === false ) { return null; }
    return gmdate( 'Y-m-d\TH:i:s\Z', $wall - $offset );
}

function rmbz_api_register_subscription_routes() {
    $ns = RMBZ_API_NAMESPACE;

    register_rest_route( $ns, '/subscription/status', [
        'methods'             => 'GET',
        'callback'            => 'rmbz_api_handle_subscription_status',
        'permission_callback' => 'rmbz_api_require_auth',
    ] );

    register_rest_route( $ns, '/subscription/plans', [
        'methods'             => 'GET',
        'callback'            => 'rmbz_api_handle_subscription_plans',
        'permission_callback' => 'rmbz_api_require_auth',
    ] );

    register_rest_route( $ns, '/subscription/buy', [
        'methods'             => 'POST',
        'callback'            => 'rmbz_api_handle_subscription_buy',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'package_id' => [ 'required' => true, 'type' => 'string' ],
            'pin'        => [ 'required' => true, 'type' => 'string' ],
            // [1.5.291] Add-on do Assistente de IA (default: não incluir).
            'include_ai' => [ 'type' => 'boolean', 'default' => false ],
            // [1.5.372] Duração em meses (1/2/3/4/12). Default 1.
            'months'     => [ 'type' => 'integer', 'default' => 1 ],
        ],
    ] );

    register_rest_route( $ns, '/subscription/history', [
        'methods'             => 'GET',
        'callback'            => 'rmbz_api_handle_subscription_history',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'page'     => [ 'type' => 'integer', 'default' => 1, 'minimum' => 1 ],
            'per_page' => [ 'type' => 'integer', 'default' => 20, 'minimum' => 1, 'maximum' => 100 ],
        ],
    ] );

    register_rest_route( $ns, '/payments/destinations', [
        'methods'             => 'GET',
        'callback'            => 'rmbz_api_handle_payment_destinations',
        'permission_callback' => 'rmbz_api_require_auth',
    ] );

    register_rest_route( $ns, '/wallet/deposits', [
        'methods'             => 'POST',
        'callback'            => 'rmbz_api_handle_create_deposit',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'amount'         => [ 'required' => true, 'type' => 'number' ],
            'payment_method' => [ 'required' => true, 'type' => 'string' ],
            'sender_phone'   => [ 'required' => true, 'type' => 'string' ],
            'transaction_id' => [ 'type' => 'string' ],
            'proof_image'    => [ 'type' => 'string' ],
            'proof_image_url'=> [ 'type' => 'string' ],
        ],
    ] );
}


/* ============================================================================
 * HELPERS — formatação
 * ============================================================================ */

/**
 * Formata um pacote VIP para a API. Sanitiza o array vindo de wp_options.
 *
 * @param array $p Pacote raw em rmbz_vip_packages.
 * @return array|null Null se inválido.
 */
function rmbz_api_format_vip_package( $p ) {
    if ( ! is_array( $p ) ) return null;
    if ( empty( $p['id'] ) || empty( $p['nome'] ) || ! isset( $p['preco'] ) || ! isset( $p['dias'] ) ) {
        return null;
    }

    // Permissões com defaults seguros — qualquer uma em falta = 0 (sem acesso)
    $perms_raw = isset( $p['perms'] ) && is_array( $p['perms'] ) ? $p['perms'] : [];
    $perms = [
        'distribute' => ! empty( $perms_raw['distribute'] ) ? 1 : 0,
        'site'       => ! empty( $perms_raw['site'] )       ? 1 : 0,
        'social'     => ! empty( $perms_raw['social'] )     ? 1 : 0,
        'video'      => ! empty( $perms_raw['video'] )      ? 1 : 0,
    ];

    // [1.6.118.20] Channel + featured (com defaults retro-compat)
    $channel = isset( $p['channel'] ) ? (string) $p['channel'] : 'both';
    if ( ! in_array( $channel, [ 'both', 'app_only', 'site_only' ], true ) ) {
        $channel = 'both';
    }

    // [1.5.291] Add-on do Assistente de IA — opcional no checkout.
    $ai_offered = ! empty( $p['ai_offered'] ) ? 1 : 0;
    $ai_price   = isset( $p['ai_price'] ) ? max( 0, (float) $p['ai_price'] ) : 0.0;

    // [1.5.372] Opções de duração (1/2/3/4/12 meses) com preços já calculados.
    $durations = function_exists( 'rmbz_vip_package_duration_options' )
        ? rmbz_vip_package_duration_options( $p ) : [];

    return [
        'id'         => (string) $p['id'],
        'nome'       => (string) $p['nome'],
        'preco'      => (float) $p['preco'],
        'dias'       => (int) $p['dias'],
        'perms'      => (object) $perms,    // CAST OBJECT — evita json_encode([]) (regra 12.10)
        'featured'   => ! empty( $p['featured'] ) ? 1 : 0,
        'channel'    => $channel,
        'ai_offered' => $ai_offered,
        'ai_price'   => $ai_price,
        // [1.5.372] Durações disponíveis (sempre ≥1 item — o de 1 mês).
        'durations'  => $durations,
    ];
}

/**
 * Devolve TODOS os pacotes VIP configurados, ordenados por preço crescente
 * (mais barato primeiro — é o que o site faz no paywall).
 *
 * [1.6.118.20] Aceita $channel para filtrar:
 *   - 'app'  → devolve só pacotes com channel='both' OU 'app_only'
 *   - 'site' → devolve só pacotes com channel='both' OU 'site_only'
 *   - null   → devolve todos (uso interno admin)
 *
 * @param string|null $channel
 * @return array
 */
function rmbz_api_get_vip_packages( $channel = null ) {
    $raw = get_option( 'rmbz_vip_packages', [] );
    if ( ! is_array( $raw ) ) return [];

    $out = [];
    foreach ( $raw as $p ) {
        $f = rmbz_api_format_vip_package( $p );
        if ( $f === null ) continue;

        // Filtrar por canal
        if ( $channel === 'app' && $f['channel'] === 'site_only' ) continue;
        if ( $channel === 'site' && $f['channel'] === 'app_only' ) continue;

        $out[] = $f;
    }

    usort( $out, function( $a, $b ) {
        // [1.6.118.20] Destacados primeiro, depois por preço crescente
        if ( $a['featured'] !== $b['featured'] ) {
            return $b['featured'] <=> $a['featured'];
        }
        return $a['preco'] <=> $b['preco'];
    } );

    return $out;
}

/**
 * Lê as permissões VIP actuais do utilizador (vindas de wallet.extra_data).
 *
 * @param string $telemovel
 * @return array { distribute, site, social, video }
 */
function rmbz_api_get_user_vip_permissions( $telemovel ) {
    global $wpdb;
    $row = $wpdb->get_row( $wpdb->prepare(
        "SELECT extra_data FROM {$wpdb->prefix}rmbz_wallets WHERE user_telemovel = %s",
        $telemovel
    ) );

    $extra = ! empty( $row->extra_data ) ? json_decode( $row->extra_data, true ) : [];
    if ( ! is_array( $extra ) ) $extra = [];

    $perms = isset( $extra['vip_permissions'] ) && is_array( $extra['vip_permissions'] )
        ? $extra['vip_permissions']
        : [];

    return [
        'distribute' => ! empty( $perms['distribute'] ) ? 1 : 0,
        'site'       => ! empty( $perms['site'] )       ? 1 : 0,
        'social'     => ! empty( $perms['social'] )     ? 1 : 0,
        'video'      => ! empty( $perms['video'] )      ? 1 : 0,
        // [1.5.291] Add-on do Assistente de IA (escolhido na compra).
        'ai'         => ! empty( $perms['ai'] )         ? 1 : 0,
    ];
}

/**
 * Lê o nome do plano activo (guardado em wallet.extra_data).
 *
 * @param string $telemovel
 * @return string|null
 */
function rmbz_api_get_active_plan_name( $telemovel ) {
    global $wpdb;
    $row = $wpdb->get_row( $wpdb->prepare(
        "SELECT extra_data FROM {$wpdb->prefix}rmbz_wallets WHERE user_telemovel = %s",
        $telemovel
    ) );

    if ( ! $row || empty( $row->extra_data ) ) return null;
    $extra = json_decode( $row->extra_data, true );
    if ( ! is_array( $extra ) ) return null;

    return ! empty( $extra['active_plan_name'] ) ? (string) $extra['active_plan_name'] : null;
}


/* ============================================================================
 * GET /subscription/status
 * ============================================================================ */
function rmbz_api_handle_subscription_status( WP_REST_Request $request ) {
    $tel = $request->get_param( 'rmbz_telemovel' );

    // First-deposit gate (estado prioritário — bloqueia tudo)
    $fdg = function_exists( 'rmbz_user_has_pending_first_deposit' )
        ? rmbz_user_has_pending_first_deposit( $tel )
        : false;

    $first_deposit_pending = null;
    if ( is_array( $fdg ) ) {
        $first_deposit_pending = [
            'deposit_id'        => (int) $fdg['deposit_id'],
            'amount'            => (float) $fdg['amount'],
            'method'            => (string) $fdg['method'],
            'transaction_id'    => isset( $fdg['transaction_id'] ) ? (string) $fdg['transaction_id'] : '',
            'sender_phone'      => isset( $fdg['sender_phone'] ) ? (string) $fdg['sender_phone'] : '',
            'request_date'      => mysql_to_rfc3339( $fdg['request_date'] ),
            'expires_at'        => mysql_to_rfc3339( $fdg['expires_at'] ),
            'seconds_remaining' => (int) $fdg['seconds_remaining'],
        ];
    }

    // Subscrição activa
    $is_active = function_exists( 'rmbz_user_has_active_subscription' )
        ? (bool) rmbz_user_has_active_subscription( $tel )
        : false;

    $end_date_raw = function_exists( 'rmbz_get_subscription_expiry' )
        ? rmbz_get_subscription_expiry( $tel )
        : null;

    $end_date_iso = $end_date_raw ? rmbz_sub_local_mysql_to_utc_rfc3339( $end_date_raw ) : null;

    // Dias restantes (negativo se já expirou)
    $days_remaining = null;
    if ( $end_date_raw ) {
        $end_ts = strtotime( $end_date_raw );
        $now_ts = current_time( 'timestamp' );
        $days_remaining = (int) floor( ( $end_ts - $now_ts ) / 86400 );
    }

    // Saldo actual (para a app saber se chega para um plano)
    $balance = function_exists( 'rmbz_get_wallet_balance_by_phone' )
        ? (float) rmbz_get_wallet_balance_by_phone( $tel )
        : 0.0;

    return rmbz_api_ok( [
        '_api_version'           => RMBZ_API_SUBSCRIPTION_VERSION,
        'is_active'              => $is_active,
        'plan_name'              => rmbz_api_get_active_plan_name( $tel ),
        'end_date'               => $end_date_iso,
        'days_remaining'         => $days_remaining,
        'permissions'            => (object) rmbz_api_get_user_vip_permissions( $tel ),
        'wallet_balance'         => $balance,
        'currency'               => 'MZN',
        'first_deposit_pending'  => $first_deposit_pending,  // null OU objecto
    ] );
}


/* ============================================================================
 * GET /subscription/plans
 * ============================================================================ */
function rmbz_api_handle_subscription_plans( WP_REST_Request $request ) {
    return rmbz_api_ok( [
        '_api_version' => RMBZ_API_SUBSCRIPTION_VERSION,
        // [1.6.118.20] Filtrar pacotes para a app (esconde 'site_only')
        'plans'        => rmbz_api_get_vip_packages( 'app' ),
        'currency'     => 'MZN',
    ] );
}


/* ============================================================================
 * POST /subscription/buy
 *
 * Compra/renova plano usando saldo da carteira. Replica EXACTAMENTE a lógica
 * de rmbz_process_buy_vip_package (admin-handlers.php) mas com auth via JWT.
 * ============================================================================ */
function rmbz_api_handle_subscription_buy( WP_REST_Request $request ) {
    // [1.6.25.2] Cast (string) + fallback de auth defensivo. Em algumas
    // combinações de WP REST API + JSON body POST, o param setado no
    // permission_callback pode aparecer vazio aqui. Re-autenticar é
    // barato e elimina classe inteira de erros "Carteira não encontrada"
    // que na verdade era "$tel veio vazio".
    $tel = (string) $request->get_param( 'rmbz_telemovel' );
    if ( $tel === '' ) {
        if ( function_exists( 'rmbz_api_authenticate_request' ) ) {
            $auth = rmbz_api_authenticate_request();
            if ( is_wp_error( $auth ) ) {
                return rmbz_api_error(
                    $auth->get_error_message(),
                    401,
                    $auth->get_error_code() ?: 'auth_required'
                );
            }
            $tel = (string) $auth;
        }
    }

    if ( $tel === '' ) {
        return rmbz_api_error(
            'Não conseguimos identificar a tua conta. Inicia sessão de novo.',
            401,
            'auth_required'
        );
    }

    $package_id = sanitize_text_field( (string) $request->get_param( 'package_id' ) );
    $pin        = (string) $request->get_param( 'pin' );

    // 1. Validar PIN (mesma estratégia das withdrawals)
    if ( strlen( $pin ) < 4 ) {
        return rmbz_api_error( 'PIN inválido.', 400, 'invalid_pin' );
    }

    global $wpdb;
    // [1.6.25.3] BUGFIX: a coluna chama-se 'user_pin', não 'pin' (ver
    // schema em functions-setup.php). Antes a query falhava silenciosamente
    // e o handler devolvia "Carteira não encontrada" mesmo com a carteira
    // a existir.
    $wallet_row = $wpdb->get_row( $wpdb->prepare(
        "SELECT user_pin, balance, extra_data FROM {$wpdb->prefix}rmbz_wallets WHERE user_telemovel = %s",
        $tel
    ) );
    if ( ! $wallet_row ) {
        return rmbz_api_error(
            'Carteira não encontrada para este telemóvel: ' . $tel,
            404,
            'wallet_missing'
        );
    }

    // [1.6.118.12] Usar `rmbz_verify_user_pin` em vez de wp_check_password
    // directo — tem auto-recovery de PINs em texto plano (bug do admin
    // reset_pin histórico).
    if ( ! rmbz_verify_user_pin( $tel, $pin ) ) {
        return rmbz_api_error( 'PIN incorrecto.', 401, 'wrong_pin' );
    }

    // 2. Encontrar o pacote
    $packages = get_option( 'rmbz_vip_packages', [] );
    $selected = null;
    if ( is_array( $packages ) ) {
        foreach ( $packages as $p ) {
            if ( is_array( $p ) && isset( $p['id'] ) && $p['id'] === $package_id ) {
                $selected = $p;
                break;
            }
        }
    }
    if ( ! $selected ) {
        return rmbz_api_error( 'Plano inválido ou inexistente.', 404, 'plan_not_found' );
    }

    $nome  = (string) $selected['nome'];
    $perms = isset( $selected['perms'] ) && is_array( $selected['perms'] ) ? $selected['perms'] : [];

    // [1.5.291] Add-on do Assistente de IA. Só conta se o pacote o oferece.
    $ai_offered  = ! empty( $selected['ai_offered'] );
    $include_ai  = $ai_offered && (bool) $request->get_param( 'include_ai' );

    // [1.5.372] Duração em meses (1/2/3/4/12). Preço = base×meses + IA×meses,
    // com desconto por duração. Dias = dias_base×meses.
    $months  = (int) $request->get_param( 'months' );
    if ( $months <= 0 ) $months = 1;
    $price   = function_exists( 'rmbz_vip_compute_price' )
        ? rmbz_vip_compute_price( $selected, $months, $include_ai ) : null;
    if ( $price === null ) {
        return rmbz_api_error( 'Duração inválida para este plano.', 400, 'invalid_duration' );
    }
    $preco   = (float) $price['base_total'];   // total do plano (sem IA) para registo
    $dias    = (int) $price['days'];
    $total   = (float) $price['total'];         // plano + IA, já com desconto

    // [1.6.25] ANTI-DOWNGRADE — bloquear compra de plano inferior enquanto
    // subscrição actual ainda está activa.
    //
    // Regra: se a subscrição corrente tem permissões P, o novo plano tem
    // de cobrir TODAS essas P (pode ter mais, nunca menos).
    //
    // Quando a subscrição expira (end_date < now), o user pode comprar
    // qualquer plano — porque já não tem nada activo.
    $current_sub = $wpdb->get_row( $wpdb->prepare(
        "SELECT id, end_date FROM {$wpdb->prefix}rmbz_subscriptions
         WHERE user_telemovel = %s AND status = 'active'
         ORDER BY end_date DESC LIMIT 1",
        $tel
    ) );

    if ( $current_sub && strtotime( $current_sub->end_date ) > time() ) {
        // Tem subscrição activa — comparar permissões
        $current_extra = ! empty( $wallet_row->extra_data )
            ? json_decode( $wallet_row->extra_data, true )
            : [];
        if ( ! is_array( $current_extra ) ) $current_extra = [];

        $current_perms = isset( $current_extra['vip_permissions'] ) && is_array( $current_extra['vip_permissions'] )
            ? $current_extra['vip_permissions']
            : [];
        $current_plan_name = isset( $current_extra['active_plan_name'] )
            ? (string) $current_extra['active_plan_name']
            : 'Plano actual';

        // Identificar permissões em falta no novo plano
        $perm_labels = [
            'distribute' => 'Distribuição musical',
            'site'       => 'Publicação no site',
            'social'     => 'Redes sociais',
            'video'      => 'Videoclipes',
        ];

        $missing = [];
        foreach ( [ 'distribute', 'site', 'social', 'video' ] as $key ) {
            $has_now = ! empty( $current_perms[ $key ] );
            $has_new = ! empty( $perms[ $key ] );
            if ( $has_now && ! $has_new ) {
                $missing[] = $perm_labels[ $key ] ?? $key;
            }
        }

        if ( ! empty( $missing ) ) {
            // É o mesmo plano (renovação) ou um upgrade lateral OK?
            // Não — se chegámos aqui é porque há perms que ficariam de
            // fora. Bloquear.
            $days_left = max( 0, (int) ceil(
                ( strtotime( $current_sub->end_date ) - time() ) / 86400
            ) );

            return rmbz_api_error(
                sprintf(
                    'Não podes mudar para "%s" enquanto tens "%s" activo. '
                    . 'Perderias acesso a: %s. '
                    . 'Espera pelo fim da subscrição actual (faltam %d dias) '
                    . 'ou escolhe um plano que mantenha as funcionalidades actuais.',
                    $nome,
                    $current_plan_name,
                    implode( ', ', $missing ),
                    $days_left
                ),
                409,
                'plan_downgrade_blocked',
                [
                    'current_plan_name'  => $current_plan_name,
                    'current_perms'      => $current_perms,
                    'new_plan_name'      => $nome,
                    'new_perms'          => $perms,
                    'missing_perms'      => array_values( array_filter(
                        [ 'distribute', 'site', 'social', 'video' ],
                        fn( $k ) => ! empty( $current_perms[ $k ] ) && empty( $perms[ $k ] )
                    ) ),
                    'missing_perms_labels' => $missing,
                    'current_end_date'   => $current_sub->end_date,
                    'days_left'          => $days_left,
                ]
            );
        }
    }

    // 3. Saldo suficiente? (preço do plano + add-on de IA, se incluído)
    if ( (float) $wallet_row->balance < $total ) {
        return rmbz_api_error(
            sprintf(
                'Saldo insuficiente. Custa %s MT, tem %s MT. Faça um depósito antes.',
                number_format( $total, 2, ',', '.' ),
                number_format( (float) $wallet_row->balance, 2, ',', '.' )
            ),
            409,
            'insufficient_balance'
        );
    }

    // 4. Debitar saldo (operação atómica — usa função existente)
    if ( ! function_exists( 'rmbz_update_wallet_balance' ) ) {
        return rmbz_api_error( 'Sistema indisponível: rmbz_update_wallet_balance ausente.', 500, 'wallet_fn_missing' );
    }

    $details = $include_ai
        ? "Compra do Plano: {$nome} ({$dias} dias) + Assistente IA [via app]"
        : "Compra do Plano: {$nome} ({$dias} dias) [via app]";
    $debit = rmbz_update_wallet_balance( $tel, -$total, 'subscription_payment', null, $details );

    if ( empty( $debit['success'] ) ) {
        $err = isset( $debit['error'] ) ? (string) $debit['error'] : 'Falha desconhecida.';
        return rmbz_api_error( 'Erro ao debitar saldo: ' . $err, 500, 'debit_failed' );
    }

    // 5. Aplicar permissões + nome do plano em extra_data
    $extra = ! empty( $wallet_row->extra_data ) ? json_decode( $wallet_row->extra_data, true ) : [];
    if ( ! is_array( $extra ) ) $extra = [];

    $extra['vip_permissions'] = [
        'distribute' => ! empty( $perms['distribute'] ) ? 1 : 0,
        'site'       => ! empty( $perms['site'] )       ? 1 : 0,
        'social'     => ! empty( $perms['social'] )     ? 1 : 0,
        'video'      => ! empty( $perms['video'] )      ? 1 : 0,
        // [1.5.291] Add-on de IA escolhido nesta compra.
        'ai'         => $include_ai ? 1 : 0,
    ];
    $extra['active_plan_name'] = $nome;

    $wpdb->update(
        $wpdb->prefix . 'rmbz_wallets',
        [ 'extra_data' => wp_json_encode( $extra ) ],
        [ 'user_telemovel' => $tel ]
    );

    // [1.6.380] Congelar a multa por atraso ANTES de estender a subscrição —
    // assim a dívida não desaparece quando o VIP ficar ativo.
    if ( function_exists( 'rmbz_project_freeze_pending_fine' ) ) {
        rmbz_project_freeze_pending_fine( $tel );
    }

    // 6. Estender ou criar subscrição
    $table_subs = $wpdb->prefix . 'rmbz_subscriptions';
    $sub = $wpdb->get_row( $wpdb->prepare(
        "SELECT id, end_date FROM {$table_subs} WHERE user_telemovel = %s AND status='active'",
        $tel
    ) );

    /* ------------------------------------------------------------------
     * [1.6.118.60] Detectar tipo de evento de compra (ANTES de mexer na BD):
     *   - 'first_vip'      → 0 subscrições anteriores (estreia)
     *   - 'renewed_active' → tem subscrição activa não-expirada (apenas estende)
     *   - 'returned'       → tem ≥1 subscrição passada MAS está expirada agora
     *
     * Usado pelo app/site para mostrar modal celebrátorio adequado.
     * ------------------------------------------------------------------ */
    $total_subs = (int) $wpdb->get_var( $wpdb->prepare(
        "SELECT COUNT(*) FROM {$table_subs} WHERE user_telemovel = %s",
        $tel
    ) );
    $has_active_now = $sub && strtotime( $sub->end_date ) > time();

    if ( $total_subs === 0 ) {
        $purchase_event = 'first_vip';
    } elseif ( $has_active_now ) {
        $purchase_event = 'renewed_active';
    } else {
        $purchase_event = 'returned';
    }

    if ( $has_active_now ) {
        $new_end_date = date( 'Y-m-d H:i:s', strtotime( $sub->end_date . " + {$dias} days" ) );
        $wpdb->update(
            $table_subs,
            [ 'end_date' => $new_end_date, 'last_payment_amount' => $total ],
            [ 'id' => $sub->id ]
        );
    } else {
        $new_end_date = date( 'Y-m-d H:i:s', strtotime( "+ {$dias} days" ) );
        $wpdb->insert( $table_subs, [
            'user_telemovel'      => $tel,
            'status'              => 'active',
            'start_date'          => current_time( 'mysql' ),
            'end_date'            => $new_end_date,
            'last_payment_amount' => $total,
        ] );
    }

    // 7. Disparar hooks compatíveis com o site
    $package_id_safe = isset( $selected['id'] ) ? (string) $selected['id'] : 'unknown';
    // [1.6.118.60] Passar purchase_event ao hook para site reagir (ver intro-welcome.php)
    do_action( 'rmbz_vip_plan_purchased', $tel, $package_id_safe, $preco, $purchase_event );

    if ( function_exists( 'rmbz_notify' ) ) {
        rmbz_notify( $tel, 'vip_purchased', [
            'plano'          => $nome,
            'dias'           => (int) $dias,
            'data_expiracao' => date_i18n( 'd/m/Y', strtotime( $new_end_date ) ),
        ] );
    }

    /* ------------------------------------------------------------------
     * [1.6.118.60] Devolver estado actualizado COM purchase_event
     * incluído. Não podemos delegar tudo ao handler de status — temos
     * de adicionar o campo de evento à resposta.
     *
     * NOTA: rmbz_api_ok envolve o payload em {success, data: {...}} —
     * temos de aceder a $envelope['data'] e re-empacotar.
     * ------------------------------------------------------------------ */
    $status_response = rmbz_api_handle_subscription_status( $request );

    if ( $status_response instanceof WP_REST_Response ) {
        $envelope = $status_response->get_data();
        if ( is_array( $envelope ) && isset( $envelope['data'] ) && is_array( $envelope['data'] ) ) {
            $envelope['data']['purchase_event']   = $purchase_event;
            $envelope['data']['plan_just_bought'] = $nome;
            $envelope['data']['days_bought']      = (int) $dias;
            $status_response->set_data( $envelope );
        }
    }
    return $status_response;
}


/* ============================================================================
 * GET /subscription/history
 * ============================================================================ */
function rmbz_api_handle_subscription_history( WP_REST_Request $request ) {
    $tel      = $request->get_param( 'rmbz_telemovel' );
    $page     = max( 1, (int) $request->get_param( 'page' ) );
    $per_page = max( 1, min( 100, (int) $request->get_param( 'per_page' ) ) );
    $offset   = ( $page - 1 ) * $per_page;

    global $wpdb;
    $table = $wpdb->prefix . 'rmbz_subscriptions';

    $total = (int) $wpdb->get_var( $wpdb->prepare(
        "SELECT COUNT(*) FROM {$table} WHERE user_telemovel = %s",
        $tel
    ) );

    $rows = $wpdb->get_results( $wpdb->prepare(
        "SELECT id, start_date, end_date, last_payment_amount, status
         FROM {$table}
         WHERE user_telemovel = %s
         ORDER BY id DESC
         LIMIT %d OFFSET %d",
        $tel, $per_page, $offset
    ) );

    $items = [];
    foreach ( (array) $rows as $r ) {
        $items[] = [
            'id'              => (int) $r->id,
            'start_date'      => $r->start_date ? rmbz_sub_local_mysql_to_utc_rfc3339( $r->start_date ) : null,
            'end_date'        => $r->end_date   ? rmbz_sub_local_mysql_to_utc_rfc3339( $r->end_date )   : null,
            'amount'          => (float) $r->last_payment_amount,
            'currency'        => 'MZN',
            'status'          => (string) $r->status,
        ];
    }

    return rmbz_api_ok( [
        'items'    => $items,
        'page'     => $page,
        'per_page' => $per_page,
        'total'    => $total,
        'has_more' => ( $page * $per_page ) < $total,
    ] );
}


/* ============================================================================
 * GET /payments/destinations
 *
 * Devolve os números M-Pesa/E-Mola para onde o utilizador faz a transferência
 * antes de criar o pedido de depósito.
 * ============================================================================ */
function rmbz_api_handle_payment_destinations( WP_REST_Request $request ) {
    // Settings em rmbz_payment_settings (admin-settings.php)
    $payment_settings = get_option( 'rmbz_payment_settings', [] );
    if ( ! is_array( $payment_settings ) ) $payment_settings = [];

    $mpesa = isset( $payment_settings['rmbz_mpesa_number'] )
        ? (string) $payment_settings['rmbz_mpesa_number']
        : '';
    $emola = isset( $payment_settings['rmbz_emola_number'] )
        ? (string) $payment_settings['rmbz_emola_number']
        : '';

    // Beneficiário (nome para confirmar na transferência)
    // [1.6.118.5] Nomes titulares por método (fallback para beneficiary global)
    $beneficiary = (string) get_option( 'rmbz_payment_beneficiary', '1MZ Records' );
    $mpesa_name = isset( $payment_settings['rmbz_mpesa_name'] ) && trim( (string) $payment_settings['rmbz_mpesa_name'] ) !== ''
        ? (string) $payment_settings['rmbz_mpesa_name']
        : $beneficiary;
    $emola_name = isset( $payment_settings['rmbz_emola_name'] ) && trim( (string) $payment_settings['rmbz_emola_name'] ) !== ''
        ? (string) $payment_settings['rmbz_emola_name']
        : $beneficiary;

    return rmbz_api_ok( [
        'destinations' => [
            [
                'method'      => 'M-Pesa',
                'number'      => $mpesa,
                'beneficiary' => $mpesa_name,
                'enabled'     => $mpesa !== '',
            ],
            [
                'method'      => 'E-Mola',
                'number'      => $emola,
                'beneficiary' => $emola_name,
                'enabled'     => $emola !== '',
            ],
        ],
        'instructions' => 'Faça a transferência para o número indicado e depois '
            . 'submeta o ID da transação ou a captura de ecrã como prova.',
    ] );
}


/* ============================================================================
 * POST /wallet/deposits
 *
 * Cria pedido de depósito. Replica rmbz_processar_deposito (form-handler.php)
 * mas com auth via JWT e devolução JSON em vez de redirect.
 * ============================================================================ */
function rmbz_api_handle_create_deposit( WP_REST_Request $request ) {
    // [1.6.47] Bloqueio global de depósitos
    if ( function_exists( 'rmbz_is_globally_blocked' ) && rmbz_is_globally_blocked( 'deposits' ) ) {
        return rmbz_api_error(
            'Os depósitos estão temporariamente suspensos pela administração. Tenta de novo mais tarde.',
            403,
            'deposits_globally_blocked'
        );
    }

    $tel    = $request->get_param( 'rmbz_telemovel' );
    $amount = (float) $request->get_param( 'amount' );
    $method = sanitize_text_field( (string) $request->get_param( 'payment_method' ) );
    $sender = sanitize_text_field( (string) $request->get_param( 'sender_phone' ) );
    $tx_id  = sanitize_text_field( (string) $request->get_param( 'transaction_id' ) );
    $proof_image_b64 = (string) $request->get_param( 'proof_image' );
    $proof           = esc_url_raw( (string) $request->get_param( 'proof_image_url' ) );

    // 1. Validações
    if ( $amount <= 0 ) {
        return rmbz_api_error( 'Valor inválido.', 422, 'invalid_amount' );
    }
    if ( $amount > 50000 ) {
        return rmbz_api_error( 'Valor excede o limite de 50.000 MT por depósito.', 422, 'amount_over_limit' );
    }
    if ( ! in_array( $method, [ 'M-Pesa', 'E-Mola' ], true ) ) {
        return rmbz_api_error( 'Método de pagamento inválido. Use M-Pesa ou E-Mola.', 422, 'invalid_method' );
    }
    if ( $sender === '' ) {
        return rmbz_api_error( 'Indique o número de telefone que usou para enviar o pagamento.', 422, 'sender_required' );
    }
    if ( $tx_id === '' && $proof === '' && $proof_image_b64 === '' ) {
        return rmbz_api_error( 'É necessário ID da transação ou imagem de prova.', 422, 'proof_required' );
    }

    // 2. Não permitir 2 depósitos pendentes
    global $wpdb;
    $table_deposits = $wpdb->prefix . 'rmbz_deposit_requests';
    $existing = $wpdb->get_var( $wpdb->prepare(
        "SELECT id FROM {$table_deposits} WHERE user_telemovel = %s AND status = 'pendente'",
        $tel
    ) );
    if ( $existing ) {
        return rmbz_api_error(
            'Já tem um depósito pendente. Aguarde aprovação antes de criar outro.',
            409,
            'pending_deposit_exists'
        );
    }

    // Guardar a imagem só depois de confirmar que o pedido pode ser criado,
    // evitando ficheiros órfãos quando já existe um depósito pendente.
    if ( $proof_image_b64 !== '' ) {
        $proof = function_exists( 'rmbz_api_save_base64_proof' )
            ? rmbz_api_save_base64_proof( $proof_image_b64 )
            : false;
        if ( ! $proof ) {
            return rmbz_api_error(
                'Não foi possível guardar o comprovativo. Escolhe outra imagem.',
                400,
                'bad_proof_image'
            );
        }
    }

    // 3. Inserir
    // [1.6.118.11] Marcar source_platform='app' — este endpoint só é
    // chamado pelo app (top-up VIP). is_first_deposit=0 porque este
    // fluxo é para utilizadores existentes (registo usa endpoint
    // separado em auth-controller).
    $inserted = $wpdb->insert( $table_deposits, [
        'user_telemovel'   => $tel,
        'amount'           => $amount,
        'status'           => 'pendente',
        'payment_method'   => $method,
        'transaction_id'   => $tx_id,
        'proof_image'      => $proof,
        'sender_phone'     => $sender,
        'request_date'     => current_time( 'mysql', 1 ),
        'source_platform'  => 'app',
        'is_first_deposit' => 0,
    ], [ '%s', '%f', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d' ] );

    if ( ! $inserted ) {
        return rmbz_api_error( 'Erro técnico ao registar depósito. Tente de novo.', 500, 'insert_failed' );
    }

    $new_id = (int) $wpdb->insert_id;

    // [1.5.396] Push aos admins (app 1MZ Admin).
    do_action( 'rmbz_deposit_request_created', $new_id, $tel, (float) $amount, 'api' );

    // 4. Notificar admin (mesmo email usado pelo site)
    if ( function_exists( 'rmbz_send_admin_deposit_alert' ) ) {
        $user_name = $wpdb->get_var( $wpdb->prepare(
            "SELECT user_nome FROM {$wpdb->prefix}rmbz_wallets WHERE user_telemovel = %s",
            $tel
        ) );
        $nome_final = $user_name ? $user_name : $tel;
        $tx_display = ! empty( $tx_id ) ? $tx_id : 'Imagem/Screenshot (Ver no Painel)';
        rmbz_send_admin_deposit_alert( $nome_final, $amount, $method, $tx_display, $new_id );
    }

    return rmbz_api_ok( [
        'deposit_id' => $new_id,
        'amount'     => $amount,
        'method'     => $method,
        'status'     => 'pendente',
        'message'    => 'Pedido de depósito enviado. Aguarde aprovação (geralmente 1-24h).',
    ], 201 );
}
