<?php
/**
 * API da app 1MZ Admin.
 *
 * Autenticação separada da app dos artistas: usa credenciais WordPress e
 * aceita apenas utilizadores com a capability manage_options.
 */

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

add_action( 'rest_api_init', function () {
    register_rest_route( RMBZ_API_NAMESPACE, '/admin-app/login', [
        'methods'             => 'POST',
        'callback'            => 'rmbz_api_admin_app_login',
        'permission_callback' => '__return_true',
    ] );

    register_rest_route( RMBZ_API_NAMESPACE, '/admin-app/refresh', [
        'methods'             => 'POST',
        'callback'            => 'rmbz_api_admin_app_refresh',
        'permission_callback' => '__return_true',
    ] );

    register_rest_route( RMBZ_API_NAMESPACE, '/admin-app/logout', [
        'methods'             => 'POST',
        'callback'            => 'rmbz_api_admin_app_logout',
        'permission_callback' => '__return_true',
    ] );

    register_rest_route( RMBZ_API_NAMESPACE, '/admin-app/me', [
        'methods'             => 'GET',
        'callback'            => 'rmbz_api_admin_app_me',
        'permission_callback' => 'rmbz_api_require_admin_app_auth',
    ] );

    register_rest_route( RMBZ_API_NAMESPACE, '/admin-app/dashboard', [
        'methods'             => 'GET',
        'callback'            => 'rmbz_api_admin_app_dashboard',
        'permission_callback' => 'rmbz_api_require_admin_app_auth',
    ] );

    register_rest_route( RMBZ_API_NAMESPACE, '/admin-app/modules/(?P<module>[a-z_]+)', [
        'methods'             => 'GET',
        'callback'            => 'rmbz_api_admin_app_module',
        'permission_callback' => 'rmbz_api_require_admin_app_auth',
    ] );

    register_rest_route( RMBZ_API_NAMESPACE, '/admin-app/modules/(?P<module>[a-z_]+)/(?P<id>\d+)/action', [
        'methods'             => 'POST',
        'callback'            => 'rmbz_api_admin_app_module_action',
        'permission_callback' => 'rmbz_api_require_admin_app_auth',
    ] );

    register_rest_route( RMBZ_API_NAMESPACE, '/admin-app/releases/(?P<id>\d+)', [
        'methods'             => 'GET',
        'callback'            => 'rmbz_api_admin_app_release_detail',
        'permission_callback' => 'rmbz_api_require_admin_app_auth',
    ] );

    register_rest_route( RMBZ_API_NAMESPACE, '/admin-app/projects/(?P<id>\d+)', [
        'methods'             => 'GET',
        'callback'            => 'rmbz_api_admin_app_project_detail',
        'permission_callback' => 'rmbz_api_require_admin_app_auth',
    ] );

    register_rest_route( RMBZ_API_NAMESPACE, '/admin-app/projects/(?P<id>\d+)/chat', [
        [
            'methods'             => 'GET',
            'callback'            => 'rmbz_api_admin_app_project_chat',
            'permission_callback' => 'rmbz_api_require_admin_app_auth',
        ],
        [
            'methods'             => 'POST',
            'callback'            => 'rmbz_api_admin_app_project_chat_send',
            'permission_callback' => 'rmbz_api_require_admin_app_auth',
        ],
    ] );

    // [1.6.394] Transações do artista (depósitos, levantamentos, extrato, envios).
    register_rest_route( RMBZ_API_NAMESPACE, '/admin-app/artists/(?P<id>\d+)/transactions', [
        'methods'             => 'GET',
        'callback'            => 'rmbz_api_admin_app_artist_transactions',
        'permission_callback' => 'rmbz_api_require_admin_app_auth',
    ] );
} );

function rmbz_api_require_admin_app_auth( WP_REST_Request $request ) {
    $token = rmbz_api_get_bearer_token();
    if ( ! $token ) {
        return new WP_Error( 'no_auth', 'Inicia sessão como administrador.', [ 'status' => 401 ] );
    }

    $payload = rmbz_api_jwt_decode( $token );
    if ( is_wp_error( $payload ) ) {
        return $payload;
    }

    if ( ( $payload['typ'] ?? '' ) !== 'access'
         || ( $payload['scope'] ?? '' ) !== 'admin_app'
         || empty( $payload['admin_user_id'] ) ) {
        return new WP_Error( 'invalid_admin_token', 'Sessão administrativa inválida.', [ 'status' => 401 ] );
    }

    $user = get_user_by( 'id', (int) $payload['admin_user_id'] );
    if ( ! $user || ! user_can( $user, 'manage_options' ) ) {
        return new WP_Error( 'admin_access_revoked', 'O acesso administrativo foi removido.', [ 'status' => 403 ] );
    }

    $request->set_param( 'rmbz_admin_user_id', (int) $user->ID );
    return true;
}

function rmbz_api_admin_app_login( WP_REST_Request $request ) {
    $ip = rmbz_api_get_client_ip();
    $rate = rmbz_api_rate_limit( 'admin_app_login_' . md5( $ip ), 8, 900 );
    if ( ! $rate['allowed'] ) {
        return rmbz_api_error( 'Demasiadas tentativas. Aguarda alguns minutos.', 429, 'rate_limited' );
    }

    $login    = sanitize_text_field( (string) $request->get_param( 'login' ) );
    $password = (string) $request->get_param( 'password' );
    if ( $login === '' || $password === '' ) {
        return rmbz_api_error( 'Utilizador/email e palavra-passe são obrigatórios.', 400, 'missing_fields' );
    }

    $user = wp_authenticate( $login, $password );
    if ( is_wp_error( $user ) || ! user_can( $user, 'manage_options' ) ) {
        return rmbz_api_error( 'Credenciais administrativas inválidas.', 401, 'bad_admin_credentials' );
    }

    $refresh = rmbz_api_admin_app_issue_refresh( (int) $user->ID );

    return rmbz_api_ok( [
        'access_token'       => rmbz_api_admin_app_issue_access( (int) $user->ID ),
        'expires_in'         => 8 * HOUR_IN_SECONDS,
        'refresh_token'      => $refresh,
        'refresh_expires_in' => 30 * DAY_IN_SECONDS,
        'admin'              => rmbz_api_admin_app_format_user( $user ),
    ] );
}

function rmbz_api_admin_app_issue_access( $user_id ) {
    $now = time();
    return rmbz_api_jwt_encode( [
        'iss'           => home_url(),
        'sub'           => 'admin:' . (int) $user_id,
        'admin_user_id' => (int) $user_id,
        'scope'         => 'admin_app',
        'iat'           => $now,
        'nbf'           => $now,
        'exp'           => $now + ( 8 * HOUR_IN_SECONDS ),
        'typ'           => 'access',
    ] );
}

/**
 * Refresh tokens do admin app — guardados como hash em user meta (não há
 * coluna de telemóvel para admins na tabela de refresh dos artistas).
 * Formato do token: "{user_id}.{64 hex}" para localizar o meta sem scan.
 */
function rmbz_api_admin_app_issue_refresh( $user_id ) {
    $refresh = (int) $user_id . '.' . rmbz_api_generate_refresh_token();
    $tokens  = get_user_meta( $user_id, 'rmbz_admin_app_refresh_tokens', true );
    if ( ! is_array( $tokens ) ) {
        $tokens = [];
    }

    $now = time();
    foreach ( $tokens as $hash => $info ) {
        if ( empty( $info['expires'] ) || (int) $info['expires'] < $now ) {
            unset( $tokens[ $hash ] );
        }
    }

    // Máximo de 5 sessões por admin — descarta as mais antigas.
    if ( count( $tokens ) >= 5 ) {
        uasort( $tokens, function ( $a, $b ) {
            return ( $a['created'] ?? 0 ) <=> ( $b['created'] ?? 0 );
        } );
        $tokens = array_slice( $tokens, -4, null, true );
    }

    $tokens[ rmbz_api_hash_refresh_token( $refresh ) ] = [
        'expires' => $now + ( 30 * DAY_IN_SECONDS ),
        'created' => $now,
        'ua'      => isset( $_SERVER['HTTP_USER_AGENT'] ) ? substr( (string) $_SERVER['HTTP_USER_AGENT'], 0, 191 ) : '',
    ];
    update_user_meta( $user_id, 'rmbz_admin_app_refresh_tokens', $tokens );

    return $refresh;
}

/**
 * Valida e REMOVE o refresh token (uso único — rotação obrigatória).
 * Devolve o user_id ou WP_Error.
 */
function rmbz_api_admin_app_consume_refresh( $refresh ) {
    $refresh = trim( (string) $refresh );
    if ( $refresh === '' || ! preg_match( '/^(\d+)\.[0-9a-f]{64}$/', $refresh, $m ) ) {
        return new WP_Error( 'bad_admin_refresh', 'Refresh token inválido.', [ 'status' => 401 ] );
    }

    $user_id = (int) $m[1];
    $tokens  = get_user_meta( $user_id, 'rmbz_admin_app_refresh_tokens', true );
    if ( ! is_array( $tokens ) ) {
        $tokens = [];
    }

    $hash = rmbz_api_hash_refresh_token( $refresh );
    if ( empty( $tokens[ $hash ] ) ) {
        return new WP_Error( 'bad_admin_refresh', 'Refresh token inválido ou revogado.', [ 'status' => 401 ] );
    }

    $info = $tokens[ $hash ];
    unset( $tokens[ $hash ] );
    update_user_meta( $user_id, 'rmbz_admin_app_refresh_tokens', $tokens );

    if ( empty( $info['expires'] ) || (int) $info['expires'] < time() ) {
        return new WP_Error( 'admin_refresh_expired', 'Sessão expirada. Inicia sessão novamente.', [ 'status' => 401 ] );
    }

    return $user_id;
}

function rmbz_api_admin_app_refresh( WP_REST_Request $request ) {
    $ip   = rmbz_api_get_client_ip();
    $rate = rmbz_api_rate_limit( 'admin_app_refresh_' . md5( $ip ), 30, 900 );
    if ( ! $rate['allowed'] ) {
        return rmbz_api_error( 'Demasiadas tentativas. Aguarda alguns minutos.', 429, 'rate_limited' );
    }

    $user_id = rmbz_api_admin_app_consume_refresh( $request->get_param( 'refresh_token' ) );
    if ( is_wp_error( $user_id ) ) {
        return rmbz_api_error( $user_id->get_error_message(), 401, $user_id->get_error_code() );
    }

    $user = get_user_by( 'id', $user_id );
    if ( ! $user || ! user_can( $user, 'manage_options' ) ) {
        return rmbz_api_error( 'O acesso administrativo foi removido.', 403, 'admin_access_revoked' );
    }

    return rmbz_api_ok( [
        'access_token'       => rmbz_api_admin_app_issue_access( $user_id ),
        'expires_in'         => 8 * HOUR_IN_SECONDS,
        'refresh_token'      => rmbz_api_admin_app_issue_refresh( $user_id ),
        'refresh_expires_in' => 30 * DAY_IN_SECONDS,
    ] );
}

function rmbz_api_admin_app_logout( WP_REST_Request $request ) {
    // Consumir = revogar. Resposta sempre ok para o logout ser idempotente.
    rmbz_api_admin_app_consume_refresh( $request->get_param( 'refresh_token' ) );
    return rmbz_api_ok( [ 'logged_out' => true ] );
}

function rmbz_api_admin_app_me( WP_REST_Request $request ) {
    $user = get_user_by( 'id', (int) $request->get_param( 'rmbz_admin_user_id' ) );
    return rmbz_api_ok( rmbz_api_admin_app_format_user( $user ) );
}

function rmbz_api_admin_app_format_user( $user ) {
    return [
        'id'           => (int) $user->ID,
        'display_name' => (string) $user->display_name,
        'email'        => (string) $user->user_email,
        'avatar_url'   => get_avatar_url( $user->ID, [ 'size' => 160 ] ),
    ];
}

function rmbz_api_admin_app_dashboard() {
    global $wpdb;

    $t_wallets     = $wpdb->prefix . 'rmbz_wallets';
    $t_submissions = $wpdb->prefix . 'rmbz_artist_submissions';
    $t_projects    = $wpdb->prefix . 'rmbz_artist_projects';
    $t_deposits    = $wpdb->prefix . 'rmbz_deposit_requests';
    $t_withdrawals = $wpdb->prefix . 'rmbz_withdrawal_requests';
    $t_releases    = $wpdb->prefix . 'rmbz_distro_releases';
    $t_chat        = $wpdb->prefix . 'rmbz_project_chat_messages';

    $table_exists = function ( $table ) use ( $wpdb ) {
        return $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table;
    };
    $count = function ( $sql, $table ) use ( $wpdb, $table_exists ) {
        return $table_exists( $table ) ? (int) $wpdb->get_var( $sql ) : 0;
    };
    $sum = function ( $sql, $table ) use ( $wpdb, $table_exists ) {
        return $table_exists( $table ) ? (float) $wpdb->get_var( $sql ) : 0.0;
    };

    $pending_site = $count(
        "SELECT COUNT(*) FROM {$t_submissions} WHERE review_status='pendente' AND scenario_type='Publicação Site'",
        $t_submissions
    );
    $pending_social = $count(
        "SELECT COUNT(*) FROM {$t_submissions} WHERE review_status='pendente' AND scenario_type='Redes Sociais'",
        $t_submissions
    );
    $pending_projects = $count(
        "SELECT COUNT(*) FROM {$t_projects} WHERE production_status IN ('pending_review','kyc_review')",
        $t_projects
    );
    $unread_chat = $count(
        "SELECT COUNT(DISTINCT project_id) FROM {$t_chat} WHERE read_by_admin=0 AND author_type='artist' AND is_deleted=0",
        $t_chat
    );
    $pending_deposits = $count( "SELECT COUNT(*) FROM {$t_deposits} WHERE status='pendente'", $t_deposits );
    $pending_withdrawals = $count( "SELECT COUNT(*) FROM {$t_withdrawals} WHERE status='pendente'", $t_withdrawals );
    $pending_releases = $count(
        "SELECT COUNT(*) FROM {$t_releases} WHERE status IN ('pendente','pending','submitted','em_revisao','enviado')",
        $t_releases
    );
    $artists_total = $count(
        "SELECT COUNT(*) FROM {$t_wallets} WHERE extra_data NOT LIKE '%\"deleted_at\"%' OR extra_data IS NULL",
        $t_wallets
    );

    // [1.5.410] CONTROLO FINANCEIRO — taxas de vistoria (equipa externa) por
    // cobrar: projetos com a filmagem terminada e a taxa em falta (bloqueados).
    $vistoria_projects = 0;
    $vistoria_pending  = 0.0;
    if ( function_exists( 'rmbz_team_vistoria_status' ) && $table_exists( $t_projects ) ) {
        $rows_v = $wpdb->get_results(
            "SELECT id, production_status, scenario_details, amount_paid FROM {$t_projects}
             WHERE production_status IN ('editing','done') AND scenario_details LIKE '%team_fee_for%'"
        );
        foreach ( (array) $rows_v as $rv ) {
            $vs = rmbz_team_vistoria_status( $rv );
            if ( ! empty( $vs['blocked'] ) ) {
                $vistoria_projects++;
                $vistoria_pending += (float) $vs['left'];
            }
        }
    }

    $modules = [
        [ 'key' => 'projects', 'title' => 'Projetos & Chat', 'description' => 'Produções, KYC e conversas', 'pending' => $pending_projects + $unread_chat ],
        [ 'key' => 'artists', 'title' => 'Artistas & Saldos', 'description' => 'Contas, saldos e sessões', 'pending' => 0 ],
        [ 'key' => 'distribution', 'title' => 'Distribuição', 'description' => 'Lançamentos e operações DSP', 'pending' => $pending_releases ],
        [ 'key' => 'finance', 'title' => 'Finanças', 'description' => 'Depósitos e levantamentos', 'pending' => $pending_deposits + $pending_withdrawals ],
        [ 'key' => 'content', 'title' => 'Conteúdo', 'description' => 'Música para site e vídeos sociais', 'pending' => $pending_site + $pending_social ],
        [ 'key' => 'coupons', 'title' => 'Cupões', 'description' => 'Campanhas promocionais', 'pending' => 0 ],
        [ 'key' => 'announcements', 'title' => 'Avisos no App', 'description' => 'Comunicação com artistas', 'pending' => 0 ],
        [ 'key' => 'system', 'title' => 'Sistema', 'description' => 'Versões, segurança e configurações', 'pending' => 0 ],
    ];

    // Fluxo dos últimos 7 dias: entradas (depósitos aprovados) vs saídas (levantamentos aprovados).
    $flow_7d = [];
    for ( $i = 6; $i >= 0; $i-- ) {
        $day = wp_date( 'Y-m-d', strtotime( "-{$i} days", current_time( 'timestamp' ) ) );
        $in  = $sum( $wpdb->prepare( "SELECT COALESCE(SUM(amount),0) FROM {$t_deposits} WHERE status='aprovado' AND DATE(COALESCE(processed_date,request_date))=%s", $day ), $t_deposits );
        $out = $sum( $wpdb->prepare( "SELECT COALESCE(SUM(amount),0) FROM {$t_withdrawals} WHERE status='aprovado' AND DATE(COALESCE(processed_date,request_date))=%s", $day ), $t_withdrawals );
        $flow_7d[] = [
            'label' => wp_date( 'D', strtotime( $day ) ),
            'in'    => round( (float) $in, 2 ),
            'out'   => round( (float) $out, 2 ),
        ];
    }

    return rmbz_api_ok( [
        'summary' => [
            'attention_total'       => $pending_site + $pending_social + $pending_projects + $unread_chat + $pending_deposits + $pending_withdrawals + $pending_releases,
            'artists_total'         => $artists_total,
            'pending_projects'      => $pending_projects,
            'unread_project_chats'  => $unread_chat,
            'pending_deposits'      => $pending_deposits,
            'pending_withdrawals'   => $pending_withdrawals,
            'pending_releases'      => $pending_releases,
            'pending_content'       => $pending_site + $pending_social,
            'pending_deposit_value' => $sum( "SELECT COALESCE(SUM(amount),0) FROM {$t_deposits} WHERE status='pendente'", $t_deposits ),
            'pending_withdrawal_value' => $sum( "SELECT COALESCE(SUM(amount),0) FROM {$t_withdrawals} WHERE status='pendente'", $t_withdrawals ),
            // [1.5.410] Taxas de vistoria (equipa) por cobrar — projetos bloqueados.
            'vistoria_blocked_projects' => $vistoria_projects,
            'vistoria_pending_value'    => round( $vistoria_pending, 2 ),
        ],
        'modules' => $modules,
        'flow_7d' => $flow_7d,
        'generated_at' => rmbz_api_admin_app_format_date( current_time( 'c' ) ),
    ] );
}

function rmbz_api_admin_app_module( WP_REST_Request $request ) {
    global $wpdb;
    $module = sanitize_key( (string) $request['module'] );
    $limit  = min( 100, max( 1, (int) ( $request->get_param( 'limit' ) ?: 50 ) ) );
    $offset = max( 0, (int) $request->get_param( 'offset' ) );
    $search = trim( sanitize_text_field( (string) $request->get_param( 'search' ) ) );
    $like   = $search !== '' ? '%' . $wpdb->esc_like( $search ) . '%' : '';
    // Pede-se 1 registo extra só para saber se existe próxima página.
    $fetch    = $limit + 1;
    $has_more = false;
    $items    = [];

    if ( $module === 'projects' ) {
        $t = $wpdb->prefix . 'rmbz_artist_projects';
        $chat = $wpdb->prefix . 'rmbz_project_chat_messages';
        $has_chat = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $chat ) ) === $chat;
        $chat_select = $has_chat
            ? ", (SELECT COUNT(*) FROM {$chat} c WHERE c.project_id=p.id AND c.read_by_admin=0 AND c.author_type='artist' AND c.is_deleted=0) unread"
            : ', 0 unread';
        $wallets = $wpdb->prefix . 'rmbz_wallets';
        $where = $like !== ''
            ? $wpdb->prepare( 'WHERE (p.project_title LIKE %s OR p.artist_telemovel LIKE %s OR w.user_nome LIKE %s)', $like, $like, $like )
            : '';
        $rows = $wpdb->get_results(
            "SELECT p.id,p.project_title,p.artist_telemovel,p.production_status,p.scenario_type,p.scenario_cost,p.amount_paid,p.contract_date,p.extra_data proj_extra,
                    w.user_nome, w.extra_data wallet_extra,
                    (SELECT MAX(end_date) FROM {$wpdb->prefix}rmbz_subscriptions sub WHERE sub.user_telemovel=p.artist_telemovel AND sub.status='active' AND sub.end_date > NOW()) vip_until
                    {$chat_select}
             FROM {$t} p LEFT JOIN {$wallets} w ON w.user_telemovel = p.artist_telemovel
             {$where} ORDER BY p.id DESC LIMIT {$fetch} OFFSET {$offset}"
        );
        $has_more = count( (array) $rows ) > $limit;
        $rows = array_slice( (array) $rows, 0, $limit );
        $rmbz_prod_states = function_exists( 'rmbz_project_inprogress_states' )
            ? rmbz_project_inprogress_states() : [ 'pre-production', 'kyc_review', 'filming', 'editing' ];
        foreach ( (array) $rows as $r ) {
            $project_actions = [];
            if ( $r->production_status === 'pending_review' ) $project_actions[] = 'project_approve';
            if ( $r->production_status === 'kyc_review' ) $project_actions[] = 'kyc_approve';
            if ( in_array( $r->production_status, [ 'pending_review', 'kyc_review', 'pre-production' ], true ) ) $project_actions[] = 'project_reject';

            // [1.6.393] Multa por atraso (deste projeto) + estado de notificações.
            $proj_extra = ! empty( $r->proj_extra ) ? json_decode( $r->proj_extra, true ) : [];
            if ( ! is_array( $proj_extra ) ) $proj_extra = [];
            $notif_muted = ! empty( $proj_extra['notifications_muted'] );

            $fine_amount = 0.0; $fine_state = '';
            if ( in_array( $r->production_status, $rmbz_prod_states, true ) && function_exists( 'rmbz_project_overdue_info' ) ) {
                $ov = rmbz_project_overdue_info( $r->artist_telemovel );
                if ( ! empty( $ov['overdue'] ) && (int) ( $ov['project_id'] ?? 0 ) === (int) $r->id
                    && ! empty( $ov['enabled'] ) && (float) ( $ov['amount'] ?? 0 ) > 0 ) {
                    $fine_amount = (float) $ov['amount'];
                    $fine_state  = ! empty( $ov['frozen'] ) ? 'frozen' : 'accruing';
                    $project_actions[] = 'project_clear_fine';
                }
            }
            // Permitir silenciar/reativar notificações em projetos em curso.
            if ( in_array( $r->production_status, $rmbz_prod_states, true ) || $r->production_status === 'pending_review' ) {
                $project_actions[] = 'project_toggle_notifications';
            }

            // Estado do artista (VIP/online) — igual ao módulo de artistas.
            $wallet_extra = ! empty( $r->wallet_extra ) ? json_decode( $r->wallet_extra, true ) : [];
            if ( ! is_array( $wallet_extra ) ) $wallet_extra = [];
            $last_seen = (int) ( $wallet_extra['last_seen'] ?? 0 );
            $is_online = $last_seen > 0 && ( time() - $last_seen ) < 300;

            $items[] = rmbz_api_admin_app_item(
                $r->id, $r->project_title ?: 'Projeto sem título', $r->artist_telemovel,
                $r->production_status, $r->contract_date,
                [ 'Cenário: ' . $r->scenario_type, 'Chat não lido: ' . (int) $r->unread ],
                (float) $r->scenario_cost, $project_actions,
                [
                    'artist_name' => (string) ( $r->user_nome ?: '' ),
                    'is_vip'      => $r->vip_until ? '1' : '0',
                    'vip_until'   => $r->vip_until ? rmbz_api_admin_app_format_date( $r->vip_until ) : '',
                    'is_online'   => $is_online ? '1' : '0',
                    // [1.6.393] Multa + notificações para o app admin mostrar.
                    'fine_amount' => $fine_amount > 0 ? number_format( $fine_amount, 2, '.', '' ) : '',
                    'fine_state'  => $fine_state,
                    'notif_muted' => $notif_muted ? '1' : '0',
                    // [1.5.396] Contagem de chat por ler — badge no cartão da lista.
                    'unread_chat' => (string) (int) $r->unread,
                    // Relativo ("há 5 minutos", "há 3 dias") para o hero do app.
                    'last_seen'   => $last_seen > 0
                        ? ( $is_online ? 'online agora' : 'há ' . human_time_diff( $last_seen, time() ) )
                        : 'sem registo',
                ]
            );
        }
    } elseif ( $module === 'artists' ) {
        $t = $wpdb->prefix . 'rmbz_wallets';
        $tokens = $wpdb->prefix . 'rmbz_api_refresh_tokens';
        $has_tokens = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $tokens ) ) === $tokens;
        $sessions_select = $has_tokens
            ? ", (SELECT COUNT(*) FROM {$tokens} tok WHERE tok.user_telemovel=w.user_telemovel AND tok.revoked_at IS NULL AND tok.expires_at > NOW()) active_sessions"
            : ', 0 active_sessions';
        $distro_stats = $wpdb->prefix . 'rmbz_distro_stats';
        $has_distro_stats = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $distro_stats ) ) === $distro_stats;
        $distro_select = $has_distro_stats
            ? ", (SELECT total_earnings FROM {$distro_stats} stats WHERE stats.artist_telemovel=w.user_telemovel LIMIT 1) distro_balance"
            : ', 0.0 distro_balance';
        $where = $like !== ''
            ? $wpdb->prepare( 'WHERE (w.user_nome LIKE %s OR w.user_telemovel LIKE %s OR w.user_email LIKE %s)', $like, $like, $like )
            : '';
        $rows = $wpdb->get_results(
            "SELECT w.id,w.user_nome,w.user_telemovel,w.user_email,w.user_provincia,w.balance,w.is_banned,w.extra_data,w.last_updated,
                    (SELECT COUNT(*) FROM {$wpdb->prefix}rmbz_wallet_transactions tx WHERE tx.user_telemovel=w.user_telemovel) tx_count,
                    (SELECT COUNT(*) FROM {$wpdb->prefix}rmbz_deposit_requests dep WHERE dep.user_telemovel=w.user_telemovel) deposit_count,
                    (SELECT COUNT(*) FROM {$wpdb->prefix}rmbz_withdrawal_requests wd WHERE wd.user_telemovel=w.user_telemovel) withdrawal_count,
                    (SELECT COUNT(*) FROM {$wpdb->prefix}rmbz_artist_projects pr WHERE pr.artist_telemovel=w.user_telemovel) project_count,
                    (SELECT COUNT(*) FROM {$wpdb->prefix}rmbz_distro_releases rel WHERE rel.artist_telemovel=w.user_telemovel) release_count,
                    (SELECT MAX(end_date) FROM {$wpdb->prefix}rmbz_subscriptions sub WHERE sub.user_telemovel=w.user_telemovel AND sub.status='active' AND sub.end_date > NOW()) vip_until,
                    (SELECT source_platform FROM {$wpdb->prefix}rmbz_deposit_requests first_dep WHERE first_dep.user_telemovel=w.user_telemovel AND first_dep.is_first_deposit=1 ORDER BY first_dep.id ASC LIMIT 1) source_platform
                    {$sessions_select}
                    {$distro_select}
             FROM {$t} w {$where} ORDER BY w.id DESC LIMIT {$fetch} OFFSET {$offset}"
        );
        $has_more = count( (array) $rows ) > $limit;
        $rows = array_slice( (array) $rows, 0, $limit );
        foreach ( (array) $rows as $r ) {
            $extra = ! empty( $r->extra_data ) ? json_decode( $r->extra_data, true ) : [];
            if ( ! is_array( $extra ) ) $extra = [];
            $blocks = rmbz_api_admin_app_artist_blocks( $extra, (string) $r->user_telemovel );
            $location = trim( (string) ( $r->user_provincia ?: ( $extra['provincia'] ?? '' ) ) );
            if ( ! empty( $extra['cidade'] ) ) {
                $location .= $location ? ', ' . $extra['cidade'] : $extra['cidade'];
            }
            $last_seen = (int) ( $extra['last_seen'] ?? 0 );
            $is_online = $last_seen > 0 && ( time() - $last_seen ) < 300;
            $last_seen_label = $last_seen > 0
                ? ( $is_online ? 'online agora' : wp_date( 'd/m/Y H:i', $last_seen ) )
                : 'sem registo';
            $origin = (string) ( $r->source_platform ?? '' );
            $origin_label = $origin === 'app' ? 'App Android' : ( $origin === 'site' ? 'Site' : '—' );
            $actions = [
                'artist_adjust_balance',
                'artist_reset_pin',
                'artist_send_popup',
                'artist_toggle_ban',
                'artist_toggle_deposits',
                'artist_toggle_withdrawals',
                'artist_toggle_distribution',
                'artist_toggle_video_applications',
                'artist_toggle_promotions',
                'artist_toggle_referrals',
            ];
            if ( ! empty( $extra['admin_popup'] ) ) {
                array_splice( $actions, 3, 0, 'artist_clear_popup' );
            }
            if ( (int) $r->active_sessions > 0 ) {
                array_splice( $actions, 3, 0, 'artist_revoke_sessions' );
            }
            if ( $r->vip_until ) {
                array_splice( $actions, 3, 0, 'artist_cancel_vip' );
            }

            $distro_balance = (float) ( $r->distro_balance ?? 0.0 );
            $items[] = rmbz_api_admin_app_item(
                $r->id, $r->user_nome ?: $r->user_telemovel, $r->user_telemovel,
                $r->is_banned ? 'suspenso' : 'ativo', $r->last_updated,
                [
                    'Email: ' . ( $r->user_email ?: '—' ),
                    'Perfil: ' . ( $location ?: 'localização não definida' ),
                    'Origem: ' . $origin_label,
                    'Último acesso: ' . $last_seen_label,
                    'VIP: ' . ( $r->vip_until ? 'ativo até ' . rmbz_api_admin_app_format_date( $r->vip_until ) : 'sem VIP ativo' ),
                    'Sessões ativas: ' . (int) $r->active_sessions,
                    'Transações: ' . (int) $r->tx_count,
                    'Depósitos: ' . (int) $r->deposit_count,
                    'Levantamentos: ' . (int) $r->withdrawal_count,
                    'Projetos: ' . (int) $r->project_count,
                    'Releases: ' . (int) $r->release_count,
                    'Distribuição: ' . number_format( $distro_balance, 2 ) . ' USD',
                    'Bloqueios: ' . ( $blocks ? implode( ', ', $blocks ) : 'nenhum' ),
                    ! empty( $extra['admin_popup'] ) ? 'Aviso direto pendente' : '',
                ],
                (float) $r->balance,
                $actions,
                [
                    'is_online'       => $is_online ? '1' : '0',
                    'is_vip'          => $r->vip_until ? '1' : '0',
                    'vip_until'       => $r->vip_until ? rmbz_api_admin_app_format_date( $r->vip_until ) : '',
                    'last_seen'       => $last_seen_label,
                    'active_sessions' => (string) (int) $r->active_sessions,
                    'email'           => (string) ( $r->user_email ?: '' ),
                    'location'        => (string) $location,
                    'origin'          => $origin_label,
                    'popup_title'     => (string) ( $extra['admin_popup']['title'] ?? '' ),
                    'popup_message'   => (string) ( $extra['admin_popup']['message'] ?? '' ),
                    'popup_type'      => (string) ( $extra['admin_popup']['type'] ?? '' ),
                    'distro_balance'  => (string) $distro_balance,
                    // Estado dos serviços (1 = ligado/permitido, 0 = bloqueado) — para os switches no app.
                    'svc_deposits'     => empty( $extra['block_deposits'] ) ? '1' : '0',
                    'svc_withdrawals'  => empty( $extra['block_withdrawals'] ) ? '1' : '0',
                    'svc_distribution' => empty( $extra['block_distribution'] ) ? '1' : '0',
                    'svc_video'        => empty( $extra['block_video_applications'] ) ? '1' : '0',
                    'svc_promotions'   => empty( $extra['block_promotions'] ) ? '1' : '0',
                    'svc_referrals'    => in_array( (string) $r->user_telemovel, array_map( 'strval', (array) get_option( 'rmbz_referral_blacklist', [] ) ), true ) ? '0' : '1',
                ]
            );
        }
    } elseif ( $module === 'distribution' ) {
        $t = $wpdb->prefix . 'rmbz_distro_releases';
        $wallets_t = $wpdb->prefix . 'rmbz_wallets';
        $where = $like !== ''
            ? $wpdb->prepare( 'WHERE (r.title LIKE %s OR r.artist_telemovel LIKE %s OR w.user_nome LIKE %s)', $like, $like, $like )
            : '';
        $rows = $wpdb->get_results(
            "SELECT r.id,r.title,r.artist_telemovel,r.status,r.release_type,r.primary_genre,r.release_date,r.created_at,r.cover_image,
                    w.user_nome
             FROM {$t} r LEFT JOIN {$wallets_t} w ON w.user_telemovel = r.artist_telemovel
             {$where} ORDER BY r.id DESC LIMIT {$fetch} OFFSET {$offset}"
        );
        $has_more = count( (array) $rows ) > $limit;
        $rows = array_slice( (array) $rows, 0, $limit );
        foreach ( (array) $rows as $r ) {
            $items[] = rmbz_api_admin_app_item(
                $r->id,
                $r->title,
                $r->artist_telemovel,
                $r->status,
                $r->release_date ?: $r->created_at,
                [ 'Artista: ' . ( $r->user_nome ?: '—' ), 'Tipo: ' . $r->release_type, 'Género: ' . ( $r->primary_genre ?: '—' ) ],
                null,
                rmbz_api_admin_app_release_actions( $r->status ),
                [
                    'artist_name' => (string) ( $r->user_nome ?: '' ),
                    'cover_image' => $r->cover_image ? ( function_exists( 'rmbz_api_safe_url' ) ? rmbz_api_safe_url( $r->cover_image ) : $r->cover_image ) : '',
                ]
            );
        }
    } elseif ( $module === 'finance' ) {
        $dep = $wpdb->prefix . 'rmbz_deposit_requests';
        $withd = $wpdb->prefix . 'rmbz_withdrawal_requests';
        // Junta dois tipos numa lista única ordenada por data, por isso o
        // offset só pode ser aplicado DEPOIS do merge — pede-se offset+limit+1
        // a cada tabela para garantir que a janela fica completa.
        $merge_fetch = $offset + $limit + 1;
        $wallets_t = $wpdb->prefix . 'rmbz_wallets';
        $dep_where = $like !== ''
            ? $wpdb->prepare( 'WHERE (d.user_telemovel LIKE %s OR d.transaction_id LIKE %s OR d.sender_phone LIKE %s OR w.user_nome LIKE %s)', $like, $like, $like, $like )
            : '';
        $withd_where = $like !== ''
            ? $wpdb->prepare( 'WHERE (wd.user_telemovel LIKE %s OR wd.payment_details LIKE %s OR w.user_nome LIKE %s)', $like, $like, $like )
            : '';
        $deps = $wpdb->get_results(
            "SELECT d.id,d.user_telemovel,d.amount,d.status,d.payment_method,d.transaction_id,d.proof_image,d.sender_phone,
                    d.request_date,d.processed_date,d.admin_notes,d.source_platform,d.is_first_deposit,
                    w.user_nome,w.balance
             FROM {$dep} d LEFT JOIN {$wallets_t} w ON w.user_telemovel = d.user_telemovel
             {$dep_where} ORDER BY d.id DESC LIMIT {$merge_fetch}"
        );
        $withds = $wpdb->get_results(
            "SELECT wd.id,wd.user_telemovel,wd.amount,wd.status,wd.payment_method,wd.payment_details,wd.request_date,wd.processed_date,
                    wd.admin_notes,wd.source,wd.exchange_rate,
                    w.user_nome,w.balance
             FROM {$withd} wd LEFT JOIN {$wallets_t} w ON w.user_telemovel = wd.user_telemovel
             {$withd_where} ORDER BY wd.id DESC LIMIT {$merge_fetch}"
        );
        foreach ( (array) $deps as $r ) {
            $origin = $r->source_platform === 'app' ? 'App Android' : ( $r->source_platform === 'site' ? 'Site' : 'Antigo' );
            $items[] = rmbz_api_admin_app_item(
                $r->id, 'Depósito', $r->user_telemovel, $r->status, $r->request_date,
                [
                    'Artista: ' . ( $r->user_nome ?: '—' ),
                    'Método: ' . $r->payment_method,
                    'Pagador: ' . ( $r->sender_phone ?: '—' ),
                    'Origem: ' . $origin,
                    'Tipo: ' . ( $r->is_first_deposit ? 'Primeiro depósito / abertura' : 'Reforço de saldo' ),
                    'Comprovativo: ' . ( $r->proof_image ? 'Imagem anexada' : ( $r->transaction_id ?: '—' ) ),
                    $r->processed_date ? 'Processado: ' . rmbz_api_admin_app_format_date( $r->processed_date ) : '',
                    $r->admin_notes ? 'Notas: ' . $r->admin_notes : '',
                ],
                (float) $r->amount,
                $r->status === 'pendente' ? [ 'deposit_approve', 'deposit_reject' ] : [],
                [
                    'kind'             => 'deposit',
                    'artist_name'      => (string) ( $r->user_nome ?: '' ),
                    'proof_image'      => (string) $r->proof_image,
                    'transaction_id'   => (string) $r->transaction_id,
                    'sender_phone'     => (string) $r->sender_phone,
                    'payment_method'   => (string) $r->payment_method,
                    'source_platform'  => (string) $r->source_platform,
                    'is_first_deposit' => ! empty( $r->is_first_deposit ) ? '1' : '0',
                    'current_balance'  => number_format( (float) $r->balance, 2, '.', '' ),
                    'balance_after'    => number_format( (float) $r->balance + (float) $r->amount, 2, '.', '' ),
                ]
            );
        }
        foreach ( (array) $withds as $r ) {
            $source = $r->source === 'royalties' ? 'Royalties USD convertidos' : 'Carteira MT';

            // Guarda-Costas: para pendentes, envia o relatório anti-fraude e a
            // checklist para o app mostrar e completar (mesma segurança do site).
            $guard_extra = [];
            if ( $r->status === 'pendente' && function_exists( 'rmbz_wg_build_forensic_report' ) ) {
                $full = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$withd} WHERE id=%d", $r->id ) );
                if ( $full ) {
                    $report = rmbz_wg_build_forensic_report( $full );
                    $review = rmbz_wg_get_review( $r->id );
                    $checklist_out = [];
                    foreach ( rmbz_wg_checklist_items( $full, $report ) as $key => $meta ) {
                        $entry = rmbz_wg_checklist_entry( $review, $key );
                        $checklist_out[] = [
                            'key'      => $key,
                            'label'    => $meta['label'],
                            'critical' => ! empty( $meta['critical'] ),
                            'done'     => $entry['done'],
                            'reason'   => $entry['reason'],
                        ];
                    }
                    $flags_out = [];
                    foreach ( (array) ( $report['risk_flags'] ?? [] ) as $flag ) {
                        $flags_out[] = [
                            'level' => (string) ( $flag['level'] ?? 'low' ),
                            'msg'   => (string) ( $flag['msg'] ?? '' ),
                        ];
                    }
                    $guard_extra = [
                        'guard_flags'     => wp_json_encode( $flags_out ),
                        'guard_checklist' => wp_json_encode( $checklist_out ),
                        'guard_complete'  => rmbz_wg_checklist_complete( $r->id, $full, $report ) ? '1' : '0',
                    ];
                }
            }

            $items[] = rmbz_api_admin_app_item(
                $r->id, 'Levantamento', $r->user_telemovel, $r->status, $r->request_date,
                [
                    'Artista: ' . ( $r->user_nome ?: '—' ),
                    'Método: ' . $r->payment_method,
                    'Destino: ' . ( $r->payment_details ?: '—' ),
                    'Origem do valor: ' . $source,
                    $r->exchange_rate ? 'Taxa aplicada: ' . number_format( (float) $r->exchange_rate, 2, ',', '.' ) . ' MT/USD' : '',
                    $r->processed_date ? 'Processado: ' . rmbz_api_admin_app_format_date( $r->processed_date ) : '',
                    $r->admin_notes ? 'Notas: ' . $r->admin_notes : '',
                ],
                (float) $r->amount,
                $r->status === 'pendente' ? [ 'withdrawal_approve', 'withdrawal_reject' ] : [],
                array_merge(
                    [
                        'kind'              => 'withdrawal',
                        'artist_name'       => (string) ( $r->user_nome ?: '' ),
                        'payment_method'    => (string) $r->payment_method,
                        'payment_details'   => (string) $r->payment_details,
                        'source'            => (string) $r->source,
                        'exchange_rate'     => (string) $r->exchange_rate,
                        'current_balance'   => number_format( (float) $r->balance, 2, '.', '' ),
                        'balance_sufficient' => ( (float) $r->balance >= (float) $r->amount ) ? '1' : '0',
                    ],
                    $guard_extra
                )
            );
        }
        usort( $items, function ( $a, $b ) {
            return rmbz_api_admin_app_date_timestamp( $b['date'] ) <=> rmbz_api_admin_app_date_timestamp( $a['date'] );
        } );
        $has_more = count( $items ) > $offset + $limit;
        $items = array_slice( $items, $offset, $limit );
    } elseif ( $module === 'content' ) {
        $t = $wpdb->prefix . 'rmbz_artist_submissions';
        $where = $like !== ''
            ? $wpdb->prepare( 'WHERE (music_title LIKE %s OR artist_telemovel LIKE %s)', $like, $like )
            : '';
        $rows = $wpdb->get_results(
            "SELECT id,music_title,artist_telemovel,review_status,scenario_type,music_genre,submission_date
             FROM {$t} {$where} ORDER BY id DESC LIMIT {$fetch} OFFSET {$offset}"
        );
        $has_more = count( (array) $rows ) > $limit;
        $rows = array_slice( (array) $rows, 0, $limit );
        foreach ( (array) $rows as $r ) {
            $items[] = rmbz_api_admin_app_item(
                $r->id, $r->music_title, $r->artist_telemovel, $r->review_status, $r->submission_date,
                [ $r->scenario_type, 'Género: ' . $r->music_genre ], null,
                [ 'approve', 'correction', 'reject' ]
            );
        }
    } elseif ( $module === 'coupons' ) {
        $t = $wpdb->prefix . 'rmbz_coupons';
        $where = $like !== '' ? $wpdb->prepare( 'WHERE coupon_code LIKE %s', $like ) : '';
        $rows = $wpdb->get_results(
            "SELECT id,coupon_code,reward_type,reward_value,usage_limit,used_count,expiry_date,status,created_at
             FROM {$t} {$where} ORDER BY id DESC LIMIT {$fetch} OFFSET {$offset}"
        );
        $has_more = count( (array) $rows ) > $limit;
        $rows = array_slice( (array) $rows, 0, $limit );
        foreach ( (array) $rows as $r ) {
            $items[] = rmbz_api_admin_app_item(
                $r->id, $r->coupon_code, $r->reward_type, $r->status, $r->expiry_date ?: $r->created_at,
                [ 'Benefício: ' . $r->reward_value, 'Utilização: ' . $r->used_count . '/' . ( $r->usage_limit ?: '∞' ) ],
                null, [ 'toggle' ]
            );
        }
    } elseif ( $module === 'announcements' ) {
        $t = $wpdb->prefix . 'rmbz_app_announcements';
        $where = $like !== ''
            ? $wpdb->prepare( 'WHERE (title LIKE %s OR message LIKE %s)', $like, $like )
            : '';
        $rows = $wpdb->get_results(
            "SELECT id,title,message,level,audience,is_active,created_at
             FROM {$t} {$where} ORDER BY id DESC LIMIT {$fetch} OFFSET {$offset}"
        );
        $has_more = count( (array) $rows ) > $limit;
        $rows = array_slice( (array) $rows, 0, $limit );
        foreach ( (array) $rows as $r ) {
            $items[] = rmbz_api_admin_app_item(
                $r->id, $r->title, wp_strip_all_tags( $r->message ),
                $r->is_active ? 'ativo' : 'inativo', $r->created_at,
                [ 'Nível: ' . $r->level, 'Público: ' . $r->audience ], null, [ 'toggle' ]
            );
        }
    } elseif ( $module === 'system' ) {
        $items = [
            rmbz_api_admin_app_item( 'plugin', 'Plugin 1MZ For Artists', 'Versão instalada', 'ativo', current_time( 'c' ), [ 'Versão: ' . ( defined( 'RMBZ_PLUGIN_VERSION' ) ? RMBZ_PLUGIN_VERSION : '1.5.261' ), 'Fuso horário: ' . wp_timezone_string() ], null, [] ),
            rmbz_api_admin_app_item( 'app', 'App dos Artistas', 'Política de atualização', get_option( 'rmbz_app_emergency_block', 0 ) ? 'bloqueio ativo' : 'normal', current_time( 'c' ), [ 'Recomendada: ' . get_option( 'rmbz_app_latest_version', '—' ), 'Mínima: ' . get_option( 'rmbz_app_min_version', '—' ) ], null, [] ),
        ];
    } else {
        return rmbz_api_error( 'Módulo administrativo inválido.', 404, 'invalid_admin_module' );
    }

    return rmbz_api_ok( [
        'module'   => $module,
        'items'    => array_values( $items ),
        'count'    => count( $items ),
        'offset'   => $offset,
        'has_more' => $has_more,
        'search'   => $search,
        // [1.5.396] Totais exatos de artistas (a lista é paginada — os stats
        // no app não podem contar só a página carregada).
        'artist_stats' => $module === 'artists' ? ( function () use ( $wpdb ) {
            $w = $wpdb->prefix . 'rmbz_wallets';
            $subs = $wpdb->prefix . 'rmbz_subscriptions';
            $not_deleted = "(extra_data NOT LIKE '%\"deleted_at\"%' OR extra_data IS NULL)";
            return [
                'total'     => (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$w} WHERE {$not_deleted}" ),
                'vip'       => (int) $wpdb->get_var( "SELECT COUNT(DISTINCT s.user_telemovel) FROM {$subs} s WHERE s.status='active' AND s.end_date > NOW()" ),
                'suspended' => (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$w} WHERE is_banned=1 AND {$not_deleted}" ),
            ];
        } )() : null,
    ] );
}

/**
 * Ações disponíveis por estado do release — espelha o workflow do site:
 * pendente → approved → delivered → live → takedown_requested → taken_down.
 */
function rmbz_api_admin_app_release_actions( $status ) {
    switch ( (string) $status ) {
        case 'pendente':
        case 'pending':
        case 'submitted':
            return [ 'release_mark_approved', 'release_return', 'release_reject' ];
        case 'approved':
            return [ 'release_mark_delivered', 'release_reject' ];
        case 'delivered':
            return [ 'release_mark_live' ];
        case 'takedown_requested':
            return [ 'release_takedown_confirm', 'release_takedown_cancel' ];
        default:
            return [];
    }
}

/**
 * Detalhe completo de um lançamento para o app admin: metadados, faixas,
 * plataformas, links nas lojas, checklist automática e histórico de estados.
 */
function rmbz_api_admin_app_release_detail( WP_REST_Request $request ) {
    global $wpdb;
    $id = absint( $request['id'] );
    $t  = $wpdb->prefix . 'rmbz_distro_releases';

    $r = $wpdb->get_row( $wpdb->prepare(
        "SELECT r.*, w.user_nome
         FROM {$t} r LEFT JOIN {$wpdb->prefix}rmbz_wallets w ON w.user_telemovel = r.artist_telemovel
         WHERE r.id = %d",
        $id
    ) );
    if ( ! $r ) return rmbz_api_error( 'Lançamento não encontrado.', 404, 'not_found' );

    $tracks = $wpdb->get_results( $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}rmbz_distro_tracks WHERE release_id = %d ORDER BY track_order ASC, id ASC",
        $id
    ) );
    $tracks_out = [];
    foreach ( (array) $tracks as $tk ) {
        $tracks_out[] = [
            'order'    => (int) $tk->track_order,
            'title'    => (string) $tk->track_title,
            'feat'     => (string) ( $tk->feat_artists ?: '' ),
            'isrc'     => (string) ( $tk->isrc_code ?: '' ),
            'explicit' => ! empty( $tk->is_explicit ),
        ];
    }

    // Plataformas/territórios + decisões de monetização
    $platforms_out = null;
    $plat_t = $wpdb->prefix . 'rmbz_distro_release_platforms';
    if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $plat_t ) ) === $plat_t ) {
        $p = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$plat_t} WHERE release_id = %d", $id ) );
        if ( $p ) {
            $dsps = json_decode( (string) ( $p->dsp_list ?: '[]' ), true );
            $platforms_out = [
                'territory_mode' => (string) $p->territory_mode,
                'dsps'           => is_array( $dsps ) ? array_values( array_map( 'strval', $dsps ) ) : [],
                'confirmed'      => ! empty( $p->user_confirmed ),
                'yt_content_id'  => (string) $p->yt_content_id_status,
                'fb_monet'       => (string) $p->fb_monet_status,
                'tt_monet'       => (string) $p->tt_monet_status,
            ];
        }
    }

    // Links nas lojas (exactos ou de pesquisa, modo híbrido do site)
    $links_out = [];
    if ( function_exists( 'rmbz_release_get_links_for_tracker' ) ) {
        $catalog = function_exists( 'rmbz_release_links_dsp_catalog' ) ? rmbz_release_links_dsp_catalog() : [];
        foreach ( (array) rmbz_release_get_links_for_tracker( $r ) as $slug => $info ) {
            if ( empty( $info['url'] ) ) continue;
            $links_out[] = [
                'dsp'   => (string) $slug,
                'label' => (string) ( $catalog[ $slug ]['label'] ?? ucfirst( str_replace( '_', ' ', $slug ) ) ),
                'url'   => (string) $info['url'],
                'exact' => ! empty( $info['is_exact'] ),
            ];
        }
    }

    // Checklist automática de revisão (a mesma do site)
    $checklist_out = [];
    if ( function_exists( 'rmbz_admin_compute_review_checklist' ) ) {
        foreach ( (array) rmbz_admin_compute_review_checklist( $r, $tracks ) as $check ) {
            $checklist_out[] = [
                'label' => (string) ( $check['label'] ?? '' ),
                'pass'  => ! empty( $check['pass'] ),
            ];
        }
    }

    // Histórico de estados
    $history_out = [];
    $hist_t = $wpdb->prefix . 'rmbz_distro_status_history';
    if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $hist_t ) ) === $hist_t ) {
        $hist = $wpdb->get_results( $wpdb->prepare(
            "SELECT from_status,to_status,reason_text,changed_by,created_at FROM {$hist_t}
             WHERE release_id = %d ORDER BY created_at DESC, id DESC LIMIT 25",
            $id
        ) );
        foreach ( (array) $hist as $h ) {
            $history_out[] = [
                'from'       => (string) ( $h->from_status ?: '' ),
                'to'         => (string) $h->to_status,
                'note'       => (string) ( $h->reason_text ?: '' ),
                'changed_by' => (string) ( $h->changed_by ?: '' ),
                'date'       => rmbz_api_admin_app_format_date( $h->created_at ),
            ];
        }
    }

    // Problemas da devolução para correção (quando existem)
    $return_problems = [];
    foreach ( (array) json_decode( (string) ( $r->return_problems ?: '[]' ), true ) as $problem ) {
        if ( is_array( $problem ) ) {
            $return_problems[] = [
                'code' => (string) ( $problem['code'] ?? '' ),
                'note' => (string) ( $problem['note'] ?? '' ),
            ];
        }
    }

    return rmbz_api_ok( [
        'release' => [
            'id'                 => (string) $r->id,
            'title'              => (string) $r->title,
            'type'               => (string) $r->release_type,
            'status'             => (string) $r->status,
            'cover_image'        => $r->cover_image ? ( function_exists( 'rmbz_api_safe_url' ) ? rmbz_api_safe_url( $r->cover_image ) : $r->cover_image ) : '',
            'language'           => (string) ( $r->language ?: '' ),
            'primary_genre'      => (string) ( $r->primary_genre ?: '' ),
            'release_date'       => rmbz_api_admin_app_format_date( $r->release_date ),
            'label_name'         => (string) ( $r->label_name ?: '' ),
            'upc_code'           => (string) ( $r->upc_code ?: '' ),
            'created_at'         => rmbz_api_admin_app_format_date( $r->created_at ),
            'submitted_at'       => rmbz_api_admin_app_format_date( $r->submitted_at ),
            'live_at'            => rmbz_api_admin_app_format_date( $r->live_at ),
            'rejection_reason'   => (string) ( $r->rejection_reason ?: '' ),
            'rejection_note'     => (string) ( $r->rejection_note ?: '' ),
            'takedown_requested' => ! empty( $r->takedown_requested ),
            'takedown_reason'    => (string) ( $r->takedown_reason ?: '' ),
            'return_deadline'    => rmbz_api_admin_app_format_date( $r->return_deadline ),
            'return_count'       => (int) $r->return_count,
        ],
        'artist' => [
            'name'      => (string) ( $r->user_nome ?: $r->artist_telemovel ),
            'telemovel' => (string) $r->artist_telemovel,
        ],
        'tracks'          => $tracks_out,
        'platforms'       => $platforms_out,
        'links'           => $links_out,
        'checklist'       => $checklist_out,
        'history'         => $history_out,
        'return_problems' => $return_problems,
        'actions'         => rmbz_api_admin_app_release_actions( $r->status ),
    ] );
}

function rmbz_api_admin_app_item( $id, $title, $subtitle, $status, $date, $meta = [], $amount = null, $actions = [], $extra = [] ) {
    return [
        'id'       => (string) $id,
        'title'    => (string) $title,
        'subtitle' => (string) $subtitle,
        'status'   => (string) $status,
        'date'     => rmbz_api_admin_app_format_date( $date ),
        'meta'     => array_values( array_filter( array_map( 'strval', $meta ) ) ),
        'amount'   => $amount,
        'actions'  => array_values( $actions ),
        'extra'    => is_array( $extra ) ? $extra : [],
    ];
}

/**
 * Formata datas da API administrativa usando sempre o fuso configurado no
 * WordPress. As datas MySQL do plugin são guardadas em UTC.
 */
function rmbz_api_admin_app_format_date( $date ) {
    if ( $date === null || $date === '' ) return '';

    if ( is_numeric( $date ) ) {
        return wp_date( 'd/m/Y H:i', (int) $date, wp_timezone() );
    }

    $raw = trim( (string) $date );
    if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $raw ) ) {
        $parsed = DateTimeImmutable::createFromFormat( '!Y-m-d', $raw, wp_timezone() );
        return $parsed ? $parsed->format( 'd/m/Y' ) : $raw;
    }

    try {
        $has_timezone = preg_match( '/(?:Z|[+-]\d{2}:?\d{2})$/', $raw );
        $source_timezone = $has_timezone ? null : new DateTimeZone( 'UTC' );
        $parsed = new DateTimeImmutable( $raw, $source_timezone );
        return $parsed->setTimezone( wp_timezone() )->format( 'd/m/Y H:i' );
    } catch ( Exception $e ) {
        return $raw;
    }
}

function rmbz_api_admin_app_date_timestamp( $date ) {
    $raw = trim( (string) $date );
    $local = DateTimeImmutable::createFromFormat( 'd/m/Y H:i', $raw, wp_timezone() );
    if ( $local ) return $local->getTimestamp();
    $timestamp = strtotime( $raw );
    return $timestamp ?: 0;
}

function rmbz_api_admin_app_artist_blocks( array $extra, $telemovel ) {
    $blocks = [];
    if ( ! empty( $extra['block_deposits'] ) ) $blocks[] = 'depósitos';
    if ( ! empty( $extra['block_withdrawals'] ) ) $blocks[] = 'levantamentos';
    if ( ! empty( $extra['block_distribution'] ) ) $blocks[] = 'distribuição';
    if ( ! empty( $extra['block_video_applications'] ) ) $blocks[] = 'videoclipes';
    if ( ! empty( $extra['block_promotions'] ) ) $blocks[] = 'promoções';
    $blacklist = (array) get_option( 'rmbz_referral_blacklist', [] );
    if ( in_array( (string) $telemovel, $blacklist, true ) ) $blocks[] = 'convites';
    return $blocks;
}

function rmbz_api_admin_app_artist_action( $id, $action, WP_REST_Request $request ) {
    global $wpdb;
    $table = $wpdb->prefix . 'rmbz_wallets';
    $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id=%d", $id ) );
    if ( ! $row ) return new WP_Error( 'not_found', 'Artista não encontrado.', [ 'status' => 404 ] );

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

    if ( $action === 'artist_adjust_balance' ) {
        $amount = (float) str_replace( ',', '.', sanitize_text_field( (string) $request->get_param( 'amount' ) ) );
        $reason = sanitize_text_field( (string) $request->get_param( 'reason' ) );
        if ( abs( $amount ) < 0.001 || $reason === '' ) {
            return new WP_Error( 'missing_adjustment_data', 'Indica o valor e o motivo do ajuste.', [ 'status' => 400 ] );
        }
        if ( ! function_exists( 'rmbz_update_wallet_balance' ) ) {
            return new WP_Error( 'wallet_fn_missing', 'Sistema de carteira indisponível.', [ 'status' => 500 ] );
        }
        $type = $amount > 0 ? 'admin_bonus' : 'admin_debit';
        $result = rmbz_update_wallet_balance( $telemovel, $amount, $type, null, 'Ajuste Admin App: ' . $reason );
        if ( empty( $result['success'] ) ) {
            return new WP_Error( 'wallet_update_failed', $result['error'] ?? 'Falha ao ajustar saldo.', [ 'status' => 500 ] );
        }
        return true;
    }

    if ( $action === 'artist_reset_pin' ) {
        $pin = preg_replace( '/\D+/', '', (string) $request->get_param( 'pin' ) );
        if ( strlen( $pin ) !== 4 ) {
            return new WP_Error( 'invalid_pin', 'O novo PIN deve ter exatamente 4 dígitos.', [ 'status' => 400 ] );
        }
        $wpdb->update( $table, [ 'user_pin' => wp_hash_password( $pin ) ], [ 'id' => $id ], [ '%s' ], [ '%d' ] );
        rmbz_api_admin_app_revoke_artist_sessions( $telemovel, 'pin_reset_admin_app' );
        return true;
    }

    if ( $action === 'artist_send_popup' ) {
        $title = sanitize_text_field( (string) $request->get_param( 'title' ) );
        $message = sanitize_textarea_field( (string) $request->get_param( 'message' ) );
        $type = sanitize_key( (string) ( $request->get_param( 'popup_type' ) ?: 'warning' ) );
        if ( $title === '' || $message === '' ) {
            return new WP_Error( 'popup_data_required', 'Indica o título e a mensagem do aviso.', [ 'status' => 400 ] );
        }
        $extra['admin_popup'] = [
            'title'   => $title,
            'message' => $message,
            'type'    => in_array( $type, [ 'info', 'warning', 'danger', 'success' ], true ) ? $type : 'warning',
        ];
        $wpdb->update( $table, [ 'extra_data' => wp_json_encode( $extra ) ], [ 'id' => $id ], [ '%s' ], [ '%d' ] );
        return true;
    }

    if ( $action === 'artist_clear_popup' ) {
        unset( $extra['admin_popup'] );
        $wpdb->update( $table, [ 'extra_data' => wp_json_encode( $extra ) ], [ 'id' => $id ], [ '%s' ], [ '%d' ] );
        return true;
    }

    if ( $action === 'artist_cancel_vip' ) {
        $wpdb->query( $wpdb->prepare(
            "UPDATE {$wpdb->prefix}rmbz_subscriptions SET end_date=%s,status='cancelled' WHERE user_telemovel=%s AND status='active'",
            current_time( 'mysql' ),
            $telemovel
        ) );
        return true;
    }

    if ( $action === 'artist_revoke_sessions' ) {
        rmbz_api_admin_app_revoke_artist_sessions( $telemovel, 'admin_app' );
        return true;
    }

    if ( $action === 'artist_toggle_ban' ) {
        $new_banned = $row->is_banned ? 0 : 1;
        $wpdb->update( $table, [ 'is_banned' => $new_banned ], [ 'id' => $id ], [ '%d' ], [ '%d' ] );
        if ( $new_banned ) rmbz_api_admin_app_revoke_artist_sessions( $telemovel, 'admin_ban' );
        return true;
    }

    $block_map = [
        'artist_toggle_deposits'           => 'block_deposits',
        'artist_toggle_withdrawals'        => 'block_withdrawals',
        'artist_toggle_distribution'       => 'block_distribution',
        'artist_toggle_video_applications' => 'block_video_applications',
        'artist_toggle_promotions'         => 'block_promotions',
    ];
    if ( isset( $block_map[ $action ] ) ) {
        $key = $block_map[ $action ];
        $extra[ $key ] = empty( $extra[ $key ] ) ? 1 : 0;
        $wpdb->update( $table, [ 'extra_data' => wp_json_encode( $extra ) ], [ 'id' => $id ], [ '%s' ], [ '%d' ] );
        return true;
    }

    if ( $action === 'artist_toggle_referrals' ) {
        $blacklist = (array) get_option( 'rmbz_referral_blacklist', [] );
        if ( in_array( $telemovel, $blacklist, true ) ) {
            $blacklist = array_values( array_filter( $blacklist, function ( $v ) use ( $telemovel ) {
                return (string) $v !== $telemovel;
            } ) );
        } else {
            $blacklist[] = $telemovel;
        }
        update_option( 'rmbz_referral_blacklist', $blacklist );
        return true;
    }

    return new WP_Error( 'invalid_artist_action', 'Ação de artista inválida.', [ 'status' => 400 ] );
}

function rmbz_api_admin_app_revoke_artist_sessions( $telemovel, $reason = 'admin_app' ) {
    global $wpdb;
    $tokens = $wpdb->prefix . 'rmbz_api_refresh_tokens';
    if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $tokens ) ) === $tokens ) {
        $wpdb->query( $wpdb->prepare(
            "UPDATE {$tokens} SET revoked_at=%s,revoke_reason=%s WHERE user_telemovel=%s AND revoked_at IS NULL",
            current_time( 'mysql', 1 ),
            $reason,
            $telemovel
        ) );
    }
    if ( function_exists( 'rmbz_app_token_mark_revoked' ) ) {
        rmbz_app_token_mark_revoked( $telemovel, $reason );
    }
}

function rmbz_api_admin_app_module_action( WP_REST_Request $request ) {
    global $wpdb;
    $module = sanitize_key( (string) $request['module'] );
    $id     = absint( $request['id'] );
    $action = sanitize_key( (string) $request->get_param( 'action' ) );
    $reason = sanitize_textarea_field( (string) $request->get_param( 'reason' ) );
    $admin_id = (int) $request->get_param( 'rmbz_admin_user_id' );

    if ( $module === 'content' ) {
        $map = [ 'approve' => 'aprovado', 'correction' => 'correcao', 'reject' => 'rejeitado' ];
        if ( ! isset( $map[ $action ] ) ) return rmbz_api_error( 'Ação inválida.', 400, 'invalid_action' );
        if ( in_array( $action, [ 'correction', 'reject' ], true ) && $reason === '' ) {
            return rmbz_api_error( 'Indica o motivo para o artista.', 400, 'reason_required' );
        }
        $t = $wpdb->prefix . 'rmbz_artist_submissions';
        $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$t} WHERE id=%d", $id ) );
        if ( ! $row ) return rmbz_api_error( 'Conteúdo não encontrado.', 404, 'not_found' );
        $wpdb->update( $t, [ 'review_status' => $map[ $action ], 'rejection_reason' => $reason ], [ 'id' => $id ], [ '%s', '%s' ], [ '%d' ] );
        if ( function_exists( 'rmbz_notify_create' ) ) {
            rmbz_notify_create( [
                'type' => 'submission_status_change',
                'severity' => $action === 'approve' ? 'success' : ( $action === 'reject' ? 'danger' : 'warning' ),
                'title' => 'Estado atualizado: ' . $row->music_title,
                'body' => $reason ?: 'A tua submissão foi aprovada pela equipa.',
                'action_url' => "1mzapp://submissions/{$id}",
                'target_telemovel' => $row->artist_telemovel,
                'dedup_key' => 'admin_app_content_' . $id . '_' . $map[ $action ] . '_' . time(),
                'created_by' => 'admin_app',
            ] );
        }
    } elseif ( $module === 'coupons' && $action === 'toggle' ) {
        $t = $wpdb->prefix . 'rmbz_coupons';
        $current = $wpdb->get_var( $wpdb->prepare( "SELECT status FROM {$t} WHERE id=%d", $id ) );
        if ( $current === null ) return rmbz_api_error( 'Cupão não encontrado.', 404, 'not_found' );
        $wpdb->update( $t, [ 'status' => $current === 'ativo' ? 'inativo' : 'ativo' ], [ 'id' => $id ], [ '%s' ], [ '%d' ] );
    } elseif ( $module === 'announcements' && $action === 'toggle' ) {
        $t = $wpdb->prefix . 'rmbz_app_announcements';
        $current = $wpdb->get_var( $wpdb->prepare( "SELECT is_active FROM {$t} WHERE id=%d", $id ) );
        if ( $current === null ) return rmbz_api_error( 'Aviso não encontrado.', 404, 'not_found' );
        $wpdb->update( $t, [ 'is_active' => $current ? 0 : 1 ], [ 'id' => $id ], [ '%d' ], [ '%d' ] );
    } elseif ( $module === 'distribution' ) {
        if ( ! function_exists( 'rmbz_distro_change_status' ) ) {
            return rmbz_api_error( 'Módulo de distribuição indisponível.', 500, 'distro_system_missing' );
        }
        $release_status = $wpdb->get_var( $wpdb->prepare(
            "SELECT status FROM {$wpdb->prefix}rmbz_distro_releases WHERE id=%d", $id
        ) );
        if ( $release_status === null ) {
            return rmbz_api_error( 'Lançamento não encontrado.', 404, 'not_found' );
        }

        if ( $action === 'release_mark_approved' ) {
            // pendente → approved: atribui UPC/ISRC e segue o workflow do site.
            if ( ! in_array( $release_status, [ 'pendente', 'pending', 'submitted' ], true ) ) {
                return rmbz_api_error( 'Este lançamento já não está pendente.', 409, 'invalid_state' );
            }
            if ( ! function_exists( 'rmbz_release_workflow_transition' ) ) {
                return rmbz_api_error( 'Workflow de releases indisponível.', 500, 'workflow_missing' );
            }
            if ( function_exists( 'rmbz_release_workflow_assign_codes' ) ) {
                rmbz_release_workflow_assign_codes( $id );
            }
            rmbz_release_workflow_transition( $id, 'approved', 'Aprovado via app administrativa' );
        } elseif ( $action === 'release_mark_delivered' ) {
            if ( $release_status !== 'approved' ) {
                return rmbz_api_error( 'O lançamento tem de estar aprovado primeiro.', 409, 'invalid_state' );
            }
            rmbz_release_workflow_transition( $id, 'delivered', 'Entregue aos DSPs via app administrativa' );
        } elseif ( $action === 'release_mark_live' ) {
            if ( $release_status !== 'delivered' ) {
                return rmbz_api_error( 'O lançamento tem de estar entregue aos DSPs primeiro.', 409, 'invalid_state' );
            }
            rmbz_release_workflow_transition( $id, 'live', 'Confirmado live nas lojas via app administrativa' );
        } elseif ( $action === 'release_takedown_confirm' ) {
            if ( $release_status !== 'takedown_requested' ) {
                return rmbz_api_error( 'Não existe pedido de takedown ativo.', 409, 'invalid_state' );
            }
            $rel_t = $wpdb->prefix . 'rmbz_distro_releases';
            $wpdb->update( $rel_t, [ 'status' => 'taken_down' ], [ 'id' => $id ], [ '%s' ], [ '%d' ] );
            if ( function_exists( 'rmbz_admin_log_status_change' ) ) {
                rmbz_admin_log_status_change( $id, 'takedown_requested', 'taken_down', 'Takedown finalizado via app administrativa' );
            }
            $owner = $wpdb->get_row( $wpdb->prepare( "SELECT artist_telemovel,title FROM {$rel_t} WHERE id=%d", $id ) );
            if ( $owner && function_exists( 'rmbz_add_notification' ) ) {
                rmbz_add_notification( $owner->artist_telemovel, sprintf( '🗑 O teu lançamento "%s" foi removido das lojas conforme pediste.', $owner->title ?: 'Lançamento' ) );
            }
        } elseif ( $action === 'release_takedown_cancel' ) {
            if ( $release_status !== 'takedown_requested' ) {
                return rmbz_api_error( 'Não existe pedido de takedown ativo.', 409, 'invalid_state' );
            }
            $rel_t = $wpdb->prefix . 'rmbz_distro_releases';
            $wpdb->update(
                $rel_t,
                [ 'status' => 'live', 'takedown_requested' => 0, 'takedown_reason' => null ],
                [ 'id' => $id ], [ '%s', '%d', '%s' ], [ '%d' ]
            );
            if ( function_exists( 'rmbz_admin_log_status_change' ) ) {
                rmbz_admin_log_status_change( $id, 'takedown_requested', 'live', 'Pedido de takedown cancelado via app administrativa' );
            }
            $owner = $wpdb->get_row( $wpdb->prepare( "SELECT artist_telemovel,title FROM {$rel_t} WHERE id=%d", $id ) );
            if ( $owner && function_exists( 'rmbz_add_notification' ) ) {
                rmbz_add_notification( $owner->artist_telemovel, sprintf( '⟲ O teu pedido de takedown para "%s" foi cancelado pelo admin. O lançamento continua nas lojas.', $owner->title ?: 'Lançamento' ) );
            }
        } elseif ( $action === 'release_approve' ) {
            // Compat: versões antigas do app aprovavam direto para live.
            $ok = rmbz_distro_change_status( $id, 'live', 'admin_app' );
            if ( ! $ok ) return rmbz_api_error( 'Falha ao aprovar o lançamento.', 500, 'approve_failed' );
        } elseif ( $action === 'release_reject' ) {
            if ( $reason === '' ) return rmbz_api_error( 'Indica o motivo da rejeição.', 400, 'reason_required' );
            $ok = rmbz_distro_change_status( $id, 'rejected', 'admin_app', [
                'reason_code' => 'other',
                'reason_text' => $reason,
            ] );
            if ( ! $ok ) return rmbz_api_error( 'Falha ao rejeitar o lançamento.', 500, 'reject_failed' );
        } elseif ( $action === 'release_return' ) {
            if ( ! function_exists( 'rmbz_distro_return_release' ) ) {
                return rmbz_api_error( 'Módulo de devolução indisponível.', 500, 'return_system_missing' );
            }
            $raw_reason = (string) $request->get_param( 'reason' );
            $data = json_decode( $raw_reason, true );
            $note = '';
            $problems = [];
            $deadline = 7;
            if ( is_array( $data ) ) {
                $note = sanitize_textarea_field( $data['note'] ?? '' );
                $problems = (array) ( $data['problems'] ?? [] );
                $deadline = isset( $data['deadline_days'] ) ? max(1, min(30, (int) $data['deadline_days'])) : 7;
            } else {
                $note = sanitize_textarea_field( $raw_reason );
                $problems = [ [ 'code' => 'other', 'note' => 'Outro problema' ] ];
            }
            $result = rmbz_distro_return_release( $id, $problems, $note, $deadline );
            if ( is_wp_error( $result ) ) {
                return rmbz_api_error( $result->get_error_message(), 500, 'return_failed' );
            }
        } else {
            return rmbz_api_error( 'Ação de distribuição inválida.', 400, 'invalid_action' );
        }
    } elseif ( $module === 'finance' ) {
        $result = rmbz_api_admin_app_finance_action( $id, $action, $reason );
        if ( is_wp_error( $result ) ) return rmbz_api_error( $result );
    } elseif ( $module === 'projects' ) {
        $result = rmbz_api_admin_app_project_action( $id, $action, $reason );
        if ( is_wp_error( $result ) ) return rmbz_api_error( $result );
    } elseif ( $module === 'artists' ) {
        $result = rmbz_api_admin_app_artist_action( $id, $action, $request );
        if ( is_wp_error( $result ) ) return rmbz_api_error( $result );
    } else {
        return rmbz_api_error( 'Esta ação ainda não está disponível neste módulo.', 400, 'action_not_available' );
    }

    error_log( sprintf( '[1MZ Admin App] admin_id=%d module=%s id=%d action=%s', $admin_id, $module, $id, $action ) );
    return rmbz_api_ok( [ 'updated' => true, 'message' => 'Operação concluída com sucesso.' ] );
}

function rmbz_api_admin_app_project_detail( WP_REST_Request $request ) {
    global $wpdb;
    $id = absint( $request['id'] );
    $p = $wpdb->get_row( $wpdb->prepare(
        "SELECT p.*, w.user_nome, w.user_email, w.user_provincia, w.balance, w.is_banned, w.is_creator
         FROM {$wpdb->prefix}rmbz_artist_projects p
         LEFT JOIN {$wpdb->prefix}rmbz_wallets w ON p.artist_telemovel=w.user_telemovel
         WHERE p.id=%d",
        $id
    ) );
    if ( ! $p ) return rmbz_api_error( 'Projeto nao encontrado.', 404, 'not_found' );

    $extra = ! empty( $p->extra_data ) ? json_decode( $p->extra_data, true ) : [];
    if ( ! is_array( $extra ) ) $extra = [];
    $budget = ! empty( $p->scenario_details ) ? json_decode( $p->scenario_details, true ) : [];
    if ( ! is_array( $budget ) ) $budget = [];
    $proposed = [];
    foreach ( (array) ( $extra['proposed_budgets'] ?? [] ) as $location => $data ) {
        $proposed[] = [
            'location' => (string) $location,
            'total'    => (float) ( $data['total'] ?? 0 ),
            'items'    => array_values( is_array( $data['items'] ?? null ) ? $data['items'] : [] ),
        ];
    }

    // [1.6.393] Multa por atraso deste projeto (para o app admin mostrar/gerir).
    $ovf = function_exists( 'rmbz_project_overdue_info' )
        ? rmbz_project_overdue_info( $p->artist_telemovel ) : [ 'overdue' => false ];
    $ovf_applies = ! empty( $ovf['overdue'] ) && (int) ( $ovf['project_id'] ?? 0 ) === (int) $p->id
        && ! empty( $ovf['enabled'] ) && (float) ( $ovf['amount'] ?? 0 ) > 0;

    return rmbz_api_ok( [
        'project' => [
            'id'               => (string) $p->id,
            'title'            => (string) $p->project_title,
            'artist_telemovel' => (string) $p->artist_telemovel,
            'status'           => (string) $p->production_status,
            'notifications_muted' => ! empty( $extra['notifications_muted'] ),
            'scenario_type'    => (string) $p->scenario_type,
            'scenario_cost'    => (float) $p->scenario_cost,
            'amount_paid'      => (float) $p->amount_paid,
            'is_paid'          => ! empty( $p->is_paid ),
            'music_producer'   => (string) $p->music_producer,
            // [1.5.398] Participação (feat) — tracking apenas.
            'participacao'     => ! empty( $extra['feat']['name'] )
                ? $extra['feat']['name'] . ( ( ! empty( $extra['feat']['has_account'] ) && ! empty( $extra['feat']['tel'] ) )
                    ? ' · conta: ' . $extra['feat']['tel'] : ' · sem conta 1MZ' )
                : '',
            'project_pitch'    => (string) $p->project_pitch,
            'production_pitch' => (string) ( $extra['production_pitch'] ?? '' ),
            'project_link_opt' => (string) $p->project_link_opt,
            'script_content'   => (string) $p->script_content,
            'final_video_url'  => (string) $p->final_video_url,
            'youtube_url'      => (string) $p->youtube_url,
            'release_date'     => (string) $p->release_date,
            'contract_date'    => rmbz_api_admin_app_format_date( $p->contract_date ),
        ],
        'artist' => [
            'name'      => (string) ( $p->user_nome ?: $p->artist_telemovel ),
            'telemovel' => (string) $p->artist_telemovel,
            'email'     => (string) $p->user_email,
            'province'  => (string) $p->user_provincia,
            'balance'   => (float) $p->balance,
            'is_banned' => ! empty( $p->is_banned ),
        ],
        'payment' => ( function () use ( $p ) {
            $pay = [
                'cost'        => (float) $p->scenario_cost,
                'amount_paid' => (float) $p->amount_paid,
                'is_paid'     => ! empty( $p->is_paid ),
            ];
            // [1.5.410] Controlo financeiro: taxa de vistoria da equipa.
            if ( function_exists( 'rmbz_team_vistoria_status' ) ) {
                $vs = rmbz_team_vistoria_status( $p );
                if ( $vs['total'] > 0 ) {
                    $pay['taxa_vistoria']     = number_format( $vs['total'], 2, ',', '.' ) . ' MT';
                    $pay['vistoria_em_falta'] = number_format( $vs['left'], 2, ',', '.' ) . ' MT';
                    if ( ! empty( $vs['blocked'] ) ) {
                        $pay['estado_vistoria'] = ! empty( $vs['editor'] )
                            ? '🔒 BLOQUEADO — ficheiros NÃO entregues ao EDITOR EXTERNO até a taxa ser paga'
                            : '🔒 BLOQUEADO — vistoria por pagar (sem edição/entrega)';
                    } elseif ( $vs['left'] <= 0 ) {
                        $pay['estado_vistoria'] = '✅ Vistoria paga';
                    }
                }
            }
            return $pay;
        } )(),
        'fine' => $ovf_applies ? [
            'estado'  => ! empty( $ovf['frozen'] ) ? 'Multa pendente (congelada)' : 'Multa a acumular',
            'valor'   => number_format( (float) $ovf['amount'], 2, ',', '.' ) . ' MT',
            'detalhe' => (int) ( $ovf['units'] ?? 0 ) . ' '
                . ( ( ( $ovf['unit'] ?? 'day' ) === 'hour' ) ? 'hora(s)' : 'dia(s)' ) . ' × '
                . number_format( (float) ( $ovf['rate'] ?? 0 ), 2, ',', '.' ) . ' MT/'
                . ( ( ( $ovf['unit'] ?? 'day' ) === 'hour' ) ? 'hora' : 'dia' ),
            // Valor numérico cru para o cliente (botão "Pagar/Marcar paga").
            'amount_raw' => number_format( (float) $ovf['amount'], 2, '.', '' ),
            'frozen'     => ! empty( $ovf['frozen'] ),
        ] : null,
        // [1.5.398] Demo enviada pelo artista — o app admin pode ouvir.
        'demo_mp3_url' => $p->demo_mp3_url
            ? ( function_exists( 'rmbz_api_safe_url' ) ? rmbz_api_safe_url( $p->demo_mp3_url ) : (string) $p->demo_mp3_url )
            : '',
        // [1.5.398] Transferência de projeto — estado + sugestão do artista.
        'transfer' => ( ! empty( $extra['transfer_request'] ) && is_array( $extra['transfer_request'] ) ) ? [
            'status'         => (string) ( $extra['transfer_request']['status'] ?? '' ),
            'reason'         => (string) ( $extra['transfer_request']['reason'] ?? '' ),
            'requested_at'   => (string) ( $extra['transfer_request']['requested_at'] ?? '' ),
            'accepted_at'    => (string) ( $extra['transfer_request']['accepted_at'] ?? '' ),
            'suggested_name' => (string) ( $extra['transfer_request']['suggested_name'] ?? '' ),
            'suggested_tel'  => (string) ( $extra['transfer_request']['suggested_tel'] ?? '' ),
        ] : null,
        'contract' => [
            'url'        => (string) ( $extra['contract_url'] ?? '' ),
            'accepted'   => ! empty( $extra['contract_accepted'] ),
            'rejected'   => ! empty( $extra['contract_rejected'] ),
            'real_name'  => (string) ( $extra['contract_real_name'] ?? '' ),
            'kyc_status' => (string) ( $extra['kyc_status'] ?? '' ),
            'kyc_photo'  => (string) ( $extra['contract_kyc_photo'] ?? '' ),
            'kyc_reason' => (string) ( $extra['kyc_rejection_reason'] ?? '' ),
            'deadline'   => ! empty( $extra['contract_deadline'] ) ? wp_date( 'd/m/Y H:i', (int) $extra['contract_deadline'] ) : '',
        ],
        // [1.5.404] GRAVAÇÃO AO VIVO — estado para o card do app admin
        // (Iniciar Gravação, cronómetro, presenças, finalizar dia).
        'filming_live' => ( function () use ( $p, $extra ) {
            $sessions = ( ! empty( $extra['filming_sessions'] ) && is_array( $extra['filming_sessions'] ) ) ? $extra['filming_sessions'] : [];
            // [1.5.410] Participantes com categoria (label + ícone resolvidos).
            $fmt_people = function ( $list ) {
                return array_map( function ( $pp ) {
                    $cat = (string) ( $pp['category'] ?? 'outro' );
                    $meta = function_exists( 'rmbz_fs_category_meta' ) ? rmbz_fs_category_meta( $cat ) : [ 'label' => 'Outro', 'icon' => '✨' ];
                    return [
                        'name'      => (string) ( $pp['name'] ?? '' ),
                        'category'  => $cat,
                        'cat_label' => (string) $meta['label'],
                        'cat_icon'  => (string) $meta['icon'],
                        'role'      => (string) ( $pp['role'] ?? '' ),
                        'photo'     => (string) ( $pp['photo'] ?? '' ),
                    ];
                }, (array) $list );
            };
            $active = null;
            $history = [];
            foreach ( $sessions as $s ) {
                if ( ! is_array( $s ) ) continue;
                if ( empty( $s['ended_at'] ) ) {
                    $active = [
                        'day'          => (int) ( $s['day'] ?? 0 ),
                        'started_at'   => (string) ( $s['started_at'] ?? '' ),
                        'started_ts'   => (int) ( $s['started_ts'] ?? 0 ),
                        'participants' => $fmt_people( $s['participants'] ?? [] ),
                    ];
                } else {
                    $history[] = [
                        'day'          => (int) ( $s['day'] ?? 0 ),
                        'date'         => (string) ( $s['date'] ?? '' ),
                        'duration_min' => (int) ( $s['duration_min'] ?? 0 ),
                        'people'       => count( (array) ( $s['participants'] ?? [] ) ),
                        // [1.5.410] Lista completa (árvore por categoria + edição no app).
                        'participants' => $fmt_people( $s['participants'] ?? [] ),
                        'edits'        => (int) ( $s['participants_edits'] ?? 0 ),
                        'edited_at'    => (string) ( $s['participants_edited_at'] ?? '' ),
                        'edited_by'    => (string) ( $s['participants_edited_by'] ?? '' ),
                        'result'       => (string) ( $s['result'] ?? '' ),
                        'percent_left' => (int) ( $s['percent_left'] ?? 0 ),
                        'next_date'    => (string) ( $s['next_date'] ?? '' ),
                    ];
                }
            }
            // Catálogo de categorias para o registo/edição no app.
            $cat_list = [];
            if ( function_exists( 'rmbz_fs_categories' ) ) {
                foreach ( rmbz_fs_categories() as $ck => $cm ) {
                    $cat_list[] = [ 'key' => $ck, 'label' => (string) $cm['label'], 'icon' => (string) $cm['icon'] ];
                }
            }
            $start = (string) ( $extra['filming_start'] ?? '' );
            $end   = (string) ( $extra['filming_end'] ?? '' );
            $today = current_time( 'Y-m-d' );
            // [1.5.407] "Iniciar Gravação" só DENTRO da janela de datas
            // (início..fim), ou quando o projeto já está na fase FILMAGEM
            // (dias de continuação). Fora disso, nada aparece.
            $in_window = $start !== '' && $today >= $start && ( $end === '' || $today <= $end );
            $date_reached = $in_window || (string) $p->production_status === 'filming';
            // [1.5.408] Equipa essencial completa? Sem ela o botão não aparece.
            $team_ok = true;
            $team_missing = [];
            if ( function_exists( 'rmbz_team_validate_project' ) ) {
                $team_diag = rmbz_team_validate_project( $extra );
                $team_ok = ! empty( $team_diag['ok'] );
                foreach ( (array) ( $team_diag['missing'] ?? [] ) as $miss ) {
                    $team_missing[] = (string) ( $miss['icon'] ?? '' ) . ' ' . (string) ( $miss['label'] ?? '' );
                }
            }
            return [
                'has_dates'    => $start !== '' || $end !== '',
                'start_date'   => $start,
                'date_reached' => $date_reached,
                'server_now'   => (int) current_time( 'timestamp', true ),
                'team_ok'      => $team_ok,
                'team_missing' => $team_missing,
                'can_start'    => in_array( (string) $p->production_status, [ 'pre-production', 'filming' ], true )
                                  && ( $start !== '' || $end !== '' )
                                  && $active === null
                                  && $date_reached
                                  && $team_ok,
                'active'       => $active,
                'sessions'     => $history,
                'categories'   => $cat_list,
                'percent_left' => isset( $extra['filming_progress']['percent_left'] ) ? (int) $extra['filming_progress']['percent_left'] : -1,
            ];
        } )(),
        // [1.5.407] EQUIPA DO VIDEOCLIPE — banco de pessoas + atribuídos, para
        // o app admin selecionar quem trabalha no projeto.
        'team_info' => ( function () use ( $extra ) {
            if ( ! function_exists( 'rmbz_team_get_all' ) ) return null;
            $assigned = ( isset( $extra['team_member_ids'] ) && is_array( $extra['team_member_ids'] ) )
                ? array_map( 'intval', $extra['team_member_ids'] ) : [];
            $req = ( isset( $extra['team_requested_by'] ) && is_array( $extra['team_requested_by'] ) )
                ? $extra['team_requested_by'] : [];
            $proles = ( isset( $extra['team_project_roles'] ) && is_array( $extra['team_project_roles'] ) )
                ? $extra['team_project_roles'] : [];
            $members = [];
            foreach ( (array) rmbz_team_get_all( true ) as $m ) {
                // [1.5.408] Cargos da pessoa (lista) + os escolhidos p/ ESTE projeto.
                $all_roles = array_values( array_filter( array_map( 'trim', explode( '·', (string) ( $m->role ?? '' ) ) ) ) );
                $sel_roles = isset( $proles[ (int) $m->id ] ) && is_array( $proles[ (int) $m->id ] )
                    ? array_values( $proles[ (int) $m->id ] )
                    : [];
                $members[] = [
                    'id'            => (int) $m->id,
                    'name'          => (string) $m->name,
                    'role'          => (string) ( $m->role ?? '' ),
                    'roles'         => $all_roles,
                    'project_roles' => $sel_roles,
                    'photo'         => (string) ( $m->photo_url ?? '' ),
                    'external'      => isset( $m->origin ) && $m->origin === 'externo',
                    'assigned'      => in_array( (int) $m->id, $assigned, true ),
                    'requested_by'  => (string) ( $req[ (int) $m->id ] ?? 'mz' ),
                ];
            }
            return [ 'members' => $members ];
        } )(),
        // [1.5.411] ENTREGA AO EDITOR EXTERNO — passo pós-pagamento da taxa:
        // registar ficheiros (nº/GB/qualidade) e marcar a entrega ao editor.
        'editor_delivery' => ( function () use ( $p, $extra ) {
            if ( ! function_exists( 'rmbz_editor_delivery_get' ) ) return null;
            $ed    = rmbz_editor_delivery_get( $extra );
            $avail = function_exists( 'rmbz_editor_delivery_available' ) && rmbz_editor_delivery_available( $p );
            if ( ! $avail && $ed['files'] <= 0 ) return null;
            return [
                'available'    => $avail,
                'files'        => (int) $ed['files'],
                'size_gb'      => (float) $ed['size_gb'],
                'quality'      => (string) $ed['quality'],
                'info_at'      => (string) $ed['info_at'],
                'info_by'      => (string) $ed['info_by'],
                'delivered'    => ! empty( $ed['delivered'] ),
                'delivered_at' => (string) $ed['delivered_at'],
                'delivered_by' => (string) $ed['delivered_by'],
            ];
        } )(),
        // [1.5.403] PARTICIPAÇÃO (feat) — estado completo para o card do app
        // admin (definir/alterar/remover; forçada ou com aceitação).
        'feat_info' => [
            'name'        => (string) ( $extra['feat']['name'] ?? '' ),
            'has_account' => ! empty( $extra['feat']['has_account'] ),
            'tel'         => (string) ( $extra['feat']['tel'] ?? '' ),
            'consent'     => (string) ( $extra['feat']['consent']['status'] ?? '' ),
            'forced'      => ! empty( $extra['feat']['consent']['forced_by_admin'] ),
        ],
        // [1.5.402] GERADOR DE CONTRATO no app admin: valores pré-preenchidos
        // do modal + rascunho pendente (fluxo gerar → prever → aprovar/rejeitar).
        'contract_gen' => ( function () use ( $p, $extra ) {
            $levels = [ 'basic' => 'BÁSICO', 'professional' => 'PROFISSIONAL', 'cinema' => 'CINEMA' ];
            $feat = '';
            if ( ! empty( $extra['feat']['name'] ) ) {
                $feat = $extra['feat']['name'];
                if ( ! empty( $extra['feat']['has_account'] ) && ! empty( $extra['feat']['tel'] ) ) {
                    $feat .= ' (conta ' . $extra['feat']['tel'] . ')';
                }
            }
            return [
                'available' => function_exists( 'rmbz_contract_generate_for_project' ),
                'defaults'  => [
                    'artist_name' => (string) ( $p->user_nome ?: ( $extra['artist_name'] ?? $p->artist_telemovel ) ),
                    'artist_tel'  => (string) $p->artist_telemovel,
                    'music'       => (string) $p->project_title,
                    'level'       => $levels[ (string) $p->scenario_type ] ?? strtoupper( (string) $p->scenario_type ),
                    'value'       => (float) $p->scenario_cost > 0 ? number_format( (float) $p->scenario_cost, 2, ',', '.' ) : '',
                    'feat'        => $feat,
                    'city'        => 'Maputo',
                    'bi_number'   => '',
                    'signer_name' => '',
                ],
                'draft' => ( ! empty( $extra['contract_draft']['url'] ) ) ? [
                    'url'        => (string) $extra['contract_draft']['url'],
                    'created_at' => (string) ( $extra['contract_draft']['created_at'] ?? '' ),
                ] : null,
            ];
        } )(),
        'filming' => [
            'start'     => (string) ( $extra['filming_start'] ?? '' ),
            'end'       => (string) ( $extra['filming_end'] ?? '' ),
            'confirmed' => ! empty( $extra['filming_confirmed_by_artist'] ),
            'proposal'  => $extra['filming_proposed_by_artist'] ?? null,
        ],
        'budget' => [
            'selected_location' => (string) ( $extra['budget_accepted_location'] ?? '' ),
            'items'             => array_values( $budget ),
            'proposed'          => $proposed,
        ],
        'change_log' => function_exists( 'rmbz_pchat_get_change_log' ) ? rmbz_pchat_get_change_log( $extra ) : [],
        'recommendations' => function_exists( 'rmbz_pchat_get_recommendations' ) ? rmbz_pchat_get_recommendations( $p, $extra ) : [],
    ] );
}

function rmbz_api_admin_app_project_chat( WP_REST_Request $request ) {
    global $wpdb;
    $id = absint( $request['id'] );
    if ( ! function_exists( 'rmbz_pchat_get_messages' ) || ! rmbz_pchat_check_project_exists( $id ) ) {
        return rmbz_api_error( 'Chat do projeto indisponivel.', 404, 'chat_not_available' );
    }
    $messages = rmbz_pchat_get_messages( $id, 0 );
    $wpdb->update(
        $wpdb->prefix . 'rmbz_project_chat_messages',
        [ 'read_by_admin' => 1 ],
        [ 'project_id' => $id, 'read_by_admin' => 0 ],
        [ '%d' ], [ '%d', '%d' ]
    );
    return rmbz_api_ok( [
        'messages' => $messages,
        'artist_can_attach' => (bool) rmbz_pchat_get_artist_can_attach( $id ),
    ] );
}

/**
 * [1.6.394] Transações do artista para o app admin: depósitos, levantamentos,
 * extrato da carteira e histórico de envios. Identificado pelo id da carteira.
 */
function rmbz_api_admin_app_artist_transactions( WP_REST_Request $request ) {
    global $wpdb;
    $wid = absint( $request['id'] );

    $w = $wpdb->get_row( $wpdb->prepare(
        "SELECT id, user_nome, user_telemovel, balance FROM {$wpdb->prefix}rmbz_wallets WHERE id=%d", $wid
    ) );
    if ( ! $w ) return rmbz_api_error( 'Artista não encontrado.', 404, 'not_found' );
    $tel = (string) $w->user_telemovel;

    $fmt = function ( $d ) { return $d ? rmbz_api_admin_app_format_date( $d ) : ''; };
    $money = function ( $v ) { return number_format( (float) $v, 2, ',', '.' ) . ' MT'; };

    // Pedidos de Depósito
    $deposits = [];
    foreach ( (array) $wpdb->get_results( $wpdb->prepare(
        "SELECT id, amount, status, payment_method, transaction_id, sender_phone, request_date, processed_date
           FROM {$wpdb->prefix}rmbz_deposit_requests WHERE user_telemovel=%s ORDER BY id DESC LIMIT 40", $tel
    ) ) as $r ) {
        $deposits[] = [
            'id'      => (string) $r->id,
            'amount'  => $money( $r->amount ),
            'status'  => (string) $r->status,
            'method'  => (string) $r->payment_method,
            'ref'     => (string) ( $r->transaction_id ?: $r->sender_phone ),
            'date'    => $fmt( $r->request_date ),
        ];
    }

    // Pedidos de Levantamento
    $withdrawals = [];
    foreach ( (array) $wpdb->get_results( $wpdb->prepare(
        "SELECT id, amount, status, payment_method, payment_details, request_date, processed_date
           FROM {$wpdb->prefix}rmbz_withdrawal_requests WHERE user_telemovel=%s ORDER BY id DESC LIMIT 40", $tel
    ) ) as $r ) {
        $withdrawals[] = [
            'id'      => (string) $r->id,
            'amount'  => $money( $r->amount ),
            'status'  => (string) $r->status,
            'method'  => (string) $r->payment_method,
            'ref'     => (string) $r->payment_details,
            'date'    => $fmt( $r->request_date ),
        ];
    }

    // Extrato da Carteira
    $wallet = [];
    foreach ( (array) $wpdb->get_results( $wpdb->prepare(
        "SELECT id, amount, type, details, transaction_date
           FROM {$wpdb->prefix}rmbz_wallet_transactions WHERE user_telemovel=%s ORDER BY id DESC LIMIT 60", $tel
    ) ) as $r ) {
        $wallet[] = [
            'id'      => (string) $r->id,
            'amount'  => ( (float) $r->amount >= 0 ? '+' : '' ) . $money( $r->amount ),
            'type'    => (string) $r->type,
            'details' => (string) $r->details,
            'date'    => $fmt( $r->transaction_date ),
        ];
    }

    // Histórico de Envios (submissões)
    $submissions = [];
    foreach ( (array) $wpdb->get_results( $wpdb->prepare(
        "SELECT id, music_title, scenario_type, review_status, music_genre, submission_date
           FROM {$wpdb->prefix}rmbz_artist_submissions WHERE artist_telemovel=%s ORDER BY id DESC LIMIT 40", $tel
    ) ) as $r ) {
        $submissions[] = [
            'id'     => (string) $r->id,
            'title'  => (string) $r->music_title,
            'type'   => (string) $r->scenario_type,
            'status' => (string) $r->review_status,
            'genre'  => (string) $r->music_genre,
            'date'   => $fmt( $r->submission_date ),
        ];
    }

    return rmbz_api_ok( [
        'artist'      => [
            'name'      => (string) ( $w->user_nome ?: $tel ),
            'telemovel' => $tel,
            'balance'   => $money( $w->balance ),
        ],
        'deposits'    => $deposits,
        'withdrawals' => $withdrawals,
        'wallet'      => $wallet,
        'submissions' => $submissions,
    ] );
}

function rmbz_api_admin_app_project_chat_send( WP_REST_Request $request ) {
    $id = absint( $request['id'] );
    $message = sanitize_textarea_field( (string) $request->get_param( 'message' ) );
    if ( $message === '' ) return rmbz_api_error( 'Escreve uma mensagem.', 400, 'message_required' );
    if ( ! function_exists( 'rmbz_pchat_insert_message' ) || ! rmbz_pchat_check_project_exists( $id ) ) {
        return rmbz_api_error( 'Chat do projeto indisponivel.', 404, 'chat_not_available' );
    }
    if ( function_exists( 'rmbz_pchat_is_chat_locked' ) && rmbz_pchat_is_chat_locked( $id ) ) {
        return rmbz_api_error( 'Chat desativado — o projeto foi cancelado ou rejeitado.', 403, 'project_chat_locked' );
    }
    $admin = get_user_by( 'id', (int) $request->get_param( 'rmbz_admin_user_id' ) );
    $msg_id = rmbz_pchat_insert_message( $id, 'admin', $message, null, $admin ? $admin->display_name : 'Equipa 1MZ' );
    if ( ! $msg_id ) return rmbz_api_error( 'Nao foi possivel enviar a mensagem.', 500, 'chat_send_failed' );
    return rmbz_api_ok( [ 'sent' => true, 'message_id' => (int) $msg_id ] );
}

function rmbz_api_admin_app_project_action( $id, $action, $reason ) {
    global $wpdb;
    $t = $wpdb->prefix . 'rmbz_artist_projects';
    $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$t} WHERE id=%d", $id ) );
    if ( ! $row ) return new WP_Error( 'not_found', 'Projeto não encontrado.', [ 'status' => 404 ] );

    if ( $action === 'project_update' ) {
        $params = json_decode( $reason, true );
        if ( ! is_array( $params ) ) {
            return new WP_Error( 'invalid_params', 'Dados de atualização inválidos.', [ 'status' => 400 ] );
        }
        $extra = ! empty( $row->extra_data ) ? json_decode( $row->extra_data, true ) : [];
        if ( ! is_array( $extra ) ) $extra = [];

        $updated_fields = [];

        if ( isset( $params['production_pitch'] ) ) {
            $pitch = sanitize_textarea_field( $params['production_pitch'] );
            $prev_pitch = $extra['production_pitch'] ?? '';
            if ( $pitch !== $prev_pitch ) {
                if ( $pitch === '' ) {
                    unset( $extra['production_pitch'], $extra['production_pitch_updated_at'] );
                } else {
                    $extra['production_pitch'] = $pitch;
                    $extra['production_pitch_updated_at'] = current_time( 'mysql', 1 );
                }
                $updated_fields[] = 'Pitch da Produção';
            }
        }

        $filming_changed = false;
        if ( isset( $params['filming_start'] ) ) {
            $start = sanitize_text_field( $params['filming_start'] );
            if ( ! isset( $extra['filming'] ) || ! is_array( $extra['filming'] ) ) $extra['filming'] = [];
            $prev_start = $extra['filming']['start'] ?? '';
            if ( $start !== $prev_start ) {
                $extra['filming']['start'] = $start;
                $filming_changed = true;
            }
        }
        if ( isset( $params['filming_end'] ) ) {
            $end = sanitize_text_field( $params['filming_end'] );
            if ( ! isset( $extra['filming'] ) || ! is_array( $extra['filming'] ) ) $extra['filming'] = [];
            $prev_end = $extra['filming']['end'] ?? '';
            if ( $end !== $prev_end ) {
                $extra['filming']['end'] = $end;
                $filming_changed = true;
            }
        }
        if ( isset( $params['filming_proposal'] ) ) {
            $proposal = sanitize_textarea_field( $params['filming_proposal'] );
            if ( ! isset( $extra['filming'] ) || ! is_array( $extra['filming'] ) ) $extra['filming'] = [];
            $prev_prop = $extra['filming']['proposal'] ?? '';
            if ( $proposal !== $prev_prop ) {
                $extra['filming']['proposal'] = $proposal;
                $filming_changed = true;
            }
        }
        if ( $filming_changed ) {
            $updated_fields[] = 'Datas de Filmagem';
        }

        if ( isset( $params['contract_url'] ) ) {
            $contract_url = esc_url_raw( $params['contract_url'] );
            $prev_contract = $extra['contract_url'] ?? '';
            if ( $contract_url !== $prev_contract ) {
                if ( $contract_url === '' ) {
                    unset( $extra['contract_url'] );
                } else {
                    $extra['contract_url'] = $contract_url;
                }
                $updated_fields[] = 'Link do Contrato';
            }
        }

        if ( empty( $updated_fields ) ) {
            return rmbz_api_ok( [ 'message' => 'Nenhuma alteração efetuada.' ] );
        }

        $wpdb->update(
            $t,
            [ 'extra_data' => wp_json_encode( $extra ) ],
            [ 'id' => $id ],
            [ '%s' ], [ '%d' ]
        );

        if ( function_exists( 'rmbz_pchat_append_change_log' ) ) {
            $who = 'admin';
            if ( function_exists( 'wp_get_current_user' ) ) {
                $u = wp_get_current_user();
                if ( $u && ! empty( $u->user_login ) ) $who = $u->user_login;
            }
            rmbz_pchat_append_change_log( $id, [
                'who'  => $who,
                'what' => 'Atualizou: ' . implode( ', ', $updated_fields )
            ] );
        }

        return rmbz_api_ok( [ 'message' => 'Projeto atualizado com sucesso.' ] );
    }

    // [1.6.393] Marcar a multa por atraso como PAGA/resolvida (limpa a dívida).
    if ( $action === 'project_clear_fine' ) {
        if ( ! function_exists( 'rmbz_project_clear_pending_fine' ) ) {
            return new WP_Error( 'unavailable', 'Função de multa indisponível.', [ 'status' => 500 ] );
        }
        rmbz_project_clear_pending_fine( $id );
        if ( function_exists( 'rmbz_pchat_append_change_log' ) ) {
            rmbz_pchat_append_change_log( $id, [
                'who'  => 'admin',
                'what' => '💸 Marcou a multa por atraso como PAGA (via app admin)',
            ] );
        }
        return true;
    }

    // [1.5.398] TRANSFERÊNCIA de projeto via app admin. Parâmetros em JSON no
    // campo `reason` (mesmo padrão do project_update): {"reason": "..."} ou
    // {"target": "84xxxxxxx"}.
    if ( in_array( $action, [ 'project_transfer_request', 'project_transfer_force', 'project_transfer_cancel', 'project_transfer_finalize' ], true ) ) {
        if ( ! function_exists( 'rmbz_project_transfer_execute' ) ) {
            return new WP_Error( 'unavailable', 'Função de transferência indisponível.', [ 'status' => 500 ] );
        }
        $params = json_decode( (string) $reason, true );
        if ( ! is_array( $params ) ) $params = [];

        if ( $action === 'project_transfer_request' ) {
            $res = rmbz_project_transfer_create_request( $id, (string) ( $params['reason'] ?? '' ) );
            if ( empty( $res['success'] ) ) return new WP_Error( 'transfer_failed', $res['message'], [ 'status' => 409 ] );
            return true;
        }

        if ( $action === 'project_transfer_cancel' ) {
            rmbz_project_transfer_cancel_request( $id );
            return true;
        }

        $target = preg_replace( '/[^0-9+]/', '', (string) ( $params['target'] ?? '' ) );
        if ( strlen( $target ) < 7 ) {
            return new WP_Error( 'target_required', 'Indica o número do novo dono do projeto.', [ 'status' => 400 ] );
        }

        if ( $action === 'project_transfer_finalize' ) {
            $req = rmbz_project_transfer_request_get( $id );
            if ( ! $req || ( $req['status'] ?? '' ) !== 'accepted' ) {
                return new WP_Error( 'not_accepted', 'O artista ainda não aceitou a transferência.', [ 'status' => 409 ] );
            }
            $res = rmbz_project_transfer_execute( $id, $target, (array) ( $req['changes'] ?? [] ), 'consent' );
        } else {
            // Forçada — muda já, sem o artista atual saber.
            $res = rmbz_project_transfer_execute( $id, $target, [], 'forced' );
        }
        if ( empty( $res['success'] ) ) return new WP_Error( 'transfer_failed', $res['message'], [ 'status' => 409 ] );
        return true;
    }

    // [1.5.403] PARTICIPAÇÃO (feat) via app admin — o admin define/altera/
    // remove. Parâmetros em JSON no `reason`: {name, has_account, tel, mode}.
    // mode 'forced' = aceite já sem o participante confirmar; 'consent' =
    // convite normal. name vazio ou {remove:true} = remover.
    if ( $action === 'feat_set' ) {
        if ( ! function_exists( 'rmbz_feat_admin_set' ) ) {
            return new WP_Error( 'unavailable', 'Função de participação indisponível.', [ 'status' => 500 ] );
        }
        $params = json_decode( (string) $reason, true );
        if ( ! is_array( $params ) ) $params = [];
        $res = rmbz_feat_admin_set(
            $id,
            ! empty( $params['remove'] ) ? '' : (string) ( $params['name'] ?? '' ),
            ! empty( $params['has_account'] ),
            (string) ( $params['tel'] ?? '' ),
            (string) ( $params['mode'] ?? 'consent' )
        );
        if ( empty( $res['success'] ) ) return new WP_Error( 'feat_failed', $res['message'], [ 'status' => 409 ] );
        return true;
    }

    // [1.5.407] EQUIPA DO VIDEOCLIPE via app admin. Params JSON no `reason`:
    // { ids: [1,2,...], requested_by: {"5":"artist","7":"mz"} }
    if ( $action === 'team_set' ) {
        $params = json_decode( (string) $reason, true );
        if ( ! is_array( $params ) ) $params = [];
        $extra = ! empty( $row->extra_data ) ? json_decode( $row->extra_data, true ) : [];
        if ( ! is_array( $extra ) ) $extra = [];

        $ids = ( isset( $params['ids'] ) && is_array( $params['ids'] ) )
            ? array_values( array_unique( array_filter( array_map( 'intval', $params['ids'] ) ) ) )
            : [];
        $extra['team_member_ids'] = $ids;

        $req_clean = [];
        if ( isset( $params['requested_by'] ) && is_array( $params['requested_by'] ) ) {
            foreach ( $params['requested_by'] as $tm_id => $tm_who ) {
                $req_clean[ (int) $tm_id ] = ( $tm_who === 'artist' ) ? 'artist' : 'mz';
            }
        }
        $extra['team_requested_by'] = $req_clean;

        // [1.5.408] QUAIS cargos cada pessoa exerce NESTE projeto.
        $proles_clean = [];
        if ( isset( $params['roles'] ) && is_array( $params['roles'] ) ) {
            foreach ( $params['roles'] as $tm_id => $tm_roles ) {
                $tm_id = (int) $tm_id;
                if ( ! in_array( $tm_id, $ids, true ) || ! is_array( $tm_roles ) ) continue;
                $clean = array_values( array_filter( array_map(
                    fn( $r ) => sanitize_text_field( (string) $r ),
                    $tm_roles
                ) ) );
                if ( ! empty( $clean ) ) $proles_clean[ $tm_id ] = $clean;
            }
        }
        $extra['team_project_roles'] = $proles_clean;

        $wpdb->update( $t, [ 'extra_data' => wp_json_encode( $extra ) ], [ 'id' => $id ], [ '%s' ], [ '%d' ] );
        if ( function_exists( 'rmbz_pchat_append_change_log' ) ) {
            rmbz_pchat_append_change_log( $id, [
                'who'  => 'admin',
                'what' => sprintf( '🎥 Atualizou a equipa do videoclipe (%d pessoa(s)) via app admin', count( $ids ) ),
            ] );
        }
        return true;
    }

    // [1.5.404] GRAVAÇÃO AO VIVO via app admin. Parâmetros em JSON no `reason`:
    //   filming_start      : (sem parâmetros)
    //   filming_add_person : { name, role, photo_url }
    //   filming_people_set : { day: int, participants: [...] }
    //   filming_finish     : { all_done: bool, percent_left: int, next_date: 'Y-m-d' }
    // [1.5.411] BUGFIX: 'filming_people_set' era tratado dentro do bloco mas
    // faltava nesta lista — a edição de participantes pelo app falhava.
    if ( in_array( $action, [ 'filming_start', 'filming_add_person', 'filming_people_set', 'filming_finish' ], true ) ) {
        if ( ! function_exists( 'rmbz_filming_session_start' ) ) {
            return new WP_Error( 'unavailable', 'Sessões de gravação indisponíveis.', [ 'status' => 500 ] );
        }
        $params = json_decode( (string) $reason, true );
        if ( ! is_array( $params ) ) $params = [];

        if ( $action === 'filming_start' ) {
            $res = rmbz_filming_session_start( $id );
        } elseif ( $action === 'filming_add_person' ) {
            $res = rmbz_filming_session_add_person(
                $id,
                (string) ( $params['name'] ?? '' ),
                (string) ( $params['role'] ?? '' ),
                (string) ( $params['photo_url'] ?? '' ),
                (string) ( $params['category'] ?? 'outro' )
            );
        } elseif ( $action === 'filming_people_set' ) {
            // [1.5.410] Edição total dos participantes de um dia (auditada).
            $res = rmbz_filming_session_people_set(
                $id,
                (int) ( $params['day'] ?? 0 ),
                is_array( $params['participants'] ?? null ) ? $params['participants'] : []
            );
        } else {
            $res = rmbz_filming_session_finish(
                $id,
                ! empty( $params['all_done'] ),
                (int) ( $params['percent_left'] ?? 0 ),
                (string) ( $params['next_date'] ?? '' )
            );
        }
        if ( empty( $res['success'] ) ) return new WP_Error( 'filming_failed', $res['message'], [ 'status' => 409 ] );
        return true;
    }

    // [1.5.411] ENTREGA AO EDITOR EXTERNO via app admin. Params JSON no `reason`:
    //   editor_delivery_info : { files: int, size_gb: float, quality: string }
    //   editor_delivery_done : (sem parâmetros)
    if ( in_array( $action, [ 'editor_delivery_info', 'editor_delivery_done' ], true ) ) {
        if ( ! function_exists( 'rmbz_editor_delivery_set_info' ) ) {
            return new WP_Error( 'unavailable', 'Entrega ao editor indisponível.', [ 'status' => 500 ] );
        }
        $params = json_decode( (string) $reason, true );
        if ( ! is_array( $params ) ) $params = [];
        $r = $action === 'editor_delivery_info'
            ? rmbz_editor_delivery_set_info(
                $id,
                (int) ( $params['files'] ?? 0 ),
                (float) ( $params['size_gb'] ?? 0 ),
                (string) ( $params['quality'] ?? '' ),
                'admin (app)'
            )
            : rmbz_editor_delivery_mark_delivered( $id, 'admin (app)' );
        if ( $r !== true ) return new WP_Error( 'editor_delivery_failed', (string) $r, [ 'status' => 409 ] );
        return true;
    }

    // [1.5.409] EDITAR O ORÇAMENTO via app admin. Params JSON no `reason`:
    // { items: [ {cat, desc, cost, must_pay, artist_assumes, checked,
    //             decided, team_fee_for?, team_fee_dept?}, ... ] }
    // Recalcula o scenario_cost e regista no histórico.
    if ( $action === 'budget_items_set' ) {
        $params = json_decode( (string) $reason, true );
        if ( ! is_array( $params ) || ! isset( $params['items'] ) || ! is_array( $params['items'] ) ) {
            return new WP_Error( 'bad_items', 'Envia a lista de itens do orçamento.', [ 'status' => 400 ] );
        }
        $clean = [];
        $total = 0.0;
        foreach ( $params['items'] as $it ) {
            if ( ! is_array( $it ) ) continue;
            $desc = sanitize_text_field( (string) ( $it['desc'] ?? '' ) );
            if ( $desc === '' ) continue;
            $cost = max( 0.0, (float) ( $it['cost'] ?? 0 ) );
            $row_item = [
                'cat'            => sanitize_text_field( (string) ( $it['cat'] ?? '' ) ),
                'desc'           => $desc,
                'cost'           => $cost,
                'checked'        => ! empty( $it['checked'] ) ? 1 : 0,
                'must_pay'       => ( isset( $it['must_pay'] ) && (int) $it['must_pay'] === 0 ) ? 0 : 1,
                'artist_assumes' => ! empty( $it['artist_assumes'] ) ? 1 : 0,
                'decided'        => isset( $it['decided'] ) ? ( (int) $it['decided'] ? 1 : 0 ) : 1,
            ];
            // Preservar marcas da taxa de vistoria (auditoria/bloqueio).
            if ( ! empty( $it['team_fee_for'] ) )  $row_item['team_fee_for']  = (int) $it['team_fee_for'];
            if ( ! empty( $it['team_fee_dept'] ) ) $row_item['team_fee_dept'] = sanitize_text_field( (string) $it['team_fee_dept'] );
            $clean[] = $row_item;
            $total  += $cost;
        }
        // [1.5.410] Recalcular o is_paid: mudar itens (ex.: acrescentar
        // vistoria) altera o valor OBRIGATÓRIO — o Controlo Financeiro do
        // artista tem de refletir "falta pagar" de imediato.
        $oblig = 0.0;
        foreach ( $clean as $ci ) {
            $mp = isset( $ci['must_pay'] ) ? (int) $ci['must_pay'] : 1;
            $aa = ! empty( $ci['artist_assumes'] );
            if ( $mp === 1 || ! $aa ) $oblig += (float) $ci['cost'];
        }
        $bi_paid = (float) $wpdb->get_var( $wpdb->prepare( "SELECT amount_paid FROM {$t} WHERE id=%d", $id ) );
        $paid_now = ( $bi_paid + 0.009 ) >= $oblig ? 1 : 0;

        $wpdb->update(
            $t,
            [ 'scenario_details' => wp_json_encode( $clean ), 'scenario_cost' => $total, 'is_paid' => $paid_now ],
            [ 'id' => $id ],
            [ '%s', '%f', '%d' ], [ '%d' ]
        );
        if ( function_exists( 'rmbz_pchat_append_change_log' ) ) {
            rmbz_pchat_append_change_log( $id, [
                'who'  => 'admin',
                'what' => sprintf( '💰 Atualizou o orçamento via app admin (%d itens · %s MT)', count( $clean ), number_format( $total, 2, ',', '.' ) ),
            ] );
        }
        return true;
    }

    // [1.5.402] GERADOR DE CONTRATO via app admin — mesmo fluxo do WP admin:
    // gerar cria um RASCUNHO (o artista não recebe nada); depois o admin
    // aprova ("enviar ao artista") ou rejeita. Overrides em JSON no `reason`.
    if ( in_array( $action, [ 'contract_generate', 'contract_draft_send', 'contract_draft_discard' ], true ) ) {
        if ( ! function_exists( 'rmbz_contract_generate_for_project' ) ) {
            return new WP_Error( 'unavailable', 'Gerador de contrato indisponível.', [ 'status' => 500 ] );
        }
        if ( $action === 'contract_generate' ) {
            $params = json_decode( (string) $reason, true );
            if ( ! is_array( $params ) ) $params = [];
            $overrides = [];
            foreach ( [ 'artist_name', 'artist_tel', 'music', 'level', 'value', 'city', 'bi_number', 'signer_name' ] as $k ) {
                if ( isset( $params[ $k ] ) ) $overrides[ $k ] = (string) $params[ $k ];
            }
            $res = rmbz_contract_generate_for_project( $id, $overrides );
        } elseif ( $action === 'contract_draft_send' ) {
            $res = rmbz_contract_draft_send_core( $id );
        } else {
            $res = rmbz_contract_draft_discard_core( $id );
        }
        if ( empty( $res['success'] ) ) return new WP_Error( 'contract_failed', $res['message'], [ 'status' => 409 ] );
        return true;
    }

    // [1.6.393] Ligar/desligar as notificações deste projeto ao artista.
    if ( $action === 'project_toggle_notifications' ) {
        $extra = ! empty( $row->extra_data ) ? json_decode( $row->extra_data, true ) : [];
        if ( ! is_array( $extra ) ) $extra = [];
        $muted = empty( $extra['notifications_muted'] ) ? 1 : 0;
        $extra['notifications_muted'] = $muted;
        $wpdb->update( $t, [ 'extra_data' => wp_json_encode( $extra ) ], [ 'id' => $id ], [ '%s' ], [ '%d' ] );
        if ( function_exists( 'rmbz_pchat_append_change_log' ) ) {
            rmbz_pchat_append_change_log( $id, [
                'who'  => 'admin',
                'what' => $muted
                    ? '🔕 Desativou as notificações do projeto (via app admin)'
                    : '🔔 Reativou as notificações do projeto (via app admin)',
            ] );
        }
        return true;
    }

    $allowed = [
        'project_approve' => [ 'from' => [ 'pending_review' ], 'to' => 'pre-production', 'label' => 'Projeto aprovado' ],
        'kyc_approve'     => [ 'from' => [ 'kyc_review' ], 'to' => 'pre-production', 'label' => 'KYC aprovado' ],
        'project_reject'  => [ 'from' => [ 'pending_review', 'kyc_review', 'pre-production' ], 'to' => 'rejected', 'label' => 'Projeto rejeitado' ],
    ];
    if ( ! isset( $allowed[ $action ] ) ) return new WP_Error( 'invalid_action', 'Ação de projeto inválida.', [ 'status' => 400 ] );
    $rule = $allowed[ $action ];
    if ( ! in_array( $row->production_status, $rule['from'], true ) ) {
        return new WP_Error( 'invalid_state', 'O projeto já mudou de estado. Atualiza a lista.', [ 'status' => 409 ] );
    }
    if ( $action === 'project_reject' && $reason === '' ) {
        return new WP_Error( 'reason_required', 'Indica o motivo da rejeição.', [ 'status' => 400 ] );
    }

    $extra = ! empty( $row->extra_data ) ? json_decode( $row->extra_data, true ) : [];
    if ( ! is_array( $extra ) ) $extra = [];
    if ( $action === 'project_approve' && empty( $extra['approved_at'] ) ) $extra['approved_at'] = current_time( 'mysql', 1 );
    if ( $action === 'kyc_approve' ) unset( $extra['kyc_rejection_reason'] );
    if ( $action === 'project_reject' ) $extra['rejection_reason'] = $reason;

    $updated = $wpdb->update(
        $t,
        [ 'production_status' => $rule['to'], 'extra_data' => wp_json_encode( $extra ) ],
        [ 'id' => $id, 'production_status' => $row->production_status ],
        [ '%s', '%s' ], [ '%d', '%s' ]
    );
    if ( $updated !== 1 ) return new WP_Error( 'race_condition', 'Outro administrador alterou este projeto.', [ 'status' => 409 ] );

    if ( function_exists( 'rmbz_notify_create' ) ) {
        rmbz_notify_create( [
            'type' => 'project_status_change',
            'severity' => $action === 'project_reject' ? 'danger' : 'success',
            'title' => $rule['label'] . ': ' . $row->project_title,
            'body' => $reason ?: 'A equipa atualizou o estado do teu projeto.',
            'action_url' => 'onemz://projects/' . $id,
            'target_telemovel' => $row->artist_telemovel,
            'dedup_key' => 'admin_app_project_' . $id . '_' . $rule['to'] . '_' . time(),
            'created_by' => 'admin_app',
        ] );
    }
    return true;
}

function rmbz_api_admin_app_finance_action( $id, $action, $reason ) {
    global $wpdb;
    $dep_t = $wpdb->prefix . 'rmbz_deposit_requests';
    $withd_t = $wpdb->prefix . 'rmbz_withdrawal_requests';

    if ( $action === 'deposit_approve' ) {
        $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$dep_t} WHERE id=%d AND status='pendente'", $id ) );
        if ( ! $row ) return new WP_Error( 'already_processed', 'Depósito não encontrado ou já processado.', [ 'status' => 409 ] );
        $reserved = $wpdb->update(
            $dep_t, [ 'status' => 'processando', 'admin_notes' => 'Em processamento via 1MZ Admin.' ],
            [ 'id' => $id, 'status' => 'pendente' ], [ '%s', '%s' ], [ '%d', '%s' ]
        );
        if ( $reserved !== 1 ) return new WP_Error( 'race_condition', 'Outro administrador está a processar este depósito.', [ 'status' => 409 ] );
        $credit = rmbz_update_wallet_balance(
            $row->user_telemovel, (float) $row->amount, 'deposit', null,
            'Depósito aprovado pela app administrativa (' . $row->payment_method . ' - ID: ' . $row->transaction_id . ')'
        );
        if ( empty( $credit['success'] ) ) {
            $wpdb->update( $dep_t, [ 'status' => 'pendente', 'admin_notes' => 'Falha ao creditar; operação revertida.' ], [ 'id' => $id, 'status' => 'processando' ] );
            return new WP_Error( 'wallet_error', $credit['error'] ?? 'Falha ao creditar carteira.', [ 'status' => 500 ] );
        }
        $updated = $wpdb->update(
            $dep_t, [ 'status' => 'aprovado', 'processed_date' => current_time( 'mysql', 1 ), 'admin_notes' => 'Aprovado via 1MZ Admin.' ],
            [ 'id' => $id, 'status' => 'processando' ], [ '%s', '%s', '%s' ], [ '%d', '%s' ]
        );
        if ( $updated !== 1 ) return new WP_Error( 'finalize_failed', 'O saldo foi creditado, mas o depósito precisa de revisão manual.', [ 'status' => 500 ] );
        if ( function_exists( 'rmbz_notify' ) ) rmbz_notify( $row->user_telemovel, 'deposit_approved', [ 'valor' => number_format( (float) $row->amount, 0, ',', '.' ) ] );
        return true;
    }

    if ( $action === 'deposit_reject' ) {
        $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$dep_t} WHERE id=%d AND status='pendente'", $id ) );
        if ( ! $row ) return new WP_Error( 'already_processed', 'Depósito não encontrado ou já processado.', [ 'status' => 409 ] );
        if ( ! empty( $row->is_first_deposit ) ) {
            return new WP_Error( 'first_deposit_requires_site', 'Por segurança, rejeita o primeiro depósito no site. Esta ação também apaga a conta recém-criada.', [ 'status' => 409 ] );
        }
        if ( $reason === '' ) return new WP_Error( 'reason_required', 'Indica o motivo da rejeição.', [ 'status' => 400 ] );
        $updated = $wpdb->update(
            $dep_t, [ 'status' => 'rejeitado', 'processed_date' => current_time( 'mysql', 1 ), 'admin_notes' => $reason ],
            [ 'id' => $id, 'status' => 'pendente' ], [ '%s', '%s', '%s' ], [ '%d', '%s' ]
        );
        if ( $updated !== 1 ) return new WP_Error( 'race_condition', 'Outro administrador processou este depósito.', [ 'status' => 409 ] );
        if ( function_exists( 'rmbz_notify' ) ) rmbz_notify( $row->user_telemovel, 'deposit_rejected', [ 'valor' => number_format( (float) $row->amount, 0, ',', '.' ), 'motivo' => $reason ] );
        return true;
    }

    if ( $action === 'withdrawal_guard_item' ) {
        if ( ! function_exists( 'rmbz_wg_get_review' ) || ! function_exists( 'rmbz_wg_checklist_items' ) ) {
            return new WP_Error( 'guard_unavailable', 'O Guarda-Costas não está disponível no servidor.', [ 'status' => 500 ] );
        }
        $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$withd_t} WHERE id=%d AND status='pendente'", $id ) );
        if ( ! $row ) return new WP_Error( 'already_processed', 'Levantamento não encontrado ou já processado.', [ 'status' => 409 ] );

        $params = json_decode( $reason, true );
        if ( ! is_array( $params ) || empty( $params['key'] ) ) {
            return new WP_Error( 'invalid_params', 'Item da checklist inválido.', [ 'status' => 400 ] );
        }
        $key         = sanitize_key( (string) $params['key'] );
        $done        = ! empty( $params['done'] );
        $item_reason = sanitize_textarea_field( (string) ( $params['reason'] ?? '' ) );

        $report = function_exists( 'rmbz_wg_build_forensic_report' ) ? rmbz_wg_build_forensic_report( $row ) : null;
        $items_def = rmbz_wg_checklist_items( $row, $report );
        if ( ! isset( $items_def[ $key ] ) ) {
            return new WP_Error( 'invalid_item', 'Este item não se aplica a este levantamento.', [ 'status' => 400 ] );
        }
        if ( $done && ! empty( $items_def[ $key ]['critical'] ) && strlen( $item_reason ) < 10 ) {
            return new WP_Error( 'reason_required', 'Este item crítico exige um motivo com pelo menos 10 caracteres.', [ 'status' => 400 ] );
        }

        $review = rmbz_wg_get_review( $id );
        $checklist = is_array( $review->_checklist ) ? $review->_checklist : [];
        $checklist[ $key ] = [ 'done' => $done, 'reason' => $item_reason ];
        rmbz_wg_save_checklist( $id, $checklist );
        if ( function_exists( 'rmbz_wg_log' ) ) {
            rmbz_wg_log( $id, 'checklist_updated_app', 'Item "' . $key . '" ' . ( $done ? 'marcado' : 'desmarcado' ) . ' via app administrativa.' );
        }
        return true;
    }

    if ( $action === 'withdrawal_approve' ) {
        $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$withd_t} WHERE id=%d AND status='pendente'", $id ) );
        if ( ! $row ) return new WP_Error( 'already_processed', 'Levantamento não encontrado ou já processado.', [ 'status' => 409 ] );
        if ( function_exists( 'rmbz_wg_checklist_complete' ) ) {
            $report = function_exists( 'rmbz_wg_build_forensic_report' ) ? rmbz_wg_build_forensic_report( $row ) : null;
            if ( ! rmbz_wg_checklist_complete( $id, $row, $report ) ) {
                return new WP_Error( 'checklist_incomplete', 'O checklist antifraude está incompleto. Conclui a análise no site antes de aprovar.', [ 'status' => 409 ] );
            }
        }
        $updated = $wpdb->update(
            $withd_t, [ 'status' => 'aprovado', 'processed_date' => current_time( 'mysql', 1 ), 'admin_notes' => 'Pagamento processado via 1MZ Admin.' ],
            [ 'id' => $id, 'status' => 'pendente' ], [ '%s', '%s', '%s' ], [ '%d', '%s' ]
        );
        if ( $updated !== 1 ) return new WP_Error( 'race_condition', 'Outro administrador processou este levantamento.', [ 'status' => 409 ] );
        do_action( 'rmbz_withdrawal_approved', $id, $row->user_telemovel, (float) $row->amount );
        if ( function_exists( 'rmbz_wg_log' ) ) rmbz_wg_log( $id, 'approved_admin_app', 'Pedido aprovado pela app administrativa.' );
        return true;
    }

    if ( $action === 'withdrawal_reject' ) {
        if ( $reason === '' ) return new WP_Error( 'reason_required', 'Indica o motivo da rejeição.', [ 'status' => 400 ] );
        $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$withd_t} WHERE id=%d AND status='pendente'", $id ) );
        if ( ! $row ) return new WP_Error( 'already_processed', 'Levantamento não encontrado ou já processado.', [ 'status' => 409 ] );
        $updated = $wpdb->update(
            $withd_t, [ 'status' => 'rejeitado', 'processed_date' => current_time( 'mysql', 1 ), 'admin_notes' => $reason ],
            [ 'id' => $id, 'status' => 'pendente' ], [ '%s', '%s', '%s' ], [ '%d', '%s' ]
        );
        if ( $updated !== 1 ) return new WP_Error( 'race_condition', 'Outro administrador processou este levantamento.', [ 'status' => 409 ] );

        $is_royalties = ( $row->source ?? 'wallet' ) === 'royalties' && (float) ( $row->exchange_rate ?? 0 ) > 0;
        if ( $is_royalties && function_exists( 'rmbz_secure_revert_mt_to_usd' ) ) {
            $refund = rmbz_secure_revert_mt_to_usd( $row->user_telemovel, (float) $row->amount, (float) $row->exchange_rate, $id );
        } else {
            $refund = rmbz_update_wallet_balance( $row->user_telemovel, (float) $row->amount, 'withdrawal_rejected', null, 'Reembolso de levantamento rejeitado pela app administrativa #' . $id );
        }
        if ( empty( $refund['success'] ) ) {
            $wpdb->update( $withd_t, [ 'status' => 'pendente', 'processed_date' => null, 'admin_notes' => 'Falha no reembolso; operação revertida.' ], [ 'id' => $id ] );
            return new WP_Error( 'refund_failed', 'Falha ao devolver o valor. O pedido voltou ao estado pendente.', [ 'status' => 500 ] );
        }
        do_action( 'rmbz_withdrawal_rejected', $id, $row->user_telemovel, (float) $row->amount, $reason );
        if ( function_exists( 'rmbz_wg_log' ) ) rmbz_wg_log( $id, 'rejected_admin_app', 'Pedido rejeitado pela app administrativa: ' . $reason );
        return true;
    }

    return new WP_Error( 'invalid_action', 'Ação financeira inválida.', [ 'status' => 400 ] );
}
