<?php
/**
 * includes/admin/admin-match-review.php
 *
 * Painel admin para rever matches sugeridos pelo detector multi-camada
 * de imports de royalties.
 *
 * Quando o importador processa um CSV e encontra uma faixa por UPC ou
 * por fuzzy match (não por ISRC), em vez de aplicar imediatamente
 * guarda em `rmbz_distro_match_review` para confirmação manual.
 *
 * Aqui o admin vê cada match com contexto completo, e decide:
 *   - APROVAR  → atribui royalties acumulados desta faixa ao artista
 *                sugerido (move da unclaimed para o ledger)
 *   - REJEITAR → confirma que NÃO é o artista certo (royalty fica em
 *                Black Box até reclamação manual)
 *   - MANUAL   → admin escolhe outra faixa (placeholder por agora)
 */

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


/* ============================================================================
 * RENDER PRINCIPAL
 * ============================================================================ */

function rmbz_render_admin_match_review() {
    if ( ! current_user_can('manage_options') ) {
        echo '<div class="notice notice-error"><p>Sem permissão.</p></div>';
        return;
    }

    global $wpdb;
    $t = $wpdb->prefix . 'rmbz_distro_match_review';
    if ( $wpdb->get_var("SHOW TABLES LIKE '$t'") !== $t ) {
        echo '<div class="notice notice-warning"><p>Tabela de matches ainda não criada — faz um import para inicializar.</p></div>';
        return;
    }

    // ──────────────────────────────────────────────────────────────────────
    // [DS 1.5.103] DETECÇÃO E LIMPEZA DE MATCHES ÓRFÃOS
    // ──────────────────────────────────────────────────────────────────────
    // Bug histórico: ao rejeitar/anular uma importação, os matches pendentes
    // dela ficavam aqui pendentes para sempre. A 1.5.103 corrige isto
    // (rmbz_handle_reject_import + rmbz_handle_revert_import já apagam),
    // mas pode haver órfãos ANTERIORES à correção. Esta lógica detecta-os
    // e oferece um botão de limpeza manual.
    //
    // Um match é "órfão" se o import_hash dele aponta para uma importação
    // que está com status='reverted' ou approval_status='rejected'.
    // ──────────────────────────────────────────────────────────────────────
    $t_logs = $wpdb->prefix . 'rmbz_distro_import_logs';

    // Limpeza: se admin clicou no botão
    if ( isset($_GET['rmbz_clean_orphans']) && check_admin_referer('rmbz_clean_orphans_match_review') ) {
        $deleted = $wpdb->query(
            "DELETE mr FROM $t mr
             INNER JOIN $t_logs il ON il.file_hash = mr.import_hash
             WHERE mr.status = 'pending'
               AND ( il.status = 'reverted' OR il.approval_status = 'rejected' )"
        );
        $del_n = (int) $deleted;
        echo '<div class="notice notice-success is-dismissible"><p>✓ ' . $del_n . ' match(es) órfão(s) removido(s).</p></div>';
    }

    // Detecção: quantos órfãos existem agora?
    $orphan_count = (int) $wpdb->get_var(
        "SELECT COUNT(*) FROM $t mr
         INNER JOIN $t_logs il ON il.file_hash = mr.import_hash
         WHERE mr.status = 'pending'
           AND ( il.status = 'reverted' OR il.approval_status = 'rejected' )"
    );

    if ( $orphan_count > 0 ) {
        $clean_url = wp_nonce_url(
            add_query_arg('rmbz_clean_orphans', '1'),
            'rmbz_clean_orphans_match_review'
        );
        ?>
        <div class="notice notice-warning" style="border-left-color:#f0b90b; padding:14px 18px;">
            <p style="margin:0 0 8px;">
                <strong>⚠ <?php echo $orphan_count; ?> match(es) pendente(s) de importações já rejeitadas/anuladas</strong>
            </p>
            <p style="margin:0 0 10px; font-size:13px; color:#555;">
                Estes matches ficaram para trás de imports que foram cancelados.
                Não há dinheiro associado — é seguro removê-los.
            </p>
            <a href="<?php echo esc_url($clean_url); ?>"
               class="button button-secondary"
               onclick="return confirm('Remover <?php echo $orphan_count; ?> match(es) órfão(s)?\n\nNão afecta wallets nem distribuição — só limpa esta lista.');">
                🧹 Limpar matches órfãos
            </a>
        </div>
        <?php
    }

    // ──────────────────────────────────────────────────────────────────────
    // [DS 1.5.105] APROVADOS ÓRFÃOS — matches aprovados manualmente cujo
    // import original foi anulado/rejeitado. O dinheiro deles JÁ FOI
    // revertido (porque as linhas no ledger têm import_hash igual ao do
    // import e foram apanhadas pela anulação), mas o status do match
    // continua 'approved' eternamente.
    //
    // Esta limpeza muda apenas o `status` para 'reverted_with_import' —
    // não toca em ledger, daily_stats ou wallets (já estão consistentes).
    // ──────────────────────────────────────────────────────────────────────
    if ( isset($_GET['rmbz_revert_approved_orphans']) && check_admin_referer('rmbz_revert_approved_orphans_match_review') ) {
        $now_str = current_time('mysql');
        $reviewer = wp_get_current_user()->user_login ?: 'admin';
        $updated = $wpdb->query( $wpdb->prepare(
            "UPDATE $t mr
             INNER JOIN $t_logs il ON il.file_hash = mr.import_hash
             SET mr.status = 'reverted_with_import',
                 mr.reviewed_at = %s,
                 mr.review_notes = CONCAT_WS(' | ', mr.review_notes,
                     CONCAT('Revertido retroactivamente (import já anulado) por ', %s, ' em ', %s))
             WHERE mr.status = 'approved'
               AND ( il.status = 'reverted' OR il.approval_status = 'rejected' )",
            $now_str, $reviewer, $now_str
        ) );
        $upd_n = (int) $updated;
        echo '<div class="notice notice-success is-dismissible"><p>✓ ' . $upd_n . ' match(es) aprovado(s) órfão(s) revertido(s). O dinheiro já tinha sido revertido com a anulação do import — só atualizei o status.</p></div>';
    }

    $approved_orphan_count = (int) $wpdb->get_var(
        "SELECT COUNT(*) FROM $t mr
         INNER JOIN $t_logs il ON il.file_hash = mr.import_hash
         WHERE mr.status = 'approved'
           AND ( il.status = 'reverted' OR il.approval_status = 'rejected' )"
    );

    if ( $approved_orphan_count > 0 ) {
        $revert_orphans_url = wp_nonce_url(
            add_query_arg('rmbz_revert_approved_orphans', '1'),
            'rmbz_revert_approved_orphans_match_review'
        );
        ?>
        <div class="notice notice-error" style="border-left-color:#e74c3c; padding:14px 18px;">
            <p style="margin:0 0 8px;">
                <strong>⚠ <?php echo $approved_orphan_count; ?> match(es) aprovado(s) cujo import foi anulado</strong>
            </p>
            <p style="margin:0 0 10px; font-size:13px; color:#555;">
                Estes matches estão marcados como "Aprovado" mas o import original foi anulado/rejeitado.
                O dinheiro deles JÁ foi revertido (saiu do ledger e da wallet do artista) — só falta
                actualizar o status do match aqui na lista para reflectir o estado real.
            </p>
            <p style="margin:0 0 10px; font-size:12px; color:#888;">
                💡 Esta acção <strong>não mexe em wallets nem em royalties</strong>. Só corrige a etiqueta
                visual dos matches.
            </p>
            <a href="<?php echo esc_url($revert_orphans_url); ?>"
               class="button button-primary"
               style="background:#e74c3c; border-color:#c0392b;"
               onclick="return confirm('Atualizar status de <?php echo $approved_orphan_count; ?> match(es) aprovado(s) cujo import foi anulado?\n\nNão afecta dinheiro — só corrige o status visual.');">
                🔄 Reverter status de matches aprovados órfãos
            </a>
        </div>
        <?php
    }
    // ──────────────────────────────────────────────────────────────────────

    // Mensagens (todos os tipos: individual + grupo + bulk)
    rmbz_match_render_messages();

    $filter = sanitize_key( $_GET['mr_status'] ?? 'pending' );
    $valid_filters = ['pending','approved','rejected','manual','reverted_with_import','all'];
    if ( ! in_array($filter, $valid_filters, true) ) $filter = 'pending';

    // [1.5.207] Ordenação configurável + filtro por artista
    $sort = sanitize_key( $_GET['sort'] ?? 'value' );
    $valid_sorts = ['value', 'artist', 'confidence', 'date'];
    if ( ! in_array($sort, $valid_sorts, true) ) $sort = 'value';

    $filter_artist = isset($_GET['mr_artist']) ? trim( wp_unslash( (string) $_GET['mr_artist'] ) ) : '';

    $where_clauses = [];
    $where_p = [];
    if ( $filter !== 'all' ) {
        // Prefixar com `mr.` é obrigatório — a tabela rmbz_distro_releases (alias r)
        // do JOIN abaixo TAMBÉM tem coluna `status`, e sem prefixo o MySQL
        // devolve "Column 'status' in where clause is ambiguous".
        $where_clauses[] = "mr.status = %s";
        $where_p[] = $filter;
    }
    if ( $filter_artist !== '' ) {
        $where_clauses[] = "mr.csv_artist = %s";
        $where_p[] = $filter_artist;
    }
    $where = ! empty( $where_clauses ) ? ('WHERE ' . implode(' AND ', $where_clauses)) : '';

    // [1.5.207] AGREGAÇÃO POR ARTISTA — para "Top artistas com dinheiro retido"
    //
    // Antes da query principal de rows, calcula soma agregada por csv_artist.
    // Mostra top N como cards no topo (clicáveis para filtrar) + tabela
    // colapsável "ver todos" para casos com muitos artistas.
    //
    // Só aplicado ao filtro pending (faz mais sentido) — para approved/rejected
    // os totais são "histórico" e menos accionáveis.
    $top_artists_data = [];
    if ( $filter === 'pending' ) {
        $top_artists_data = $wpdb->get_results(
            "SELECT
                mr.csv_artist,
                COUNT(DISTINCT mr.csv_isrc, mr.csv_upc, mr.match_method) AS group_count,
                SUM(mr.csv_amount_usd) AS total_amount_usd,
                SUM(mr.csv_streams)    AS total_streams
             FROM $t mr
             WHERE mr.status = 'pending' AND mr.csv_artist IS NOT NULL AND mr.csv_artist != ''
             GROUP BY mr.csv_artist
             ORDER BY total_amount_usd DESC
             LIMIT 200"
        );
    }

    // ──────────────────────────────────────────────────────────────────────
    // QUERY AGRUPADA — uma linha por (faixa sugerida + ISRC + UPC do CSV).
    //
    // Antes mostrava 918 linhas individuais (1 por loja × país do CSV).
    // Agora agrega: SUM(streams), SUM(amount), COUNT(linhas), GROUP_CONCAT
    // dos IDs originais para acções em massa operarem em todos os IDs do
    // grupo de uma só vez.
    // ──────────────────────────────────────────────────────────────────────
    // [1.5.207] ORDER BY dinâmico consoante $sort
    // NOTA: ao usar GROUP BY com mr.suggested_track_id/csv_isrc/csv_upc/...,
    // o csv_artist já é "estável" por grupo (todas as linhas duma mesma faixa
    // sugerida têm o mesmo artista no CSV). Mas usamos MAX() defensivamente
    // contra ONLY_FULL_GROUP_BY em SQL strict mode.
    $order_by_clause = '';
    switch ( $sort ) {
        case 'artist':
            // Agrupa todas as faixas do mesmo artista juntas
            $order_by_clause = 'ORDER BY MAX(mr.csv_artist) ASC, total_amount_usd DESC';
            break;
        case 'confidence':
            // Match seguro (upc_title) primeiro, fuzzy último
            $order_by_clause = 'ORDER BY MAX(mr.match_confidence) DESC, total_amount_usd DESC';
            break;
        case 'date':
            // FIFO — mais antigo primeiro (ordem de chegada)
            $order_by_clause = 'ORDER BY latest_created_at ASC, total_amount_usd DESC';
            break;
        case 'value':
        default:
            // Default: maior valor primeiro
            $order_by_clause = 'ORDER BY total_amount_usd DESC, latest_created_at DESC';
    }

    $sql = "
        SELECT
            MIN(mr.id)                        AS sample_id,
            GROUP_CONCAT(mr.id ORDER BY mr.id) AS all_ids,
            COUNT(*)                          AS line_count,
            SUM(mr.csv_streams)               AS total_streams,
            SUM(mr.csv_amount_usd)            AS total_amount_usd,
            mr.csv_isrc, mr.csv_upc, mr.csv_track_title, mr.csv_artist,
            mr.csv_album, mr.csv_label,
            GROUP_CONCAT(DISTINCT mr.csv_store ORDER BY mr.csv_store SEPARATOR ', ') AS stores_csv,
            COUNT(DISTINCT mr.csv_country)    AS country_count,
            mr.suggested_track_id, mr.suggested_release_id, mr.suggested_artist_telemovel,
            mr.match_method, mr.match_confidence, mr.match_distance,
            mr.artist_conflict, mr.status, mr.reviewed_by, mr.reviewed_at,
            MAX(mr.created_at)                AS latest_created_at,
            w.user_nome   AS artist_name,
            t.track_title AS db_track_title,
            r.title       AS db_release_title,
            r.upc_code    AS db_upc
        FROM $t mr
        LEFT JOIN {$wpdb->prefix}rmbz_wallets w        ON mr.suggested_artist_telemovel = w.user_telemovel
        LEFT JOIN {$wpdb->prefix}rmbz_distro_tracks t  ON mr.suggested_track_id = t.id
        LEFT JOIN {$wpdb->prefix}rmbz_distro_releases r ON mr.suggested_release_id = r.id
        $where
        GROUP BY mr.suggested_track_id, mr.csv_isrc, mr.csv_upc, mr.match_method, mr.status, mr.artist_conflict
        $order_by_clause
        LIMIT 200
    ";

    $rows = empty($where_p)
        ? $wpdb->get_results( $sql )
        : $wpdb->get_results( $wpdb->prepare( $sql, ...$where_p ) );

    // Contadores por status (não por grupo — total real)
    $counts = $wpdb->get_results( "SELECT status, COUNT(*) AS n FROM $t GROUP BY status", OBJECT_K );

    // Contadores agrupados (para badges nos botões)
    $group_counts = [];
    foreach ( ['pending','approved','rejected'] as $st ) {
        $group_counts[$st] = (int) $wpdb->get_var( $wpdb->prepare(
            "SELECT COUNT(*) FROM (
                SELECT 1 FROM $t mr
                WHERE mr.status = %s
                GROUP BY mr.suggested_track_id, mr.csv_isrc, mr.csv_upc, mr.match_method, mr.artist_conflict
            ) AS grouped",
            $st
        ) );
    }

    // Para acções em massa: contar quantos pendentes têm conflito
    $pending_with_conflict = (int) $wpdb->get_var(
        "SELECT COUNT(*) FROM $t WHERE status = 'pending' AND artist_conflict = 1"
    );
    $pending_low_confidence = (int) $wpdb->get_var(
        "SELECT COUNT(*) FROM $t WHERE status = 'pending' AND match_confidence < 50"
    );

    // ──────────────────────────────────────────────────────────────────────
    // [1.5.164] BACKFILL DE WALLETS — DESACTIVADO PÓS-1.5.110
    //
    // CONTEXTO HISTÓRICO:
    // Esta detecção foi criada para encontrar matches aprovados antes de um
    // bug em rmbz_match_approve_one ser corrigido. O bug era: aprovar um
    // match não creditava `rmbz_wallets.balance` (MT) — só ledger USD e stats.
    //
    // PORQUE FOI DESACTIVADA:
    // A 1.5.110 mudou a arquitectura: matches aprovados PROPOSITALMENTE não
    // creditam mais a wallet MT. Royalties são USD e vivem em
    // `rmbz_distro_stats.total_earnings`. Para mover USD → MT, o artista usa
    // o "Levantamento Inteligente" (rmbz_secure_transfer_usd_to_mt) que
    // converte ao câmbio actual.
    //
    // RESULTADO DO DETECTOR ANTIGO PÓS-1.5.110:
    //   - TODOS os matches aprovados não têm transacção MT (correcto!)
    //   - Detector marca todos como "dessincronizados"
    //   - Falso positivo (era 1207 matches no caso real)
    //
    // PERIGO DO BOTÃO "SINCRONIZAR":
    //   - Iria creditar wallet MT com valor numérico USD (ex: $2.17 → 2.17 MT)
    //   - Sem conversão de câmbio (devia ser ~140 MT)
    //   - Reintroduzia exactamente o bug que 1.5.110 corrigiu
    //
    // POR ISSO: forçamos $unsynced_matches=0 para esconder o alerta.
    // A função handler `rmbz_match_backfill_wallets_handler` ainda existe
    // (mais abaixo) mas o botão deixa de aparecer e ela já não tem como
    // ser disparada via UI.
    // ──────────────────────────────────────────────────────────────────────
    $unsynced_matches = 0;
    $unsynced_amount  = 0.0;

    ?>
    <?php /* [1.5.180] Redesign moderno: header dashboard, container 100%
       largura, filtros como pills modernas, empty state polido.
       Lógica PHP/handlers/queries totalmente preservada — só CSS. */ ?>
    <div class="wrap rmbz-mr-wrap">
        <div class="rmbz-mr-header">
            <div class="rmbz-mr-kicker">DISTRIBUIÇÃO · CONTROLO DE QUALIDADE</div>
            <h1 class="rmbz-mr-title">🔍 Matches a Rever</h1>
            <p class="rmbz-mr-subtitle">
                Linhas de royalty cujo match com a faixa do catálogo não foi feito por ISRC (que é 100% fiável)
                mas sim por UPC ou por nome+artista (que precisam de confirmação humana). Aprovar atribui
                o royalty ao artista; rejeitar deixa em Black Box.
            </p>
        </div>

        <div class="rmbz-mr-filters-card">
            <div class="rmbz-mr-filters-head">
                <strong>Filtrar por estado</strong>
                <span class="rmbz-mr-filters-hint">
                    Os números mostram <strong>grupos</strong> (faixas únicas) / <strong>linhas brutas</strong> da BD.
                    Cada grupo agrega múltiplas linhas do CSV (uma por loja×país).
                </span>
            </div>
            <div class="rmbz-mr-filter-pills">
                <?php foreach ( ['pending'=>['⏳','Pendente'], 'approved'=>['✓','Aprovado'], 'rejected'=>['✗','Rejeitado'], 'all'=>['📋','Todos']] as $key => $labelarr ):
                    $is_active = ($filter === $key);
                ?>
                    <a href="?page=rmbz-admin-distribution&tab=match_review&mr_status=<?php echo esc_attr($key); ?>"
                       class="rmbz-mr-pill <?php echo $is_active ? 'is-active' : ''; ?> rmbz-mr-pill-<?php echo esc_attr($key); ?>">
                        <span class="rmbz-mr-pill-icon"><?php echo esc_html($labelarr[0]); ?></span>
                        <span class="rmbz-mr-pill-label"><?php echo esc_html($labelarr[1]); ?></span>
                        <?php if ( isset($group_counts[$key]) && $group_counts[$key] > 0 ): ?>
                            <span class="rmbz-mr-pill-count">
                                <?php echo (int) $group_counts[$key]; ?>
                                <?php if ( isset($counts[$key]) && (int) $counts[$key]->n !== (int) $group_counts[$key] ): ?>
                                    <small>/<?php echo (int) $counts[$key]->n; ?></small>
                                <?php endif; ?>
                            </span>
                        <?php endif; ?>
                    </a>
                <?php endforeach; ?>
            </div>
        </div>

        <?php
        // [1.5.207] CARD: Top artistas com dinheiro retido + ordenação
        // Só aparece em filtro 'pending' onde faz mais sentido.
        if ( $filter === 'pending' && ! empty( $top_artists_data ) ):
            $top_8 = array_slice( $top_artists_data, 0, 8 );
            $remaining = max( 0, count($top_artists_data) - 8 );
            $total_pending_usd = 0;
            foreach ( $top_artists_data as $a ) {
                $total_pending_usd += (float) $a->total_amount_usd;
            }

            // URL builder helper preservando outros params
            $build_url = function( $changes = [] ) {
                $args = $_GET;
                foreach ( $changes as $k => $v ) {
                    if ( $v === null || $v === '' ) {
                        unset( $args[$k] );
                    } else {
                        $args[$k] = $v;
                    }
                }
                return '?' . http_build_query( $args );
            };
        ?>
            <div class="rmbz-mr-top-artists-card">
                <div class="rmbz-mr-tac-head">
                    <div>
                        <strong>💰 Top artistas com dinheiro retido</strong>
                        <small style="color:#666; display:block; margin-top:2px;">
                            <?php echo count($top_artists_data); ?> artista(s) · total $<?php echo number_format($total_pending_usd, 2); ?> USD pendente
                            <?php if ( $filter_artist !== '' ): ?>
                                · <span style="color:var(--rmbz-info-strong, #2271b1); font-weight:600;">filtrado por: <?php echo esc_html($filter_artist); ?></span>
                                <a href="<?php echo esc_url( $build_url( ['mr_artist' => null] ) ); ?>" style="color:#dc3545; text-decoration:none; margin-left:6px;">✗ limpar</a>
                            <?php endif; ?>
                        </small>
                    </div>
                    <small style="color:var(--rmbz-text-faint, #888);">clique no artista para filtrar</small>
                </div>

                <div class="rmbz-mr-tac-grid">
                    <?php foreach ( $top_8 as $a ):
                        $is_active = ( $filter_artist === $a->csv_artist );
                        $card_url = $is_active
                            ? $build_url( ['mr_artist' => null] )
                            : $build_url( ['mr_artist' => $a->csv_artist] );
                    ?>
                        <a href="<?php echo esc_url($card_url); ?>"
                           class="rmbz-mr-tac-card <?php echo $is_active ? 'is-active' : ''; ?>"
                           title="<?php echo $is_active ? 'Click para desfiltrar' : 'Click para filtrar por este artista'; ?>">
                            <div class="rmbz-mr-tac-name"><?php echo esc_html( $a->csv_artist ?: '(sem nome)' ); ?></div>
                            <div class="rmbz-mr-tac-value">$<?php echo number_format( (float) $a->total_amount_usd, 2 ); ?></div>
                            <div class="rmbz-mr-tac-meta"><?php echo (int) $a->group_count; ?> faixa(s)</div>
                        </a>
                    <?php endforeach; ?>
                </div>

                <?php if ( $remaining > 0 ): ?>
                    <details class="rmbz-mr-tac-more">
                        <summary>▶ Ver todos os <?php echo count($top_artists_data); ?> artistas (+<?php echo $remaining; ?>)</summary>
                        <table class="rmbz-mr-tac-table">
                            <thead>
                                <tr>
                                    <th>Artista</th>
                                    <th style="text-align:right;">Faixas</th>
                                    <th style="text-align:right;">Streams</th>
                                    <th style="text-align:right;">Total USD</th>
                                    <th></th>
                                </tr>
                            </thead>
                            <tbody>
                                <?php foreach ( $top_artists_data as $a ):
                                    $is_active = ( $filter_artist === $a->csv_artist );
                                    $card_url = $is_active
                                        ? $build_url( ['mr_artist' => null] )
                                        : $build_url( ['mr_artist' => $a->csv_artist] );
                                ?>
                                    <tr class="<?php echo $is_active ? 'is-active' : ''; ?>">
                                        <td><strong><?php echo esc_html( $a->csv_artist ?: '(sem nome)' ); ?></strong></td>
                                        <td style="text-align:right;"><?php echo (int) $a->group_count; ?></td>
                                        <td style="text-align:right;"><?php echo number_format( (int) $a->total_streams ); ?></td>
                                        <td style="text-align:right;"><strong>$<?php echo number_format( (float) $a->total_amount_usd, 2 ); ?></strong></td>
                                        <td>
                                            <a href="<?php echo esc_url($card_url); ?>" class="button button-small">
                                                <?php echo $is_active ? '✗ Limpar' : '🔍 Filtrar'; ?>
                                            </a>
                                        </td>
                                    </tr>
                                <?php endforeach; ?>
                            </tbody>
                        </table>
                    </details>
                <?php endif; ?>
            </div>
        <?php endif; ?>

        <?php
        // [1.5.207] DROPDOWN DE ORDENAÇÃO — sempre visível
        if ( ! isset($build_url) ) {
            $build_url = function( $changes = [] ) {
                $args = $_GET;
                foreach ( $changes as $k => $v ) {
                    if ( $v === null || $v === '' ) {
                        unset( $args[$k] );
                    } else {
                        $args[$k] = $v;
                    }
                }
                return '?' . http_build_query( $args );
            };
        }
        $sort_labels = [
            'value'      => '💰 Maior valor primeiro',
            'artist'     => '👤 Agrupado por artista',
            'confidence' => '✓ Maior confiança primeiro',
            'date'       => '📅 Mais antigo primeiro',
        ];
        ?>
        <div class="rmbz-mr-sort-bar">
            <label for="rmbz-mr-sort-select">Ordenar por:</label>
            <select id="rmbz-mr-sort-select" onchange="window.location.href = this.value;">
                <?php foreach ( $sort_labels as $key => $label ):
                    $sort_url = $build_url( ['sort' => $key] );
                ?>
                    <option value="<?php echo esc_url($sort_url); ?>" <?php selected($sort, $key); ?>>
                        <?php echo esc_html($label); ?>
                    </option>
                <?php endforeach; ?>
            </select>
            <small style="color:var(--rmbz-text-faint, #888); margin-left:auto;">
                Mostrando <?php echo count($rows); ?> grupo(s)
            </small>
        </div>

        <?php if ( $filter === 'pending' && ( $pending_with_conflict > 0 || $pending_low_confidence > 0 ) ): ?>
            <div style="background:#fff3cd; border:1px solid #ffc107; border-left-width:4px; padding:14px 18px; margin:16px 0; border-radius:4px;">
                <strong style="color:#856404;">⚡ Acções em massa para limpar a fila rapidamente:</strong>
                <div style="margin-top:10px; display:flex; gap:8px; flex-wrap:wrap;">
                    <?php if ( $pending_with_conflict > 0 ): ?>
                        <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"
                              onsubmit="return confirm('Rejeitar TODAS as <?php echo (int) $pending_with_conflict; ?> linhas pendentes com conflito de artista?\n\nIsto vai marcar como rejeitadas todas as linhas onde o artista do CSV é diferente do artista do release na BD.');">
                            <?php wp_nonce_field('rmbz_match_bulk_reject'); ?>
                            <input type="hidden" name="action" value="rmbz_match_bulk_reject">
                            <input type="hidden" name="filter_type" value="conflict">
                            <button type="submit" class="button" style="background:#dc3545; color:#fff; border-color:#dc3545;">
                                ⚠ Rejeitar todos com conflito (<?php echo (int) $pending_with_conflict; ?>)
                            </button>
                        </form>
                    <?php endif; ?>
                    <?php if ( $pending_low_confidence > 0 ): ?>
                        <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"
                              onsubmit="return confirm('Rejeitar TODAS as <?php echo (int) $pending_low_confidence; ?> linhas pendentes com confiança < 50%?\n\nIsto inclui matches por UPC só (com ou sem conflito) e matches fuzzy fracos.');">
                            <?php wp_nonce_field('rmbz_match_bulk_reject'); ?>
                            <input type="hidden" name="action" value="rmbz_match_bulk_reject">
                            <input type="hidden" name="filter_type" value="low_confidence">
                            <button type="submit" class="button">
                                Rejeitar todos com confiança &lt; 50% (<?php echo (int) $pending_low_confidence; ?>)
                            </button>
                        </form>
                    <?php endif; ?>
                </div>
            </div>
        <?php endif; ?>

        <?php if ( $unsynced_matches > 0 ): ?>
            <div style="background:#fde4e2; border:1px solid #f5b1ad; border-left:4px solid #dc3545; padding:14px 18px; margin:16px 0; border-radius:4px;">
                <strong style="color:#921a13;">⚠ Matches aprovados sem crédito na wallet</strong>
                <p style="margin:6px 0 10px; font-size:13px; color:#5c1410;">
                    Há <strong><?php echo (int) $unsynced_matches; ?></strong> matches aprovados antes da correcção do bug que <strong>não creditaram</strong>
                    a wallet do artista (total: <strong>$<?php echo number_format($unsynced_amount, 2); ?></strong>).
                    O ledger e analytics estão correctos, mas o saldo levantável está em falta.
                    Sincronizar agora vai creditar essas wallets retroactivamente — é seguro e idempotente
                    (usa o mesmo marker <code>[match_review:ID]</code>, impossível duplicar).
                </p>
                <div style="display:flex; gap:8px; flex-wrap:wrap;">
                    <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"
                          onsubmit="return confirm('Creditar retroactivamente <?php echo (int) $unsynced_matches; ?> wallets (total $<?php echo number_format($unsynced_amount, 2); ?>)?\n\nA operação é idempotente — se correr outra vez não credita o mesmo match duas vezes.');">
                        <?php wp_nonce_field('rmbz_match_backfill_wallets'); ?>
                        <input type="hidden" name="action" value="rmbz_match_backfill_wallets">
                        <button type="submit" class="button button-primary" style="background:#dc3545; border-color:#dc3545;">
                            🔄 Sincronizar wallets agora (<?php echo (int) $unsynced_matches; ?>)
                        </button>
                    </form>
                </div>
            </div>
        <?php endif; ?>

        <?php if ( empty($rows) ): ?>
            <div class="rmbz-mr-empty">
                <div class="rmbz-mr-empty-icon">✨</div>
                <h2 class="rmbz-mr-empty-title">Sem matches no estado <code><?php echo esc_html($filter); ?></code></h2>
                <p class="rmbz-mr-empty-desc">
                    <?php if ($filter === 'pending'): ?>
                        Tudo limpo! Não há nada a rever neste momento.
                    <?php elseif ($filter === 'approved'): ?>
                        Ainda nenhum match foi aprovado.
                    <?php elseif ($filter === 'rejected'): ?>
                        Ainda nenhum match foi rejeitado.
                    <?php else: ?>
                        A tabela <code>rmbz_distro_match_review</code> está vazia neste filtro.
                    <?php endif; ?>
                </p>
            </div>
        <?php else: ?>

            <?php
            // [FEAT 1.6.20] PAINEL DE APROVAÇÃO EM MASSA POR ARTISTA
            //
            // Para cada artista com matches pendentes acima do threshold (default 90%),
            // permitir aprovação em batch num único clique. Threshold ajustável.
            //
            // Reutiliza o handler `rmbz_match_approve_group_handler` que aceita uma
            // lista CSV de IDs.
            //
            // Só aparece se existir pelo menos 1 artista com ≥2 matches que cumpram
            // o threshold (caso contrário aprovar 1 por 1 é igual).
            $bulk_threshold = isset($_GET['bulk_threshold']) ? max(80, min(100, (int)$_GET['bulk_threshold'])) : 90;
            $bulk_by_artist = []; // [tel => [ 'name', 'ids' => [], 'amount' => 0.0, 'methods' => [] ]]
            foreach ( $rows as $r ) {
                if ( $r->status !== 'pending' ) continue;
                $conf = (int) ( $r->match_confidence ?? 0 );
                if ( $conf < $bulk_threshold ) continue;
                if ( ! empty( $r->artist_conflict ) ) continue; // nunca em massa quando há conflito
                $tel = $r->suggested_artist_telemovel ?: '';
                if ( ! $tel ) continue;
                if ( ! isset( $bulk_by_artist[$tel] ) ) {
                    $bulk_by_artist[$tel] = [
                        'name'      => $r->artist_name ?: $tel,
                        'ids'       => [],
                        'amount'    => 0.0,
                        'tracks'    => 0,
                        'methods'   => [],
                        'min_conf'  => 100,
                    ];
                }
                $row_ids = explode(',', $r->all_ids);
                foreach ( $row_ids as $rid ) {
                    $rid = (int) $rid;
                    if ( $rid > 0 ) $bulk_by_artist[$tel]['ids'][] = $rid;
                }
                $bulk_by_artist[$tel]['amount'] += (float) $r->total_amount_usd;
                $bulk_by_artist[$tel]['tracks']++;
                $bulk_by_artist[$tel]['methods'][ $r->match_method ] = true;
                if ( $conf < $bulk_by_artist[$tel]['min_conf'] ) {
                    $bulk_by_artist[$tel]['min_conf'] = $conf;
                }
            }
            // Mostrar só artistas com ≥2 matches (poupar tempo real)
            $bulk_by_artist = array_filter( $bulk_by_artist, function( $a ) { return count( $a['ids'] ) >= 2; } );
            // Ordenar: mais matches primeiro
            uasort( $bulk_by_artist, function ( $a, $b ) { return count( $b['ids'] ) <=> count( $a['ids'] ); } );
            ?>

            <?php if ( ! empty( $bulk_by_artist ) ): ?>
                <div class="rmbz-mr-bulk">
                    <div class="rmbz-mr-bulk-head">
                        <div>
                            <strong>⚡ Aprovação em massa por artista</strong>
                            <small>Aprovar de uma vez todos os matches deste artista acima do threshold</small>
                        </div>
                        <div class="rmbz-mr-bulk-threshold-wrap">
                            <label>Confiança mínima:</label>
                            <form method="get" style="display:inline-flex; align-items:center; gap:6px;">
                                <input type="hidden" name="page" value="rmbz-admin-distribution">
                                <input type="hidden" name="tab" value="match_review">
                                <?php
                                // Preservar filtros activos (status, etc)
                                foreach ( $_GET as $gk => $gv ) {
                                    if ( in_array($gk, ['page','tab','bulk_threshold'], true) ) continue;
                                    if ( ! is_string($gv) && ! is_numeric($gv) ) continue;
                                ?>
                                    <input type="hidden" name="<?php echo esc_attr($gk); ?>" value="<?php echo esc_attr($gv); ?>">
                                <?php } ?>
                                <select name="bulk_threshold" onchange="this.form.submit()">
                                    <option value="80" <?php selected($bulk_threshold, 80); ?>>≥ 80%</option>
                                    <option value="85" <?php selected($bulk_threshold, 85); ?>>≥ 85%</option>
                                    <option value="90" <?php selected($bulk_threshold, 90); ?>>≥ 90% (recomendado)</option>
                                    <option value="95" <?php selected($bulk_threshold, 95); ?>>≥ 95%</option>
                                    <option value="100" <?php selected($bulk_threshold, 100); ?>>= 100% (só exactos)</option>
                                </select>
                            </form>
                        </div>
                    </div>

                    <div class="rmbz-mr-bulk-list">
                        <?php foreach ( $bulk_by_artist as $tel => $info ):
                            $methods_str = implode(', ', array_keys($info['methods']));
                            $confirm_msg = sprintf(
                                '✓ APROVAR %d match(es) do %s?\\n\\nReceita total: $%s USD\\nMétodos: %s\\nConfiança mínima: %d%%\\n\\nAcção pode ser revertida via "Desfazer aprovação" depois.',
                                count($info['ids']),
                                addslashes($info['name']),
                                number_format($info['amount'], 4),
                                $methods_str,
                                $info['min_conf']
                            );
                        ?>
                            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"
                                  class="rmbz-mr-bulk-row"
                                  onsubmit="return confirm('<?php echo esc_attr($confirm_msg); ?>');">
                                <?php wp_nonce_field('rmbz_match_action_group'); ?>
                                <input type="hidden" name="action" value="rmbz_match_approve_group">
                                <input type="hidden" name="all_ids" value="<?php echo esc_attr( implode(',', $info['ids']) ); ?>">

                                <div class="rmbz-mr-bulk-artist">
                                    <strong>🎤 <?php echo esc_html( $info['name'] ); ?></strong>
                                    <small><code><?php echo esc_html($tel); ?></code></small>
                                </div>
                                <div class="rmbz-mr-bulk-stats">
                                    <span class="rmbz-mr-bulk-count"><?php echo count($info['ids']); ?> match<?php echo count($info['ids']) === 1 ? '' : 'es'; ?></span>
                                    <span class="rmbz-mr-bulk-amount">$<?php echo number_format($info['amount'], 2); ?></span>
                                    <span class="rmbz-mr-bulk-conf">min. <?php echo $info['min_conf']; ?>%</span>
                                </div>
                                <button type="submit" class="rmbz-mr-bulk-btn">
                                    ✓ Aprovar <?php echo count($info['ids']); ?>
                                </button>
                            </form>
                        <?php endforeach; ?>
                    </div>

                    <div class="rmbz-mr-bulk-hint">
                        💡 Apenas matches <strong>sem conflito de artista</strong> e com <strong>confiança ≥ <?php echo $bulk_threshold; ?>%</strong> aparecem aqui.
                        Artistas com 1 só match não aparecem (basta usar o botão "Aprovar grupo" da tabela em baixo).
                    </div>
                </div>
                <style>
                .rmbz-mr-bulk {
                    background: linear-gradient(135deg, #ecfdf5 0%, #f0fdfa 100%);
                    border: 1px solid #6ee7b7;
                    border-left: 4px solid #10b981;
                    border-radius: 10px;
                    padding: 14px 18px;
                    margin: 0 0 20px 0;
                }
                .rmbz-mr-bulk-head {
                    display: flex; justify-content: space-between;
                    align-items: center; gap: 14px; flex-wrap: wrap;
                    margin-bottom: 12px;
                }
                .rmbz-mr-bulk-head strong { color: #065f46; font-size: 14px; }
                .rmbz-mr-bulk-head small { display: block; color: #047857; margin-top: 2px; font-size: 12px; }
                .rmbz-mr-bulk-threshold-wrap {
                    display: flex; align-items: center; gap: 8px;
                    font-size: 12px; color: #065f46;
                }
                .rmbz-mr-bulk-threshold-wrap select {
                    padding: 4px 8px; border-radius: 5px;
                    border: 1px solid #6ee7b7; background: #fff;
                    font-size: 12px;
                }
                .rmbz-mr-bulk-list {
                    background: #fff;
                    border-radius: 7px;
                    overflow: hidden;
                }
                .rmbz-mr-bulk-row {
                    display: grid;
                    grid-template-columns: 1.5fr 1.5fr auto;
                    gap: 14px;
                    align-items: center;
                    padding: 10px 14px;
                    border-bottom: 1px solid #f3f4f6;
                }
                .rmbz-mr-bulk-row:last-child { border-bottom: none; }
                .rmbz-mr-bulk-row:hover { background: #f9fafb; }
                .rmbz-mr-bulk-artist strong { font-size: 13px; color: #1a1a1a; }
                .rmbz-mr-bulk-artist small { display: block; color: #6b7280; font-size: 10px; margin-top: 2px; }
                .rmbz-mr-bulk-stats {
                    display: flex; gap: 10px; align-items: center;
                    font-size: 11px; color: #4b5563;
                }
                .rmbz-mr-bulk-count {
                    background: #dbeafe; color: #1e40af;
                    padding: 3px 9px; border-radius: 10px; font-weight: 700;
                }
                .rmbz-mr-bulk-amount {
                    background: linear-gradient(135deg, #10b981, #059669);
                    color: #fff;
                    padding: 3px 9px; border-radius: 10px; font-weight: 700;
                    font-variant-numeric: tabular-nums;
                }
                .rmbz-mr-bulk-conf {
                    background: #f3f4f6; color: #6b7280;
                    padding: 3px 9px; border-radius: 10px; font-size: 10px;
                }
                .rmbz-mr-bulk-btn {
                    background: linear-gradient(135deg, #10b981, #059669);
                    color: #fff; border: none;
                    padding: 7px 16px; border-radius: 6px;
                    font-size: 12px; font-weight: 700;
                    cursor: pointer;
                    box-shadow: 0 1px 2px rgba(16, 185, 129, 0.3);
                    transition: transform 0.1s;
                }
                .rmbz-mr-bulk-btn:hover { transform: translateY(-1px); box-shadow: 0 2px 4px rgba(16, 185, 129, 0.4); }
                .rmbz-mr-bulk-hint {
                    margin-top: 10px;
                    font-size: 11px; color: #047857; line-height: 1.5;
                }
                </style>
            <?php endif; // bulk_by_artist ?>

            <div class="rmbz-mr-table-card">
                <table class="widefat striped rmbz-mr-table">
                <thead>
                    <tr>
                        <th style="width:90px;">Método</th>
                        <th>Faixa do CSV (royalty)</th>
                        <th>Match sugerido</th>
                        <th style="width:110px; text-align:right;">Total</th>
                        <th style="width:200px;">Acções</th>
                    </tr>
                </thead>
                <tbody>
                    <?php foreach ( $rows as $r ): ?>
                        <tr>
                            <td>
                                <?php
                                $method_labels = [
                                    'isrc'               => '<span style="background:#d4edda; color:#155724; padding:3px 8px; border-radius:3px; font-size:11px; font-weight:600;">ISRC</span>',
                                    'upc_title'          => '<span style="background:#fff3cd; color:#856404; padding:3px 8px; border-radius:3px; font-size:11px; font-weight:600;">UPC + Título</span>',
                                    'fuzzy_artist_title' => '<span style="background:#fde4e2; color:#921a13; padding:3px 8px; border-radius:3px; font-size:11px; font-weight:600;">Fuzzy</span>',
                                    'upc_only'           => '<span style="background:#e2e3e5; color:#495057; padding:3px 8px; border-radius:3px; font-size:11px; font-weight:600;">UPC só</span>',
                                ];
                                echo $method_labels[$r->match_method] ?? esc_html($r->match_method);
                                ?>
                                <br>
                                <small style="color:#888;"><?php echo (int) $r->match_confidence; ?>% conf.</small>
                                <?php if ( $r->match_distance !== null && $r->match_distance > 0 ): ?>
                                    <br><small style="color:#999;">dist: <?php echo (int) $r->match_distance; ?></small>
                                <?php endif; ?>
                                <?php if ( ! empty($r->artist_conflict) ): ?>
                                    <br>
                                    <span style="background:#dc3545; color:#fff; padding:2px 6px; border-radius:3px; font-size:10px; font-weight:600; margin-top:4px; display:inline-block;" title="O artista do CSV não corresponde ao artista do release na BD — possível UPC duplicado entre 2 distribuidores diferentes">
                                        ⚠ CONFLITO ARTISTA
                                    </span>
                                <?php endif; ?>
                            </td>
                            <td style="font-size:12px;">
                                <strong><?php echo esc_html($r->csv_track_title); ?></strong>
                                — <?php echo esc_html($r->csv_artist); ?>
                                <br>
                                <small style="color:#666;">
                                    <?php if ( $r->csv_isrc ): ?>ISRC: <code><?php echo esc_html($r->csv_isrc); ?></code> · <?php endif; ?>
                                    <?php if ( $r->csv_upc ): ?>UPC: <code><?php echo esc_html($r->csv_upc); ?></code> · <?php endif; ?>
                                    <?php if ( $r->csv_album ): ?>Álbum: "<?php echo esc_html($r->csv_album); ?>"<?php endif; ?>
                                </small>
                                <br>
                                <small style="color:#888; margin-top:4px; display:inline-block;">
                                    📊 <strong><?php echo number_format( (int) $r->total_streams ); ?></strong> streams
                                    em <strong><?php echo (int) $r->line_count; ?></strong> linhas do CSV
                                    (<?php echo (int) $r->country_count; ?> países)
                                    <?php if ( $r->stores_csv ):
                                        $stores_short = strlen($r->stores_csv) > 60
                                            ? substr($r->stores_csv, 0, 57) . '…'
                                            : $r->stores_csv;
                                    ?>
                                        <br>🏪 <?php echo esc_html($stores_short); ?>
                                    <?php endif; ?>
                                </small>
                            </td>
                            <td style="font-size:12px;">
                                <strong><?php echo esc_html($r->db_track_title ?: '(faixa removida)'); ?></strong>
                                <br>
                                <small>
                                    Artista: <?php echo esc_html($r->artist_name ?: $r->suggested_artist_telemovel); ?>
                                    <br>Release: "<?php echo esc_html($r->db_release_title); ?>"
                                    <?php if ( $r->db_upc ): ?>
                                        <br>UPC BD: <code><?php echo esc_html($r->db_upc); ?></code>
                                    <?php endif; ?>
                                </small>
                            </td>
                            <td style="text-align:right;">
                                <strong style="font-size:14px;">$<?php echo number_format( (float) $r->total_amount_usd, 4 ); ?></strong>
                                <br>
                                <small style="color:#888;">de <?php echo (int) $r->line_count; ?> linhas</small>
                            </td>
                            <td>
                                <?php if ( $r->status === 'pending' ): ?>
                                    <div class="rmbz-mr-actions">
                                        <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"
                                              onsubmit="return confirm('Aprovar esta faixa? Vai atribuir <?php echo (int) $r->line_count; ?> linhas (US$<?php echo number_format( (float) $r->total_amount_usd, 4 ); ?>) ao artista <?php echo esc_js($r->artist_name ?: $r->suggested_artist_telemovel); ?>.');">
                                            <?php wp_nonce_field('rmbz_match_action_group'); ?>
                                            <input type="hidden" name="action" value="rmbz_match_approve_group">
                                            <input type="hidden" name="all_ids" value="<?php echo esc_attr($r->all_ids); ?>">
                                            <button type="submit" class="rmbz-mr-btn rmbz-mr-btn-approve">✓ Aprovar grupo</button>
                                        </form>
                                        <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"
                                              onsubmit="return confirm('Rejeitar este grupo de <?php echo (int) $r->line_count; ?> linhas?');">
                                            <?php wp_nonce_field('rmbz_match_action_group'); ?>
                                            <input type="hidden" name="action" value="rmbz_match_reject_group">
                                            <input type="hidden" name="all_ids" value="<?php echo esc_attr($r->all_ids); ?>">
                                            <button type="submit" class="rmbz-mr-btn rmbz-mr-btn-reject">X Rejeitar grupo</button>
                                        </form>
                                    </div>
                                <?php else: ?>
                                    <small style="color:#666;">
                                        <?php echo esc_html(ucfirst($r->status)); ?>
                                        <?php if ( $r->reviewed_at ): ?>
                                            <br><?php echo esc_html(date('d/m/Y H:i', strtotime($r->reviewed_at))); ?>
                                        <?php endif; ?>
                                        <?php if ( $r->reviewed_by ): ?>
                                            <br>por <code><?php echo esc_html($r->reviewed_by); ?></code>
                                        <?php endif; ?>
                                    </small>
                                <?php endif; ?>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>
            </div><!-- /rmbz-mr-table-card -->
        <?php endif; ?>

        <p class="rmbz-mr-footer-note">
            Mostrar até 200 linhas. Para histórico completo, consultar a tabela <code>rmbz_distro_match_review</code> directamente.
        </p>
    </div>

    <?php /* [1.5.180] Estilo moderno da página Matches a Rever */ ?>
    <style>
    .rmbz-mr-wrap {
        width: 100%;
        max-width: 100%;
        font-family: var(--rmbz-font-body, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
    }
    .rmbz-mr-header {
        margin: 16px 0 24px 0;
    }
    .rmbz-mr-kicker {
        font-size: 10px;
        color: #888;
        text-transform: uppercase;
        letter-spacing: 1.2px;
        font-weight: 600;
        margin-bottom: 4px;
    }
    .rmbz-mr-title {
        font-size: 24px;
        font-weight: 600;
        color: #1a1a1a;
        margin: 0 0 8px 0;
        font-family: var(--rmbz-font-display, inherit);
    }
    .rmbz-mr-subtitle {
        font-size: 14px;
        color: #666;
        margin: 0;
        max-width: 900px;
        line-height: 1.5;
    }
    .rmbz-mr-filters-card {
        background: #fff;
        border: 1px solid #e5e5e5;
        border-radius: 10px;
        padding: 16px 20px;
        margin-bottom: 16px;
    }
    .rmbz-mr-filters-head {
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        margin-bottom: 12px;
        gap: 16px;
        flex-wrap: wrap;
    }
    .rmbz-mr-filters-head strong {
        font-size: 13px;
        color: #1a1a1a;
        font-weight: 600;
    }
    .rmbz-mr-filters-hint {
        font-size: 11px;
        color: #888;
        line-height: 1.5;
    }
    .rmbz-mr-filter-pills {
        display: flex;
        gap: 8px;
        flex-wrap: wrap;
    }

    /* [1.5.207] Top artistas com dinheiro retido + ordenação */
    .rmbz-mr-top-artists-card {
        background: #fffbeb;
        border: 1px solid #f0b90b;
        border-radius: 6px;
        padding: 14px 16px;
        margin: 16px 0;
    }
    .rmbz-mr-tac-head {
        display: flex;
        justify-content: space-between;
        align-items: flex-start;
        margin-bottom: 12px;
        gap: 12px;
        flex-wrap: wrap;
    }
    .rmbz-mr-tac-head strong {
        font-size: 14px;
        color: #1a1a1a;
    }
    .rmbz-mr-tac-grid {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
        gap: 8px;
    }
    .rmbz-mr-tac-card {
        display: block;
        background: #fff;
        border: 1px solid #dcdcde;
        padding: 10px 12px;
        border-radius: 4px;
        text-decoration: none;
        color: #1a1a1a;
        transition: all 0.15s ease;
    }
    .rmbz-mr-tac-card:hover {
        border-color: var(--wp-admin-theme-color, #2271b1);
        box-shadow: 0 2px 4px rgba(0,0,0,0.08);
        transform: translateY(-1px);
    }
    .rmbz-mr-tac-card.is-active {
        background: var(--wp-admin-theme-color, #2271b1);
        color: #fff;
        border-color: var(--wp-admin-theme-color, #2271b1);
    }
    .rmbz-mr-tac-card.is-active:hover {
        background: var(--wp-admin-theme-color-darker-10, #135e96);
    }
    .rmbz-mr-tac-name {
        font-weight: 600;
        font-size: 12px;
        line-height: 1.3;
        word-break: break-word;
    }
    .rmbz-mr-tac-value {
        color: var(--wp-admin-theme-color, #2271b1);
        font-weight: 700;
        font-size: 16px;
        margin: 4px 0;
    }
    .rmbz-mr-tac-card.is-active .rmbz-mr-tac-value {
        color: #fff;
    }
    .rmbz-mr-tac-meta {
        color: #888;
        font-size: 11px;
    }
    .rmbz-mr-tac-card.is-active .rmbz-mr-tac-meta {
        color: rgba(255,255,255,0.85);
    }
    .rmbz-mr-tac-more {
        margin-top: 12px;
        border-top: 1px dashed #f0b90b;
        padding-top: 10px;
    }
    .rmbz-mr-tac-more summary {
        cursor: pointer;
        color: #856404;
        font-size: 13px;
        font-weight: 600;
        padding: 4px 0;
    }
    .rmbz-mr-tac-more summary:hover {
        color: #654d03;
    }
    .rmbz-mr-tac-table {
        width: 100%;
        border-collapse: collapse;
        margin-top: 8px;
        font-size: 13px;
        background: #fff;
        border: 1px solid #dcdcde;
        border-radius: 4px;
        overflow: hidden;
    }
    .rmbz-mr-tac-table th {
        background: #f6f7f7;
        padding: 8px 10px;
        text-align: left;
        font-weight: 600;
        color: #555;
        font-size: 11px;
        text-transform: uppercase;
        letter-spacing: 0.3px;
        border-bottom: 1px solid #dcdcde;
    }
    .rmbz-mr-tac-table td {
        padding: 8px 10px;
        border-bottom: 1px solid #f0f0f1;
    }
    .rmbz-mr-tac-table tr:last-child td {
        border-bottom: none;
    }
    .rmbz-mr-tac-table tr.is-active {
        background: #e7f3fb;
    }
    .rmbz-mr-tac-table tr:hover:not(.is-active) {
        background: #fafafa;
    }
    .rmbz-mr-sort-bar {
        display: flex;
        gap: 8px;
        align-items: center;
        padding: 10px 14px;
        background: #fafafa;
        border: 1px solid #dcdcde;
        border-radius: 4px;
        margin: 12px 0;
        font-size: 13px;
    }
    .rmbz-mr-sort-bar label {
        font-weight: 600;
        color: #555;
        white-space: nowrap;
    }
    #rmbz-mr-sort-select {
        padding: 5px 10px;
        border: 1px solid #dcdcde;
        border-radius: 4px;
        background: #fff;
        font-size: 13px;
        cursor: pointer;
        min-width: 240px;
    }
    #rmbz-mr-sort-select:focus {
        outline: none;
        border-color: var(--wp-admin-theme-color, #2271b1);
        box-shadow: 0 0 0 1px var(--wp-admin-theme-color, #2271b1);
    }

    .rmbz-mr-pill {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        padding: 8px 14px;
        /* [1.5.181] Cores hardcoded — var(--rmbz-text-primary) e
           var(--rmbz-bg-card) podem apontar para tons dark theme em
           alguns contextos (text-primary = white, bg-card = dark).
           Pills aqui são light theme deliberado (admin WP nativo). */
        background: #fff;
        border: 1px solid #d8dade;
        border-radius: 999px;
        text-decoration: none;
        color: #1a1a1a;
        font-size: 13px;
        font-weight: 500;
        transition: all 0.15s ease;
    }
    .rmbz-mr-pill:hover {
        background: #f5f5f7;
        border-color: #b0b3b8;
        color: #000;
    }
    .rmbz-mr-pill.is-active {
        background: var(--rmbz-info, #3b82f6);
        border-color: var(--rmbz-info, #3b82f6);
        color: #fff;
    }
    .rmbz-mr-pill.is-active:hover {
        background: var(--rmbz-info-strong, #2563eb);
        border-color: var(--rmbz-info-strong, #2563eb);
        color: #fff;
    }
    .rmbz-mr-pill-icon {
        font-size: 14px;
        line-height: 1;
    }
    .rmbz-mr-pill-label {
        line-height: 1.4;
    }
    .rmbz-mr-pill-count {
        background: rgba(255,255,255,0.25);
        padding: 1px 8px;
        border-radius: 999px;
        font-size: 11px;
        font-weight: 600;
        line-height: 1.4;
    }
    .rmbz-mr-pill:not(.is-active) .rmbz-mr-pill-count {
        background: var(--rmbz-info, #3b82f6);
        color: #fff;
    }
    .rmbz-mr-pill-count small {
        opacity: 0.7;
        font-weight: 400;
    }
    .rmbz-mr-empty {
        background: #fff;
        border: 1px solid #e5e5e5;
        border-radius: 10px;
        padding: 60px 20px;
        text-align: center;
        margin-top: 20px;
    }
    .rmbz-mr-empty-icon {
        font-size: 56px;
        margin-bottom: 12px;
        line-height: 1;
    }
    .rmbz-mr-empty-title {
        font-size: 18px;
        color: #1a1a1a;
        margin: 0 0 8px 0;
        font-weight: 600;
        font-family: var(--rmbz-font-display, inherit);
    }
    .rmbz-mr-empty-title code {
        background: #f0f0f0;
        padding: 2px 8px;
        border-radius: 4px;
        font-size: 14px;
    }
    .rmbz-mr-empty-desc {
        font-size: 14px;
        color: #666;
        margin: 0;
        max-width: 480px;
        margin-left: auto;
        margin-right: auto;
        line-height: 1.5;
    }
    .rmbz-mr-table-card {
        background: #fff;
        border: 1px solid #e5e5e5;
        border-radius: 10px;
        overflow: hidden;
        margin-top: 16px;
    }
    .rmbz-mr-table {
        margin: 0 !important;
        border: none !important;
        box-shadow: none !important;
    }
    .rmbz-mr-footer-note {
        margin-top: 16px;
        color: #888;
        font-size: 12px;
    }
    .rmbz-mr-footer-note code {
        background: #f0f0f0;
        padding: 1px 6px;
        border-radius: 3px;
        font-size: 11px;
    }

    /* ============================================================
       [1.5.170] BOTÕES APROVAR/REJEITAR — espaço + cores fortes
       Anti mis-click: gap vertical generoso, cores muito distintas.
       ============================================================ */
    .rmbz-mr-actions {
        display: flex;
        flex-direction: column;
        gap: 12px;             /* gap claro entre botões */
        align-items: stretch;
    }
    .rmbz-mr-actions form {
        margin: 0;
        display: block;
    }
    .rmbz-mr-btn {
        width: 100%;
        padding: 10px 16px;
        border-radius: 6px;
        font-size: 13px;
        font-weight: 600;
        cursor: pointer;
        transition: all 0.15s ease;
        line-height: 1.2;
        border: 2px solid transparent;
    }
    .rmbz-mr-btn-approve {
        background: #16a34a;        /* verde forte */
        color: #ffffff;
        border-color: #15803d;
    }
    .rmbz-mr-btn-approve:hover {
        background: #15803d;
        border-color: #166534;
        transform: translateY(-1px);
    }
    .rmbz-mr-btn-reject {
        background: #ffffff;
        color: #b91c1c;             /* texto vermelho */
        border-color: #b91c1c;      /* border vermelho */
    }
    .rmbz-mr-btn-reject:hover {
        background: #b91c1c;
        color: #ffffff;
        border-color: #991b1b;
    }

    /* ============================================================
       [1.5.170] SNACKBAR DE UNDO
       Aparece bottom-right após aprovar/rejeitar grupo.
       Click "Desfazer" → AJAX → reverte tudo.
       ============================================================ */
    .rmbz-mr-snackbar {
        position: fixed;
        bottom: 24px;
        right: 24px;
        z-index: 999999;
        background: #1a1a1a;
        color: #ffffff;
        padding: 14px 18px;
        border-radius: 8px;
        box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
        display: flex;
        align-items: center;
        gap: 14px;
        max-width: 420px;
        font-size: 13px;
        line-height: 1.4;
        animation: rmbzSnackbarIn 0.25s ease-out;
        border-left: 4px solid #16a34a;
    }
    .rmbz-mr-snackbar.is-reject {
        border-left-color: #b91c1c;
    }
    @keyframes rmbzSnackbarIn {
        from { opacity: 0; transform: translateY(20px); }
        to   { opacity: 1; transform: translateY(0); }
    }
    .rmbz-mr-snackbar.is-leaving {
        animation: rmbzSnackbarOut 0.2s ease-in forwards;
    }
    @keyframes rmbzSnackbarOut {
        from { opacity: 1; transform: translateY(0); }
        to   { opacity: 0; transform: translateY(20px); }
    }
    .rmbz-mr-snackbar-msg {
        flex: 1;
        min-width: 0;
    }
    .rmbz-mr-snackbar-undo {
        background: transparent;
        border: 1px solid rgba(255,255,255,0.3);
        color: #f0b90b;
        padding: 6px 14px;
        border-radius: 5px;
        font-size: 12px;
        font-weight: 700;
        cursor: pointer;
        text-transform: uppercase;
        letter-spacing: 0.04em;
        transition: all 0.15s ease;
        flex-shrink: 0;
    }
    .rmbz-mr-snackbar-undo:hover {
        background: #f0b90b;
        color: #000;
        border-color: #f0b90b;
    }
    .rmbz-mr-snackbar-undo:disabled {
        opacity: 0.5;
        cursor: not-allowed;
    }
    .rmbz-mr-snackbar-countdown {
        font-size: 11px;
        opacity: 0.6;
        margin-left: 8px;
    }
    </style>
    <?php
}


/* ============================================================================
 * MENSAGENS UI — usadas pelos handlers via redirect &msg=<key>
 * ============================================================================ */

function rmbz_match_render_messages() {
    if ( ! isset($_GET['msg']) ) return;
    $msg = sanitize_key( $_GET['msg'] );
    $count = isset($_GET['n']) ? (int) $_GET['n'] : 0;

    $messages = [
        'approved'      => ['success', '✓ Match aprovado.'],
        'rejected'      => ['warning', '✗ Match rejeitado.'],
        // [1.5.174] Mensagem clarifica que é necessário aprovar import depois
        'group_approved'=> ['success', "✓ Grupo aprovado: {$count} linha(s) atribuída(s). O artista verá o dinheiro quando o import for aprovado."],
        'group_rejected'=> ['warning', "✗ Grupo rejeitado: {$count} linhas marcadas como rejeitadas."],
        'bulk_done'     => ['warning', "✗ Bulk reject: {$count} linhas marcadas como rejeitadas."],
        'backfill_done' => ['success', "✓ Wallets sincronizadas: {$count} matches creditados retroactivamente."],
        'backfill_none' => ['info',    'ℹ Nenhuma wallet por sincronizar — todos os matches aprovados já têm crédito.'],
        'backfill_part' => ['warning', "⚠ Sincronização parcial: {$count} matches creditados; alguns falharam (ver error_log)."],
        'backfill_disabled' => ['info', 'ℹ Sincronização de wallets desactivada desde 1.5.110 — royalties USD vivem em rmbz_distro_stats e só convertem para MT no Levantamento Inteligente.'],
        'invalid'       => ['error',   '⚠ Match inválido ou já processado.'],
        'no_pending'    => ['error',   '⚠ Sem linhas pendentes para esta acção.'],
        'unclaimed_backfilled' => ['success', "✓ Limpeza concluída: {$count} linha(s) de Não Reclamados marcadas como atribuídas."],
        'undone_approve' => ['info', "✓ Aprovação revertida: {$count} match(es) voltaram a estado pendente."],
        'undone_reject'  => ['info', "✓ Rejeição revertida: {$count} match(es) voltaram a estado pendente."],
    ];
    if ( isset($messages[$msg]) ) {
        list($type, $text) = $messages[$msg];
        echo '<div class="notice notice-' . esc_attr($type) . ' is-dismissible"><p>' . esc_html($text) . '</p></div>';
    }

    // [1.5.170] Snackbar de undo
    // Aparece se redirect tiver ?undo=TOKEN — só para approve_group / reject_group
    if ( in_array( $msg, [ 'group_approved', 'group_rejected' ], true ) && ! empty( $_GET['undo'] ) ) {
        $token = sanitize_text_field( $_GET['undo'] );
        // Validar que existe transient ainda — não mostrar snackbar para token expirado
        if ( ! empty( $token ) && get_transient( 'rmbz_undo_' . $token ) !== false ) {
            $is_reject = ( $msg === 'group_rejected' );
            $action_label = $is_reject ? 'Rejeição' : 'Aprovação';
            $undo_nonce = wp_create_nonce( 'rmbz_match_undo' );
            ?>
            <div class="rmbz-mr-snackbar <?php echo $is_reject ? 'is-reject' : ''; ?>"
                 id="rmbz-mr-snackbar"
                 data-token="<?php echo esc_attr( $token ); ?>"
                 data-nonce="<?php echo esc_attr( $undo_nonce ); ?>"
                 data-action="<?php echo $is_reject ? 'reject' : 'approve'; ?>">
                <div class="rmbz-mr-snackbar-msg">
                    <strong><?php echo esc_html( $action_label ); ?> aplicada</strong>
                    a <?php echo (int) $count; ?> match(es).
                    Mudaste de ideias?
                </div>
                <button type="button" class="rmbz-mr-snackbar-undo" id="rmbz-mr-undo-btn">
                    Desfazer
                </button>
                <span class="rmbz-mr-snackbar-countdown" id="rmbz-mr-countdown">10s</span>
            </div>
            <script>
            (function() {
                var snackbar = document.getElementById('rmbz-mr-snackbar');
                if (!snackbar) return;

                var btn       = document.getElementById('rmbz-mr-undo-btn');
                var countdown = document.getElementById('rmbz-mr-countdown');
                var token     = snackbar.getAttribute('data-token');
                var nonce     = snackbar.getAttribute('data-nonce');
                var action    = snackbar.getAttribute('data-action'); // 'approve' | 'reject'
                var ajaxUrl   = '<?php echo esc_js( admin_url('admin-ajax.php') ); ?>';

                var seconds = 10;
                var timer = setInterval(function() {
                    seconds--;
                    if (seconds <= 0) {
                        clearInterval(timer);
                        snackbar.classList.add('is-leaving');
                        setTimeout(function() {
                            snackbar.parentNode && snackbar.parentNode.removeChild(snackbar);
                        }, 200);
                    } else {
                        countdown.textContent = seconds + 's';
                    }
                }, 1000);

                btn.addEventListener('click', function() {
                    btn.disabled = true;
                    btn.textContent = 'A desfazer...';
                    clearInterval(timer);

                    var fd = new FormData();
                    fd.append('action', 'rmbz_match_undo');
                    fd.append('undo_token', token);
                    fd.append('_nonce', nonce);

                    fetch(ajaxUrl, { method: 'POST', body: fd, credentials: 'same-origin' })
                        .then(function(r) { return r.json(); })
                        .then(function(data) {
                            if (data && data.success) {
                                // Recarregar página com mensagem de sucesso de undo
                                var msgKey = (action === 'reject') ? 'undone_reject' : 'undone_approve';
                                var n = (data.data && data.data.reverted) ? data.data.reverted : 0;
                                window.location.href = '?page=rmbz-admin-distribution&tab=match_review&msg=' + msgKey + '&n=' + n;
                            } else {
                                btn.textContent = 'Falhou';
                                setTimeout(function() {
                                    btn.disabled = false;
                                    btn.textContent = 'Tentar de novo';
                                }, 1500);
                            }
                        })
                        .catch(function() {
                            btn.disabled = false;
                            btn.textContent = 'Erro — tentar';
                        });
                });
            })();
            </script>
            <?php
        }
    }
}


/* ============================================================================
 * HANDLERS — aprovar / rejeitar (individual + grupo + bulk)
 * ============================================================================ */

/**
 * Helper: aprovar UM match individual (usado pelos handlers de grupo e
 * pelo handler legacy individual).
 *
 * Devolve:
 *   - false se já processado ou inexistente
 *   - array (manifesto) com IDs/valores criados, para permitir undo:
 *       [
 *         'match_id'              => int,
 *         'ledger_id'             => int|null,
 *         'stats_artist'          => string (telemovel),
 *         'stats_streams_added'   => int,
 *         'stats_amount_added'    => float,
 *         'daily_match_keys'      => array (chaves das daily linhas tocadas),
 *         'unclaimed_ids_marked'  => array (IDs marcadas como assigned),
 *         'isrc_propagated'       => array | null (track_id + valor antigo),
 *         'upc_propagated'        => array | null (release_id + valor antigo),
 *         'previous_status'       => string,
 *       ]
 *
 * COMPATIBILIDADE: callers antigos esperam bool. (bool)$manifest === true
 * se algum trabalho foi feito (a array não-vazia avalia true), false caso
 * contrário. Logo o wrapper continua a funcionar com `if ( approve_one(X) )`.
 */
function rmbz_match_approve_one( $match_id ) {
    global $wpdb;
    $t = $wpdb->prefix . 'rmbz_distro_match_review';
    $row = $wpdb->get_row( $wpdb->prepare("SELECT * FROM $t WHERE id = %d", $match_id) );
    if ( ! $row || $row->status !== 'pending' ) return false;

    // [1.5.170] Manifesto de mudanças para suportar undo
    $manifest = [
        'match_id'             => (int) $match_id,
        'ledger_id'            => null,
        'stats_artist'         => null,
        'stats_streams_added'  => 0,
        'stats_amount_added'   => 0.0,
        'daily_match_keys'     => [],
        'unclaimed_ids_marked' => [],
        'isrc_propagated'      => null,
        'upc_propagated'       => null,
        'previous_status'      => 'pending',
    ];

    // ─────────────────────────────────────────────────────────────────────
    // ATRIBUIR O ROYALTY ao artista sugerido.
    //
    // O dinheiro tem que entrar nas MESMAS tabelas que o importador usa,
    // senão nunca vai aparecer nas wallets ou analytics:
    //   - rmbz_distro_ledger       → saldo / wallet (a tabela principal)
    //   - rmbz_distro_daily_stats  → analytics por artista/faixa/dia
    //
    // Ambas aceitam `pending_approval` (0/1). Como o admin já deu OK
    // explicitamente nesta linha, entramos já com pending_approval=0
    // (visível imediatamente, sem precisar da aprovação em lote).
    //
    // Este handler não re-calcula splits nem aplica taxa 1MZ — assume
    // que o valor já vem calculado (é o valor que ficou em unclaimed
    // quando o importador processou a linha, que já é o valor líquido
    // do artista).
    // ─────────────────────────────────────────────────────────────────────

    $amount    = (float) $row->csv_amount_usd;
    $streams   = (int)   $row->csv_streams;
    $payee     = $row->suggested_artist_telemovel;
    $track_id  = (int) $row->suggested_track_id;
    $release_id = (int) $row->suggested_release_id;
    $period    = $row->report_date ? substr((string)$row->report_date, 0, 7) : current_time('Y-m');
    $store     = $row->csv_store   ?: 'Unknown';
    $country   = $row->csv_country ?: 'UNK';
    $isrc_src  = $row->csv_isrc    ?: '';
    $upc_src   = $row->csv_upc     ?: '';

    // CORREÇÃO: usar SEMPRE o hash real da importação que originou esta linha.
    // Antes usava `?: '(match_review)'` como fallback, mas isso quebrava
    // a anulação de importações: o revert procura por hash exacto, e
    // entradas com '(match_review)' literal nunca eram apagadas.
    // Se o hash estiver mesmo em falta (linha legacy), abortamos —
    // melhor que criar uma entrada órfã sem rastro.
    $hash = $row->import_hash ?? '';
    if ( empty($hash) ) {
        error_log('[1MZ match_review] Match ' . $match_id . ' sem import_hash — abortado para evitar entrada órfã.');
        return false;
    }

    $reviewer  = wp_get_current_user()->user_login ?: 'admin';

    // 1. LEDGER — entrada financeira para a wallet do artista
    $t_ledger = $wpdb->prefix . 'rmbz_distro_ledger';
    if ( $wpdb->get_var("SHOW TABLES LIKE '$t_ledger'") === $t_ledger ) {
        $desc = sprintf(
            'Match Review (aprovado por %s): "%s" (%s) — %s | $%.4f',
            $reviewer,
            $row->csv_track_title ?: '(sem título)',
            $isrc_src ?: 'sem ISRC',
            $store . '/' . $country,
            $amount
        );
        $wpdb->insert( $t_ledger, [
            'artist_telemovel'  => $payee,
            'amount'            => $amount,
            'currency'          => 'USD',
            'original_amount'   => $amount,
            'original_currency' => 'USD',
            'exchange_rate'     => 1.0,
            'source_name'       => 'Match Review',
            'reporting_period'  => $period,
            'type'              => 'royalty_payout',
            'description'       => $desc,
            'import_hash'       => $hash,
            'isrc'              => $isrc_src,                      // [DS 1.5.115]
            // [1.5.174] Era 0 (visível imediato). Agora 1 — só desbloqueia
            // quando o IMPORT inteiro for aprovado. Resolve paradoxo UX:
            // "import pendente mas artista já vê dinheiro/notificação".
            'pending_approval'  => 1,
        ], [ '%s','%f','%s','%f','%s','%f','%s','%s','%s','%s','%s','%s','%d' ] );

        // [1.5.170] Capturar id do ledger criado para undo
        $manifest['ledger_id'] = (int) $wpdb->insert_id;
    }

    // 2. DAILY STATS — analytics por artista/faixa/dia
    $t_daily = $wpdb->prefix . 'rmbz_distro_daily_stats';
    if ( $wpdb->get_var("SHOW TABLES LIKE '$t_daily'") === $t_daily ) {
        $wpdb->query( $wpdb->prepare(
            "INSERT INTO $t_daily
                (artist_telemovel, release_id, track_id, report_date, streams, sales, country_code, store, import_hash, pending_approval)
             VALUES (%s, %d, %d, %s, %d, %f, %s, %s, %s, 1)
             ON DUPLICATE KEY UPDATE
                streams = streams + %d,
                sales   = sales   + %f",
            $payee, $release_id, $track_id,
            $period . '-01', $streams, $amount, $country, $store, $hash,
            $streams, $amount
        ) );

        // [1.5.170] Guardar chave única para reverter (a unique key inclui import_hash desde 1.5.164)
        $manifest['daily_match_keys'][] = [
            'artist_telemovel' => $payee,
            'release_id'       => $release_id,
            'track_id'         => $track_id,
            'report_date'      => $period . '-01',
            'country_code'     => $country,
            'store'            => $store,
            'import_hash'      => $hash,
            'streams_added'    => (int) $streams,
            'sales_added'      => (float) $amount,
        ];
    }

    // 3. Actualizar status do match para approved
    $wpdb->update( $t, [
        'status'      => 'approved',
        'reviewed_by' => $reviewer,
        'reviewed_at' => current_time('mysql'),
    ], [ 'id' => $match_id ], [ '%s','%s','%s' ], [ '%d' ] );

    // ─────────────────────────────────────────────────────────────────────
    // 4. STATS ACUMULADOS (rmbz_distro_stats) — UPSERT por artista.
    //    Esta tabela é o que aparece como "Lifetime Earnings" no analytics
    //    do artista. Sem este passo, o valor entra na música (ledger +
    //    daily_stats) mas o "total acumulado" do artista não muda — bug
    //    histórico que fazia parecer "valor entra para a música mas não
    //    para o artista".
    // ─────────────────────────────────────────────────────────────────────
    // ─────────────────────────────────────────────────────────────────────
    // [1.5.174] DISTRO_STATS NÃO SOMADO AQUI — espera approve_import.
    //
    // Antes (1.5.110-1.5.173): somávamos imediatamente em rmbz_distro_stats
    // ao aprovar match — o artista via dinheiro logo. Mas isto criava o
    // paradoxo: import "Pendente aprovação" mas dinheiro já visível.
    //
    // Agora: ledger e daily_stats inseridos com pending_approval=1.
    // Quando admin aprovar o IMPORT, approve_import chama recalc_artist_stats
    // que filtra por pending_approval=0 e soma tudo de uma vez.
    //
    // Excepção: se o import já foi aprovado (raro mas possível —
    // admin aprova import primeiro, e só depois aparece em Match Review),
    // somamos imediatamente porque o flippamento pending_approval=0 já
    // aconteceu para os ledger entries existentes mas o NOSSO entry novo
    // entra com pending=1 e fica preso. Detectamos via approval_status do
    // import_log.
    // ─────────────────────────────────────────────────────────────────────
    $import_already_approved = false;
    $t_logs = $wpdb->prefix . 'rmbz_distro_import_logs';
    if ( $wpdb->get_var("SHOW TABLES LIKE '$t_logs'") === $t_logs && ! empty( $hash ) ) {
        $log_status = $wpdb->get_var( $wpdb->prepare(
            "SELECT approval_status FROM $t_logs WHERE file_hash = %s ORDER BY id DESC LIMIT 1",
            $hash
        ) );
        $import_already_approved = ( $log_status === 'approved' );
    }

    if ( $import_already_approved ) {
        // Caso edge: import já foi aprovado antes deste match. Flipamos as
        // entradas que acabámos de criar e somamos stats agora para evitar
        // limbo permanente.
        $t_ledger_x = $wpdb->prefix . 'rmbz_distro_ledger';
        $wpdb->query( $wpdb->prepare(
            "UPDATE $t_ledger_x SET pending_approval = 0
             WHERE id = %d AND pending_approval = 1",
            (int) $manifest['ledger_id']
        ) );
        $t_daily_x = $wpdb->prefix . 'rmbz_distro_daily_stats';
        $wpdb->query( $wpdb->prepare(
            "UPDATE $t_daily_x SET pending_approval = 0
             WHERE artist_telemovel = %s AND release_id = %d AND track_id = %d
               AND report_date = %s AND country_code = %s AND store = %s
               AND import_hash = %s AND pending_approval = 1",
            $payee, $release_id, $track_id,
            $period . '-01', $country, $store, $hash
        ) );

        // Soma stats agora (caso normal pré-1.5.174)
        $t_stats = $wpdb->prefix . 'rmbz_distro_stats';
        if ( $wpdb->get_var("SHOW TABLES LIKE '$t_stats'") === $t_stats ) {
            $wpdb->query( $wpdb->prepare(
                "INSERT INTO $t_stats (artist_telemovel, total_streams, total_earnings, last_updated)
                 VALUES (%s, %d, %f, %s)
                 ON DUPLICATE KEY UPDATE
                    total_streams  = total_streams  + VALUES(total_streams),
                    total_earnings = total_earnings + VALUES(total_earnings),
                    last_updated   = VALUES(last_updated)",
                $payee, $streams, $amount, current_time('mysql')
            ) );

            // Manifest: capturar para undo
            $manifest['stats_artist']        = $payee;
            $manifest['stats_streams_added'] = (int) $streams;
            $manifest['stats_amount_added']  = (float) $amount;
        }
        $manifest['import_was_approved'] = true; // Flag para undo saber se subtrair stats
    } else {
        // Caso normal: import ainda pendente. Stats não somadas.
        // Marcar manifest para undo saber que NÃO deve subtrair stats.
        $manifest['stats_artist']         = $payee;
        $manifest['stats_streams_added']  = 0;
        $manifest['stats_amount_added']   = 0.0;
        $manifest['import_was_approved']  = false;
    }

    // ─────────────────────────────────────────────────────────────────────
    // [DS 1.5.110] WALLET MT NÃO É CREDITADA AUTOMATICAMENTE
    // ─────────────────────────────────────────────────────────────────────
    // Antes deste fix, aprovar um match creditava `rmbz_wallets.balance`
    // (em MT) com o valor em USD — mesmo bug do approve_import.
    //
    // Agora os matches só afectam o ledger USD e o `rmbz_distro_stats`
    // (Conta de Distribuição). O artista vê o royalty na sua página de
    // distribuição em USD e usa o "Levantamento Inteligente" para escolher
    // levantar dessa conta (com conversão ao câmbio na altura).
    //
    // A wallet MT (Conta Principal) só recebe dinheiro através de:
    //   - Vendas de produtos (rifas, etc.)
    //   - Comissões manuais
    //   - Transferências USD→MT explícitas pelo artista
    // ─────────────────────────────────────────────────────────────────────

    // ─────────────────────────────────────────────────────────────────────
    // [1.5.166] PROPAGAR ISRC/UPC PARA A FAIXA E RELEASE
    // ─────────────────────────────────────────────────────────────────────
    // PROBLEMA RESOLVIDO:
    // Quando o admin atribui um match (ex: porque a faixa não tinha ISRC
    // mas o admin sabe que é a música certa), o ISRC do CSV ficava só na
    // tabela match_review. Próxima importação com mesmo ISRC voltava a
    // não fazer match e ia parar à revisão outra vez — admin tinha de
    // aprovar várias vezes a mesma faixa.
    //
    // FIX:
    // Ao aprovar, gravar csv_isrc em tracks.isrc_code (se vazio) e csv_upc
    // em releases.upc_code (se vazio). Próximas importações fazem match
    // automático por ISRC sem ir parar à revisão.
    //
    // SEGURANÇA:
    // - Só preenche se o campo do destino estiver vazio (NULL ou string vazia)
    // - Nunca sobrescreve um valor já existente (preserva integridade)
    // - Se já existir ISRC diferente, log informativo (admin pode investigar
    //   manualmente — pode ser remasterização legítima)
    // ─────────────────────────────────────────────────────────────────────
    if ( $track_id > 0 && ! empty( $isrc_src ) ) {
        $t_tracks = $wpdb->prefix . 'rmbz_distro_tracks';
        $current_isrc = $wpdb->get_var( $wpdb->prepare(
            "SELECT isrc_code FROM $t_tracks WHERE id = %d",
            $track_id
        ) );

        if ( $current_isrc === null || $current_isrc === '' ) {
            // Track sem ISRC — preencher
            $wpdb->update( $t_tracks,
                [ 'isrc_code' => $isrc_src ],
                [ 'id' => $track_id ],
                [ '%s' ],
                [ '%d' ]
            );
            // [1.5.170] Manifest: gravámos um valor novo, undo deve repor NULL
            $manifest['isrc_propagated'] = [
                'track_id'        => (int) $track_id,
                'previous_value'  => null, // era NULL/vazio antes
                'wrote_value'     => $isrc_src,
            ];
        } elseif ( strcasecmp( $current_isrc, $isrc_src ) !== 0 ) {
            // Track tem ISRC DIFERENTE do CSV — não sobrescrever, log para admin
            error_log( sprintf(
                '[1MZ match_review] Match #%d: track #%d já tem ISRC "%s" (CSV traz "%s") — não sobrescrito. Próximos imports com "%s" continuarão a ir para revisão até admin investigar.',
                $match_id, $track_id, $current_isrc, $isrc_src, $isrc_src
            ) );
        }
        // Se for o mesmo ISRC, no-op (já lá está, próximos imports já fazem match auto)
    }

    if ( $release_id > 0 && ! empty( $upc_src ) ) {
        $t_releases = $wpdb->prefix . 'rmbz_distro_releases';
        $current_upc = $wpdb->get_var( $wpdb->prepare(
            "SELECT upc_code FROM $t_releases WHERE id = %d",
            $release_id
        ) );

        if ( $current_upc === null || $current_upc === '' ) {
            // Release sem UPC — preencher
            $wpdb->update( $t_releases,
                [ 'upc_code' => $upc_src ],
                [ 'id' => $release_id ],
                [ '%s' ],
                [ '%d' ]
            );
            // [1.5.170] Manifest: gravámos UPC novo, undo deve repor NULL
            $manifest['upc_propagated'] = [
                'release_id'      => (int) $release_id,
                'previous_value'  => null,
                'wrote_value'     => $upc_src,
            ];
        } elseif ( strcasecmp( $current_upc, $upc_src ) !== 0 ) {
            // Release tem UPC diferente — não sobrescrever, log para admin
            error_log( sprintf(
                '[1MZ match_review] Match #%d: release #%d já tem UPC "%s" (CSV traz "%s") — não sobrescrito.',
                $match_id, $release_id, $current_upc, $upc_src
            ) );
        }
    }

    // ─────────────────────────────────────────────────────────────────────
    // [FEAT 1.6.5] APRENDIZAGEM AUTOMÁTICA — criar alias para próximos imports
    //
    // Quando o admin aprova manualmente um match, gravamos o par
    // (CSV_value → track_id) na tabela de aliases. Próximos imports com o
    // mesmo CSV_value vão directamente ao track correcto (camada 0 do
    // matcher), sem voltar a passar por revisão.
    //
    // Cobrimos 2 cenários (3º cenário "só fuzzy título" fica de fora porque
    // requeria coluna nova e maior risco de aprendizagem errada):
    //
    //   - B) CSV traz ISRC que não bate com nenhuma faixa OU bate em
    //        faixa diferente da escolhida pelo admin → grava alias ISRC.
    //   - D) CSV traz UPC alternativo → grava alias UPC.
    //
    // Usamos INSERT IGNORE — se o alias já existe (uk_value_type), não
    // duplica. Se ficar "incorrecto", o admin pode desactivá-lo no
    // Catálogo de Faixas (soft delete via deleted_at).
    //
    // O manifest guarda os IDs criados para o undo poder apagar.
    // ─────────────────────────────────────────────────────────────────────
    $t_aliases = $wpdb->prefix . 'rmbz_distro_isrc_overrides';
    $manifest['aliases_created'] = [];
    if ( $track_id > 0 && $wpdb->get_var("SHOW TABLES LIKE '$t_aliases'") === $t_aliases ) {

        // Helper local — evita duplicação. Devolve insert_id ou 0 se falhou.
        $insert_alias = function ( $csv_value, $alias_type ) use ( $wpdb, $t_aliases, $track_id, $reviewer, $match_id ) {
            if ( $csv_value === '' ) return 0;
            // Verificar se já existe alias activo para este (value, type)
            $existing = $wpdb->get_var( $wpdb->prepare(
                "SELECT id FROM $t_aliases
                  WHERE csv_value = %s AND alias_type = %s AND deleted_at IS NULL
                  LIMIT 1",
                $csv_value, $alias_type
            ) );
            if ( $existing ) return 0; // já existe, não duplicar
            $ok = $wpdb->insert( $t_aliases, [
                'csv_value'  => $csv_value,
                'alias_type' => $alias_type,
                'track_id'   => (int) $track_id,
                'notes'      => "Auto-criado ao aprovar match #{$match_id}",
                'created_by' => $reviewer,
            ], [ '%s', '%s', '%d', '%s', '%s' ] );
            return $ok ? (int) $wpdb->insert_id : 0;
        };

        // Cenário B: ISRC do CSV
        if ( ! empty( $isrc_src ) ) {
            $new_id = $insert_alias( $isrc_src, 'isrc' );
            if ( $new_id ) {
                $manifest['aliases_created'][] = [ 'id' => $new_id, 'csv_value' => $isrc_src, 'type' => 'isrc' ];
            }
        }
        // Cenário D: UPC do CSV
        if ( ! empty( $upc_src ) ) {
            $new_id = $insert_alias( $upc_src, 'upc' );
            if ( $new_id ) {
                $manifest['aliases_created'][] = [ 'id' => $new_id, 'csv_value' => $upc_src, 'type' => 'upc' ];
            }
        }
    }

    // ─────────────────────────────────────────────────────────────────────
    // [1.5.165] FIX BUG DE COMUNICAÇÃO MATCH_REVIEW ↔ UNCLAIMED
    // ─────────────────────────────────────────────────────────────────────
    // PROBLEMA:
    // No fluxo de import, uma linha que precise de revisão (UPC match, fuzzy,
    // etc.) entra simultaneamente em DUAS tabelas:
    //   - rmbz_distro_match_review (com sugestão para admin rever)
    //   - rmbz_distro_unclaimed    (como royalty não atribuído)
    //
    // Quando o admin aprovava o match aqui, o royalty era atribuído ao
    // artista (ledger + stats) mas a linha em `unclaimed` continuava
    // status='pending' — aparecia infinitamente no painel "Não Reclamados"
    // mesmo já tendo sido atribuída.
    //
    // FIX:
    // Marcar as linhas correspondentes em `unclaimed` como `assigned` ao
    // mesmo tempo. Critério de matching:
    //   - mesmo ISRC (csv_isrc)
    //   - mesmo período (report_date == reporting_period . '-01')
    //   - mesmo import_hash (linhas do mesmo upload)
    //   - status atual = 'pending' (não tocar em assigned/rejected)
    // ─────────────────────────────────────────────────────────────────────
    $t_unclaimed = $wpdb->prefix . 'rmbz_distro_unclaimed';
    if ( $wpdb->get_var("SHOW TABLES LIKE '$t_unclaimed'") === $t_unclaimed ) {
        $unclaimed_cols = $wpdb->get_col("SHOW COLUMNS FROM $t_unclaimed");
        $has_status     = in_array('status', $unclaimed_cols, true);
        $has_isrc       = in_array('isrc', $unclaimed_cols, true);
        $has_report     = in_array('report_date', $unclaimed_cols, true);
        $has_hash       = in_array('import_hash', $unclaimed_cols, true);

        if ( $has_status && $has_isrc && $has_report && ! empty($isrc_src) ) {
            // Construir UPDATE com fallback se import_hash estiver em falta
            // em linhas legacy (pré-1.5.130). Sem hash, fazemos match só por
            // ISRC + período (mais permissivo, mas seguro porque ainda
            // verifica status=pending).
            $extra_data = wp_json_encode([
                'assigned_to'        => $payee,
                'assigned_via'       => 'match_review',
                'match_review_id'    => (int) $match_id,
                'assigned_amount'    => (float) $amount,
                'assigned_at'        => current_time('mysql'),
                'assigned_by'        => $reviewer,
            ]);

            // Verificar se temos coluna `extra_data` ou similar para guardar
            // metadata (defensivo — schema pode variar)
            $has_extra = in_array('extra_data', $unclaimed_cols, true);

            if ( $has_hash && ! empty($hash) && $hash !== '(match_review)' ) {
                // [1.5.170] Capturar IDs antes do update para suportar undo
                $ids_to_mark = $wpdb->get_col( $wpdb->prepare(
                    "SELECT id FROM $t_unclaimed
                     WHERE isrc = %s AND report_date = %s AND import_hash = %s AND status = 'pending'",
                    $isrc_src, $period . '-01', $hash
                ) );
                $manifest['unclaimed_ids_marked'] = array_map('intval', $ids_to_mark);

                // Fluxo normal: matching exacto por hash
                $update_data = [ 'status' => 'assigned' ];
                $update_fmt  = [ '%s' ];
                if ( $has_extra ) {
                    $update_data['extra_data'] = $extra_data;
                    $update_fmt[]              = '%s';
                }
                $where     = [
                    'isrc'        => $isrc_src,
                    'report_date' => $period . '-01',
                    'import_hash' => $hash,
                    'status'      => 'pending',
                ];
                $where_fmt = [ '%s', '%s', '%s', '%s' ];
                $wpdb->update( $t_unclaimed, $update_data, $where, $update_fmt, $where_fmt );
            } else {
                // [1.5.170] Capturar IDs (fallback sem hash)
                $ids_to_mark = $wpdb->get_col( $wpdb->prepare(
                    "SELECT id FROM $t_unclaimed
                     WHERE isrc = %s AND report_date = %s AND status = 'pending'",
                    $isrc_src, $period . '-01'
                ) );
                $manifest['unclaimed_ids_marked'] = array_map('intval', $ids_to_mark);

                // Fallback: linha legacy sem import_hash. Match só por ISRC + período.
                $update_data = [ 'status' => 'assigned' ];
                $update_fmt  = [ '%s' ];
                if ( $has_extra ) {
                    $update_data['extra_data'] = $extra_data;
                    $update_fmt[]              = '%s';
                }
                $where     = [
                    'isrc'        => $isrc_src,
                    'report_date' => $period . '-01',
                    'status'      => 'pending',
                ];
                $where_fmt = [ '%s', '%s', '%s' ];
                $wpdb->update( $t_unclaimed, $update_data, $where, $update_fmt, $where_fmt );
            }
        }
    }

    return $manifest;
}


function rmbz_match_approve_handler() {
    if ( ! current_user_can('manage_options') ) wp_die('Sem permissão.');
    $match_id = (int) ($_POST['match_id'] ?? 0);
    if ( ! $match_id ) wp_die('ID inválido.');
    check_admin_referer('rmbz_match_action_' . $match_id);

    $ok = rmbz_match_approve_one( $match_id );
    $msg = $ok ? 'approved' : 'invalid';
    wp_safe_redirect( admin_url('admin.php?page=rmbz-admin-distribution&tab=match_review&msg=' . $msg) );
    exit;
}
add_action('admin_post_rmbz_match_approve', 'rmbz_match_approve_handler');


function rmbz_match_reject_handler() {
    if ( ! current_user_can('manage_options') ) wp_die('Sem permissão.');
    $match_id = (int) ($_POST['match_id'] ?? 0);
    if ( ! $match_id ) wp_die('ID inválido.');
    check_admin_referer('rmbz_match_action_' . $match_id);

    global $wpdb;
    $t = $wpdb->prefix . 'rmbz_distro_match_review';
    $row = $wpdb->get_row( $wpdb->prepare("SELECT status FROM $t WHERE id = %d", $match_id) );
    if ( ! $row || $row->status !== 'pending' ) {
        wp_safe_redirect( admin_url('admin.php?page=rmbz-admin-distribution&tab=match_review&msg=invalid') );
        exit;
    }

    $wpdb->update( $t, [
        'status'      => 'rejected',
        'reviewed_by' => wp_get_current_user()->user_login ?: 'admin',
        'reviewed_at' => current_time('mysql'),
    ], [ 'id' => $match_id ], [ '%s','%s','%s' ], [ '%d' ] );

    wp_safe_redirect( admin_url('admin.php?page=rmbz-admin-distribution&tab=match_review&msg=rejected') );
    exit;
}
add_action('admin_post_rmbz_match_reject', 'rmbz_match_reject_handler');


/**
 * Aprovar GRUPO — recebe lista de IDs (CSV de inteiros), aplica approve
 * a cada um. Usado pelo botão "✓ Aprovar grupo" da UI agregada.
 */
function rmbz_match_approve_group_handler() {
    if ( ! current_user_can('manage_options') ) wp_die('Sem permissão.');
    check_admin_referer('rmbz_match_action_group');

    $all_ids_raw = (string) ($_POST['all_ids'] ?? '');
    $ids = array_filter( array_map('intval', explode(',', $all_ids_raw)), function($v){ return $v > 0; } );
    if ( empty($ids) ) {
        wp_safe_redirect( admin_url('admin.php?page=rmbz-admin-distribution&tab=match_review&msg=invalid') );
        exit;
    }

    // [1.5.184] Antes de aprovar, capturar metadata do grupo para o hook
    // de notificações inline. Se todos os matches do grupo são da mesma
    // faixa/artista (caso típico — agrupados por loja×país), conseguimos
    // dar um banner único ao artista.
    global $wpdb;
    $t = $wpdb->prefix . 'rmbz_distro_match_review';
    $placeholders = implode(',', array_fill(0, count($ids), '%d'));
    $sql = $wpdb->prepare(
        "SELECT suggested_artist_telemovel, csv_track_title,
                SUM(csv_amount_usd) AS total_usd,
                COUNT(*) AS group_count
         FROM {$t}
         WHERE id IN ({$placeholders})
           AND status = 'pending'
         GROUP BY suggested_artist_telemovel, csv_track_title",
        ...$ids
    );
    $group_meta = $wpdb->get_results( $sql );

    $approved = 0;
    $manifests = []; // [1.5.170] manifestos para undo
    foreach ( $ids as $id ) {
        $result = rmbz_match_approve_one( (int) $id );
        // approve_one devolve array (manifest) em sucesso, false em falha
        if ( is_array( $result ) ) {
            $approved++;
            $manifests[] = $result;
        }
    }

    // [1.5.170] Criar token de undo se algo foi aprovado
    $undo_token = '';
    if ( $approved > 0 && ! empty( $manifests ) ) {
        $undo_token = wp_generate_password( 24, false, false );
        set_transient(
            'rmbz_undo_' . $undo_token,
            [
                'action'    => 'approve_group',
                'manifests' => $manifests,
                'created_by' => wp_get_current_user()->user_login ?: 'admin',
                'created_at' => current_time('mysql'),
            ],
            5 * MINUTE_IN_SECONDS
        );
    }

    // [1.5.184] Disparar hook UMA VEZ por (artista × faixa) — não por linha
    // individual, senão um grupo de 50 linhas de Spotify×países geraria
    // 50 banners. Em vez disso, 1 banner com o total agregado.
    //
    // [1.5.174] GUARDA: só disparar notificação para matches cujo import
    // JÁ foi aprovado (caso edge — admin aprova match num import já aprovado).
    // Se import ainda pendente, a notificação será disparada pela
    // approve_import_handler quando o admin aprovar o import inteiro
    // (ver royalties-approval-handler.php → rmbz_notif_dispatch_royalties_credited).
    //
    // Construir set de hashes que JÁ foram aprovados (a partir dos manifestos).
    $approved_hashes = [];
    foreach ( $manifests as $mf ) {
        if ( ! empty( $mf['import_was_approved'] ) ) {
            // O match_id permite recuperar o hash via DB — mais robusto que
            // gravar no manifesto. Mas para evitar query extra, vamos detectar
            // pela existência de stats_amount_added > 0 (só quem foi flippado
            // soma stats imediatamente).
            if ( ! empty( $mf['stats_amount_added'] ) ) {
                // Marcar este artista como elegível para notif imediata
                $approved_hashes[ $mf['stats_artist'] ?? '' ] = true;
            }
        }
    }

    if ( $approved > 0 && ! empty( $approved_hashes ) ) {
        foreach ( (array) $group_meta as $g ) {
            if ( empty( $g->suggested_artist_telemovel ) ) continue;
            // [1.5.174] Só notificar artistas cujo import já foi aprovado
            if ( empty( $approved_hashes[ $g->suggested_artist_telemovel ] ) ) continue;

            // Buscar nome do artista (best-effort)
            $artist_name = $wpdb->get_var( $wpdb->prepare(
                "SELECT artist_name FROM {$wpdb->prefix}rmbz_artist_profiles
                 WHERE telemovel = %s LIMIT 1",
                $g->suggested_artist_telemovel
            ));

            do_action(
                'rmbz_match_review_group_approved',
                $g->suggested_artist_telemovel,
                $artist_name ?: '',
                (float) $g->total_usd,
                (int) $g->group_count,
                (string) ( $g->csv_track_title ?: '' )
            );
        }
    }

    $redirect = admin_url('admin.php?page=rmbz-admin-distribution&tab=match_review&msg=group_approved&n=' . $approved);
    if ( ! empty( $undo_token ) ) {
        $redirect .= '&undo=' . $undo_token;
    }
    wp_safe_redirect( $redirect );
    exit;
}
add_action('admin_post_rmbz_match_approve_group', 'rmbz_match_approve_group_handler');


/**
 * Rejeitar GRUPO — recebe lista de IDs (CSV), faz UPDATE em massa.
 */
function rmbz_match_reject_group_handler() {
    if ( ! current_user_can('manage_options') ) wp_die('Sem permissão.');
    check_admin_referer('rmbz_match_action_group');

    $all_ids_raw = (string) ($_POST['all_ids'] ?? '');
    $ids = array_filter( array_map('intval', explode(',', $all_ids_raw)), function($v){ return $v > 0; } );
    if ( empty($ids) ) {
        wp_safe_redirect( admin_url('admin.php?page=rmbz-admin-distribution&tab=match_review&msg=invalid') );
        exit;
    }

    global $wpdb;
    $t = $wpdb->prefix . 'rmbz_distro_match_review';
    $reviewer = wp_get_current_user()->user_login ?: 'admin';
    $now = current_time('mysql');
    $placeholders = implode(',', array_fill(0, count($ids), '%d'));

    // [1.5.170] Capturar IDs que estão pending ANTES do update — apenas estes
    // serão revertidos no undo (idempotente: não toca em IDs já não-pending)
    $params_get = $ids;
    $rejected_ids = $wpdb->get_col( $wpdb->prepare(
        "SELECT id FROM $t WHERE status = 'pending' AND id IN ($placeholders)",
        $params_get
    ) );
    $rejected_ids = array_map('intval', $rejected_ids);

    $params = array_merge( ['rejected', $reviewer, $now], $ids );
    $wpdb->query( $wpdb->prepare(
        "UPDATE $t SET status = %s, reviewed_by = %s, reviewed_at = %s
         WHERE status = 'pending' AND id IN ($placeholders)",
        $params
    ) );
    $rejected = (int) $wpdb->rows_affected;

    // [1.5.170] Token de undo para reject — é simples, só repõe status='pending'
    $undo_token = '';
    if ( $rejected > 0 && ! empty( $rejected_ids ) ) {
        $undo_token = wp_generate_password( 24, false, false );
        set_transient(
            'rmbz_undo_' . $undo_token,
            [
                'action'       => 'reject_group',
                'rejected_ids' => $rejected_ids,
                'created_by'   => $reviewer,
                'created_at'   => $now,
            ],
            5 * MINUTE_IN_SECONDS
        );
    }

    $redirect = admin_url('admin.php?page=rmbz-admin-distribution&tab=match_review&msg=group_rejected&n=' . $rejected);
    if ( ! empty( $undo_token ) ) {
        $redirect .= '&undo=' . $undo_token;
    }
    wp_safe_redirect( $redirect );
    exit;
}
add_action('admin_post_rmbz_match_reject_group', 'rmbz_match_reject_group_handler');


/* ============================================================================
 * [1.5.170] HANDLER UNDO — desfazer aprovação/rejeição em massa
 *
 * Recebe `undo_token` via AJAX. Lê transient com manifesto, reverte tudo,
 * apaga transient. Operação atómica em transação.
 *
 * Para approve_group: percorre cada manifesto e reverte:
 *   1. match_review.status: 'approved' → 'pending'
 *   2. ledger: DELETE da linha criada (capturada no manifest)
 *   3. distro_stats: subtrair stats_streams_added e stats_amount_added
 *   4. daily_stats: subtrair valores nas chaves capturadas
 *   5. unclaimed: status 'assigned' → 'pending' nas IDs capturadas
 *   6. tracks.isrc_code: reverter para previous_value (NULL) se o foi propagado
 *   7. releases.upc_code: idem
 *
 * Para reject_group: simples — repor status 'pending' nas IDs capturadas.
 * ============================================================================ */
function rmbz_match_undo_handler() {
    // Nonce + permissão
    if ( ! current_user_can('manage_options') ) {
        wp_send_json_error( [ 'msg' => 'no_permission' ], 403 );
    }
    if ( ! isset($_POST['_nonce']) || ! wp_verify_nonce( $_POST['_nonce'], 'rmbz_match_undo' ) ) {
        wp_send_json_error( [ 'msg' => 'nonce_invalid' ], 403 );
    }

    $token = sanitize_text_field( $_POST['undo_token'] ?? '' );
    if ( empty( $token ) || strlen( $token ) > 50 ) {
        wp_send_json_error( [ 'msg' => 'invalid_token' ], 400 );
    }

    $key = 'rmbz_undo_' . $token;
    $data = get_transient( $key );
    if ( empty( $data ) || ! is_array( $data ) ) {
        wp_send_json_error( [ 'msg' => 'expired_or_invalid' ], 410 );
    }

    global $wpdb;
    $action = $data['action'] ?? '';

    // Apagar transient logo para impedir double-undo (mesmo se algo falhar
    // depois, melhor do que executar 2x em race condition)
    delete_transient( $key );

    if ( $action === 'reject_group' ) {
        // Reverter rejeição: status 'rejected' → 'pending' nas IDs capturadas
        $ids = isset( $data['rejected_ids'] ) ? array_map('intval', (array) $data['rejected_ids']) : [];
        if ( empty( $ids ) ) {
            wp_send_json_success( [ 'reverted' => 0 ] );
        }
        $t = $wpdb->prefix . 'rmbz_distro_match_review';
        $placeholders = implode(',', array_fill(0, count($ids), '%d'));
        $params = array_merge( ['pending', null, null], $ids );
        $wpdb->query( $wpdb->prepare(
            "UPDATE $t SET status = %s, reviewed_by = %s, reviewed_at = %s
             WHERE status = 'rejected' AND id IN ($placeholders)",
            $params
        ) );
        $reverted = (int) $wpdb->rows_affected;
        wp_send_json_success( [ 'reverted' => $reverted, 'action' => 'reject_undo' ] );
    }

    if ( $action === 'approve_group' ) {
        $manifests = isset( $data['manifests'] ) ? (array) $data['manifests'] : [];
        if ( empty( $manifests ) ) {
            wp_send_json_success( [ 'reverted' => 0 ] );
        }

        $t_match     = $wpdb->prefix . 'rmbz_distro_match_review';
        $t_ledger    = $wpdb->prefix . 'rmbz_distro_ledger';
        $t_daily     = $wpdb->prefix . 'rmbz_distro_daily_stats';
        $t_stats     = $wpdb->prefix . 'rmbz_distro_stats';
        $t_unclaimed = $wpdb->prefix . 'rmbz_distro_unclaimed';
        $t_tracks    = $wpdb->prefix . 'rmbz_distro_tracks';
        $t_releases  = $wpdb->prefix . 'rmbz_distro_releases';

        $reverted = 0;
        $wpdb->query('START TRANSACTION');
        try {
            foreach ( $manifests as $m ) {
                $match_id = (int) ( $m['match_id'] ?? 0 );
                if ( $match_id <= 0 ) continue;

                // 1. match_review: status approved → pending
                $wpdb->update( $t_match,
                    [ 'status' => 'pending', 'reviewed_by' => null, 'reviewed_at' => null ],
                    [ 'id' => $match_id, 'status' => 'approved' ],
                    [ '%s', '%s', '%s' ],
                    [ '%d', '%s' ]
                );

                // 2. Ledger: apagar linha que approve_one criou
                if ( ! empty( $m['ledger_id'] ) ) {
                    $wpdb->delete( $t_ledger, [ 'id' => (int) $m['ledger_id'] ], [ '%d' ] );
                }

                // 3. distro_stats: subtrair valores adicionados
                if ( ! empty( $m['stats_artist'] ) ) {
                    $wpdb->query( $wpdb->prepare(
                        "UPDATE $t_stats
                         SET total_streams = GREATEST(0, total_streams - %d),
                             total_earnings = GREATEST(0, total_earnings - %f),
                             last_updated = %s
                         WHERE artist_telemovel = %s",
                        (int) $m['stats_streams_added'],
                        (float) $m['stats_amount_added'],
                        current_time('mysql'),
                        $m['stats_artist']
                    ) );
                }

                // 4. daily_stats: subtrair valores adicionados em cada chave
                if ( ! empty( $m['daily_match_keys'] ) && is_array( $m['daily_match_keys'] ) ) {
                    foreach ( $m['daily_match_keys'] as $k ) {
                        $wpdb->query( $wpdb->prepare(
                            "UPDATE $t_daily
                             SET streams = GREATEST(0, streams - %d),
                                 sales   = GREATEST(0, sales - %f)
                             WHERE artist_telemovel = %s
                               AND release_id = %d
                               AND track_id = %d
                               AND report_date = %s
                               AND country_code = %s
                               AND store = %s
                               AND import_hash = %s",
                            (int) ( $k['streams_added'] ?? 0 ),
                            (float) ( $k['sales_added'] ?? 0 ),
                            $k['artist_telemovel'] ?? '',
                            (int) ( $k['release_id'] ?? 0 ),
                            (int) ( $k['track_id'] ?? 0 ),
                            $k['report_date'] ?? '',
                            $k['country_code'] ?? '',
                            $k['store'] ?? '',
                            $k['import_hash'] ?? ''
                        ) );
                    }
                }

                // 5. unclaimed: status assigned → pending nas IDs capturadas
                if ( ! empty( $m['unclaimed_ids_marked'] ) && is_array( $m['unclaimed_ids_marked'] ) ) {
                    $u_ids = array_map('intval', $m['unclaimed_ids_marked']);
                    $u_ids = array_filter( $u_ids, function($v) { return $v > 0; } );
                    if ( ! empty( $u_ids ) ) {
                        $u_placeholders = implode(',', array_fill(0, count($u_ids), '%d'));
                        // SET extra_data = NULL para limpar metadata de atribuição.
                        // Verificar se a coluna existe (defensivo).
                        $u_cols = $wpdb->get_col("SHOW COLUMNS FROM $t_unclaimed");
                        $has_extra = in_array('extra_data', $u_cols, true);
                        if ( $has_extra ) {
                            $wpdb->query( $wpdb->prepare(
                                "UPDATE $t_unclaimed
                                 SET status = 'pending', extra_data = NULL
                                 WHERE id IN ($u_placeholders) AND status = 'assigned'",
                                $u_ids
                            ) );
                        } else {
                            $wpdb->query( $wpdb->prepare(
                                "UPDATE $t_unclaimed
                                 SET status = 'pending'
                                 WHERE id IN ($u_placeholders) AND status = 'assigned'",
                                $u_ids
                            ) );
                        }
                    }
                }

                // 6. tracks.isrc_code: reverter se foi propagado novo
                if ( ! empty( $m['isrc_propagated'] ) && is_array( $m['isrc_propagated'] ) ) {
                    $track_id = (int) ( $m['isrc_propagated']['track_id'] ?? 0 );
                    $wrote = $m['isrc_propagated']['wrote_value'] ?? '';
                    if ( $track_id > 0 && ! empty( $wrote ) ) {
                        // Só reverter se o valor actual ainda for o que escrevemos
                        // (evitar conflito se entretanto algo mais alterou)
                        $current = $wpdb->get_var( $wpdb->prepare(
                            "SELECT isrc_code FROM $t_tracks WHERE id = %d",
                            $track_id
                        ) );
                        if ( $current !== null && strcasecmp( $current, $wrote ) === 0 ) {
                            $wpdb->update( $t_tracks,
                                [ 'isrc_code' => null ],
                                [ 'id' => $track_id ],
                                [ '%s' ],
                                [ '%d' ]
                            );
                        }
                    }
                }

                // 7. releases.upc_code: idem
                if ( ! empty( $m['upc_propagated'] ) && is_array( $m['upc_propagated'] ) ) {
                    $release_id = (int) ( $m['upc_propagated']['release_id'] ?? 0 );
                    $wrote = $m['upc_propagated']['wrote_value'] ?? '';
                    if ( $release_id > 0 && ! empty( $wrote ) ) {
                        $current = $wpdb->get_var( $wpdb->prepare(
                            "SELECT upc_code FROM $t_releases WHERE id = %d",
                            $release_id
                        ) );
                        if ( $current !== null && strcasecmp( $current, $wrote ) === 0 ) {
                            $wpdb->update( $t_releases,
                                [ 'upc_code' => null ],
                                [ 'id' => $release_id ],
                                [ '%s' ],
                                [ '%d' ]
                            );
                        }
                    }
                }

                // 8. [FEAT 1.6.5] Aliases criados na aprovação — soft delete
                //    (não APAGAR de vez para manter rasto auditável).
                if ( ! empty( $m['aliases_created'] ) && is_array( $m['aliases_created'] ) ) {
                    $t_aliases = $wpdb->prefix . 'rmbz_distro_isrc_overrides';
                    foreach ( $m['aliases_created'] as $a ) {
                        $alias_id = (int) ( $a['id'] ?? 0 );
                        if ( $alias_id > 0 ) {
                            $wpdb->update( $t_aliases,
                                [
                                    'deleted_at'    => current_time('mysql'),
                                    'deleted_by'    => $reviewer,
                                    'delete_reason' => 'Match aprovado foi revertido (aprendizagem cancelada)',
                                ],
                                [ 'id' => $alias_id ],
                                [ '%s', '%s', '%s' ],
                                [ '%d' ]
                            );
                        }
                    }
                }

                $reverted++;
            }

            $wpdb->query('COMMIT');
        } catch ( Exception $e ) {
            $wpdb->query('ROLLBACK');
            error_log('[1MZ match_review] Undo falhou: ' . $e->getMessage());
            wp_send_json_error( [ 'msg' => 'rollback', 'error' => $e->getMessage() ], 500 );
        }

        wp_send_json_success( [ 'reverted' => $reverted, 'action' => 'approve_undo' ] );
    }

    wp_send_json_error( [ 'msg' => 'unknown_action' ], 400 );
}
add_action('wp_ajax_rmbz_match_undo', 'rmbz_match_undo_handler');


/**
 * BULK REJECT — rejeita TODAS as linhas pendentes que correspondem a um
 * filtro: 'conflict' (artist_conflict=1) ou 'low_confidence' (<50%).
 *
 * Útil para limpar rapidamente a fila quando a maioria são casos óbvios.
 */
function rmbz_match_bulk_reject_handler() {
    if ( ! current_user_can('manage_options') ) wp_die('Sem permissão.');
    check_admin_referer('rmbz_match_bulk_reject');

    $filter_type = sanitize_key( $_POST['filter_type'] ?? '' );
    if ( ! in_array($filter_type, ['conflict','low_confidence'], true) ) {
        wp_safe_redirect( admin_url('admin.php?page=rmbz-admin-distribution&tab=match_review&msg=invalid') );
        exit;
    }

    global $wpdb;
    $t = $wpdb->prefix . 'rmbz_distro_match_review';
    $reviewer = wp_get_current_user()->user_login ?: 'admin';
    $now = current_time('mysql');

    if ( $filter_type === 'conflict' ) {
        $sql = "UPDATE $t SET status = %s, reviewed_by = %s, reviewed_at = %s
                WHERE status = 'pending' AND artist_conflict = 1";
        $wpdb->query( $wpdb->prepare( $sql, 'rejected', $reviewer, $now ) );
    } else {
        // low_confidence: < 50%
        $sql = "UPDATE $t SET status = %s, reviewed_by = %s, reviewed_at = %s
                WHERE status = 'pending' AND match_confidence < 50";
        $wpdb->query( $wpdb->prepare( $sql, 'rejected', $reviewer, $now ) );
    }
    $rejected = (int) $wpdb->rows_affected;

    if ( $rejected === 0 ) {
        wp_safe_redirect( admin_url('admin.php?page=rmbz-admin-distribution&tab=match_review&msg=no_pending') );
        exit;
    }

    wp_safe_redirect( admin_url('admin.php?page=rmbz-admin-distribution&tab=match_review&msg=bulk_done&n=' . $rejected) );
    exit;
}
add_action('admin_post_rmbz_match_bulk_reject', 'rmbz_match_bulk_reject_handler');


/* ============================================================================
 * BACKFILL DE WALLETS — sincronizar matches aprovados que NÃO chegaram a
 * creditar a wallet do artista (bug histórico antes do fix em
 * rmbz_match_approve_one que adicionou o passo 5 — crédito real na wallet).
 *
 * Antes do fix, aprovar um match só inseria no ledger + daily_stats + stats
 * (Lifetime Earnings), mas o saldo levantável (rmbz_wallets.balance) ficava
 * intocado. Resultado: o artista via royalties no analytics mas não os
 * podia levantar.
 *
 * Esta função, disparada por botão admin, percorre todos os matches com
 * status='approved' e credita os que ainda não foram processados pela
 * wallet. É IDEMPOTENTE: o marker [match_review:ID] em wallet_transactions
 * garante que correr duas vezes não duplica nada.
 *
 * NOTA: não faz START TRANSACTION em volta de tudo porque
 * rmbz_update_wallet_balance já é atómico (faz UPDATE + INSERT em sequência),
 * e queremos que falhas individuais não cancelem os créditos bem-sucedidos.
 * ============================================================================ */
function rmbz_match_backfill_wallets_handler() {
    if ( ! current_user_can('manage_options') ) wp_die('Sem permissão.');
    check_admin_referer('rmbz_match_backfill_wallets');

    // [1.5.164] DESACTIVADO PÓS-1.5.110.
    // Esta função creditava `rmbz_wallets.balance` (MT) com valor numérico
    // USD — sem conversão de câmbio. Era um bug arquitectural: $2.17 USD
    // tornava-se 2.17 MT em vez dos ~140 MT reais. A 1.5.110 corrigiu este
    // comportamento em rmbz_match_approve_one (matches deixaram de creditar
    // wallet MT). Esta função, se executada, REINTRODUZ o bug.
    //
    // Mantemos a função por compatibilidade histórica mas redirecionamos
    // para mensagem informativa e não fazemos nenhuma alteração à BD.
    wp_safe_redirect(
        admin_url('admin.php?page=rmbz-admin-distribution&tab=match_review&msg=backfill_disabled')
    );
    exit;

    // ────────── CÓDIGO LEGADO ABAIXO — NÃO EXECUTADO ──────────
    if ( ! function_exists('rmbz_update_wallet_balance') ) {
        wp_safe_redirect( admin_url('admin.php?page=rmbz-admin-distribution&tab=match_review&msg=invalid') );
        exit;
    }

    global $wpdb;
    $t       = $wpdb->prefix . 'rmbz_distro_match_review';
    $t_trans = $wpdb->prefix . 'rmbz_wallet_transactions';

    if ( $wpdb->get_var("SHOW TABLES LIKE '$t'") !== $t ) {
        wp_safe_redirect( admin_url('admin.php?page=rmbz-admin-distribution&tab=match_review&msg=invalid') );
        exit;
    }
    if ( $wpdb->get_var("SHOW TABLES LIKE '$t_trans'") !== $t_trans ) {
        wp_safe_redirect( admin_url('admin.php?page=rmbz-admin-distribution&tab=match_review&msg=invalid') );
        exit;
    }

    // Apanhar TODOS os matches aprovados elegíveis. Mesmo critério da query
    // de contagem na render — usamos NOT EXISTS para excluir já creditados.
    // Limit de segurança: 10000 por execução (caso real para já <500).
    $rows = $wpdb->get_results(
        "SELECT mr.id, mr.suggested_artist_telemovel, mr.csv_amount_usd,
                mr.csv_track_title, mr.csv_isrc
         FROM $t mr
         WHERE mr.status = 'approved'
           AND mr.suggested_artist_telemovel IS NOT NULL
           AND mr.suggested_artist_telemovel != ''
           AND mr.csv_amount_usd > 0
           AND mr.import_hash IS NOT NULL
           AND mr.import_hash != ''
           AND NOT EXISTS (
               SELECT 1 FROM $t_trans tx
               WHERE tx.user_telemovel = mr.suggested_artist_telemovel
                 AND tx.type = 'royalty_payout'
                 AND tx.details LIKE CONCAT('%[match_review:', mr.id, ']%')
           )
         ORDER BY mr.id ASC
         LIMIT 10000"
    );

    if ( empty($rows) ) {
        wp_safe_redirect( admin_url('admin.php?page=rmbz-admin-distribution&tab=match_review&msg=backfill_none') );
        exit;
    }

    $credited = 0;
    $failed   = 0;

    foreach ( $rows as $r ) {
        $payee  = $r->suggested_artist_telemovel;
        $amount = (float) $r->csv_amount_usd;
        if ( $amount <= 0 || empty($payee) ) continue;

        // Re-confirmação anti-race: re-verificar dentro do loop. Se outro
        // processo creditou entretanto (improvável mas possível), pulamos.
        $marker = '[match_review:' . (int) $r->id . ']';
        $already = (int) $wpdb->get_var( $wpdb->prepare(
            "SELECT COUNT(*) FROM $t_trans
             WHERE user_telemovel = %s
               AND type = 'royalty_payout'
               AND details LIKE %s",
            $payee, '%' . $wpdb->esc_like($marker) . '%'
        ) );
        if ( $already > 0 ) continue;

        $desc = sprintf(
            '[BACKFILL] Royalty Match Review — "%s" (%s) — $%.4f %s',
            $r->csv_track_title ?: '(sem título)',
            $r->csv_isrc ?: 'sem ISRC',
            $amount,
            $marker
        );

        $res = rmbz_update_wallet_balance(
            $payee,
            $amount,
            'royalty_payout',
            null,
            $desc
        );

        if ( ! empty($res['success']) ) {
            $credited++;
        } else {
            $failed++;
            error_log(sprintf(
                '[1MZ match_backfill] FALHOU crédito match #%d a %s: %s',
                $r->id, $payee, $res['error'] ?? '?'
            ));
        }
    }

    $msg_key = ( $failed > 0 ) ? 'backfill_part' : 'backfill_done';
    wp_safe_redirect( admin_url(
        'admin.php?page=rmbz-admin-distribution&tab=match_review&msg=' . $msg_key . '&n=' . $credited
    ) );
    exit;
}
add_action('admin_post_rmbz_match_backfill_wallets', 'rmbz_match_backfill_wallets_handler');


/* ============================================================================
 * BACKFILL — preencher import_hash em linhas legacy do match_review
 *
 * Bug histórico: ao chamar rmbz_match_save_for_review(), o import-processor.php
 * passava `$import_hash ?? ''` (variável que não existia, devia ser `$hash`).
 * Resultado: TODAS as linhas pendentes ficaram com import_hash = '' vazio.
 *
 * Combinado com o fix recente que aborta aprovações com hash vazio, o admin
 * fica preso — não consegue aprovar nem nada.
 *
 * Esta função tenta descobrir o hash correcto de cada linha "pendente sem
 * hash" cruzando-a com rmbz_distro_import_logs por (reporting_period,
 * source_name) — o match único de uma importação naquele período.
 * Corre uma vez por load do admin (idempotente).
 * ============================================================================ */
function rmbz_match_review_backfill_hashes() {
    if ( ! is_admin() ) return;
    if ( ! current_user_can('manage_options') ) return;
    if ( get_option('rmbz_match_review_hash_backfill_v') === '1' ) return;

    global $wpdb;
    $t_mr   = $wpdb->prefix . 'rmbz_distro_match_review';
    $t_logs = $wpdb->prefix . 'rmbz_distro_import_logs';

    if ( $wpdb->get_var("SHOW TABLES LIKE '$t_mr'") !== $t_mr )   return;
    if ( $wpdb->get_var("SHOW TABLES LIKE '$t_logs'") !== $t_logs ) return;

    // Linhas com hash vazio
    $orphans = $wpdb->get_results(
        "SELECT id, report_date FROM $t_mr
         WHERE (import_hash IS NULL OR import_hash = '')
           AND status = 'pending'"
    );

    if ( empty($orphans) ) {
        update_option('rmbz_match_review_hash_backfill_v', '1');
        return;
    }

    // Agrupar por período (YYYY-MM)
    $by_period = [];
    foreach ( $orphans as $o ) {
        $period = substr( (string) $o->report_date, 0, 7 );
        if ( empty($period) ) continue;
        $by_period[$period][] = (int) $o->id;
    }

    $total_fixed = 0;
    foreach ( $by_period as $period => $ids ) {
        // Encontrar a importação activa mais recente desse período
        // que ainda esteja pendente ou aprovada (não anulada/rejeitada)
        $log = $wpdb->get_row( $wpdb->prepare(
            "SELECT file_hash FROM $t_logs
             WHERE reporting_period = %s
               AND (status IS NULL OR status != 'reverted')
             ORDER BY id DESC LIMIT 1",
            $period
        ) );

        if ( ! $log || empty($log->file_hash) ) {
            // Sem importação activa para este período — manter NULL, são órfãos legítimos
            continue;
        }

        // Atualizar todas as linhas deste período em batch
        $placeholders = implode(',', array_fill(0, count($ids), '%d'));
        $params = array_merge( [ $log->file_hash ], $ids );
        $r = $wpdb->query( $wpdb->prepare(
            "UPDATE $t_mr SET import_hash = %s WHERE id IN ($placeholders)",
            $params
        ) );
        if ( $r > 0 ) {
            $total_fixed += (int) $r;
            error_log(sprintf(
                '[1MZ match_review backfill] %d linhas do período %s actualizadas com hash %s',
                $r, $period, substr($log->file_hash, 0, 8)
            ));
        }
    }

    update_option('rmbz_match_review_hash_backfill_v', '1');

    if ( $total_fixed > 0 ) {
        set_transient('rmbz_mr_backfill_notice', sprintf(
            '✓ %d linhas do match_review tiveram o import_hash recuperado.',
            $total_fixed
        ), 60 * 5);
    }
}
add_action('admin_init', 'rmbz_match_review_backfill_hashes', 95);

add_action('admin_notices', function() {
    $msg = get_transient('rmbz_mr_backfill_notice');
    if ( $msg && current_user_can('manage_options') ) {
        echo '<div class="notice notice-success is-dismissible"><p><strong>1MZ:</strong> ' . esc_html($msg) . '</p></div>';
        delete_transient('rmbz_mr_backfill_notice');
    }
});


/* ============================================================================
 * [1.5.165] BACKFILL UNCLAIMED — limpar linhas órfãs após aprovação histórica
 *
 * Antes deste fix, aprovar um match em `rmbz_distro_match_review` atribuía
 * o royalty ao artista (ledger + stats) MAS deixava a linha correspondente
 * em `rmbz_distro_unclaimed` com status='pending' — aparecia infinitamente
 * no painel "Não Reclamados" mesmo já tendo dinheiro atribuído.
 *
 * Esta função, disparada por botão admin, percorre matches APROVADOS e
 * marca as linhas correspondentes em unclaimed como `assigned`. É IDEMPOTENTE:
 * só toca em linhas com status='pending'.
 *
 * Critério de matching:
 *   - mesmo ISRC
 *   - mesmo período (report_date)
 *   - mesmo import_hash (se ambas as tabelas tiverem)
 *   - status atual = 'pending'
 * ============================================================================ */
function rmbz_unclaimed_backfill_assigned_handler() {
    if ( ! current_user_can('manage_options') ) wp_die('Sem permissão.');
    check_admin_referer('rmbz_unclaimed_backfill_assigned');

    global $wpdb;
    $t_match     = $wpdb->prefix . 'rmbz_distro_match_review';
    $t_unclaimed = $wpdb->prefix . 'rmbz_distro_unclaimed';

    if ( $wpdb->get_var("SHOW TABLES LIKE '$t_match'") !== $t_match ||
         $wpdb->get_var("SHOW TABLES LIKE '$t_unclaimed'") !== $t_unclaimed ) {
        wp_safe_redirect( admin_url('admin.php?page=rmbz-admin-distribution&tab=match_review&msg=invalid') );
        exit;
    }

    // Verificar colunas antes de fazer queries
    $u_cols = $wpdb->get_col("SHOW COLUMNS FROM $t_unclaimed");
    if ( ! in_array('status', $u_cols, true) || ! in_array('isrc', $u_cols, true) || ! in_array('report_date', $u_cols, true) ) {
        wp_safe_redirect( admin_url('admin.php?page=rmbz-admin-distribution&tab=match_review&msg=invalid') );
        exit;
    }
    $u_has_hash  = in_array('import_hash', $u_cols, true);
    $u_has_extra = in_array('extra_data', $u_cols, true);

    // Apanhar todos os matches aprovados com ISRC + reporting_period + hash
    $rows = $wpdb->get_results(
        "SELECT id, csv_isrc, suggested_artist_telemovel, csv_amount_usd,
                import_hash, report_date, reviewed_at, reviewed_by
         FROM $t_match
         WHERE status = 'approved'
           AND csv_isrc IS NOT NULL AND csv_isrc != ''
           AND report_date IS NOT NULL
         LIMIT 50000"
    );

    $matched = 0;
    $skipped = 0;

    foreach ( $rows as $r ) {
        // Construir período (YYYY-MM-01) a partir de report_date (que já vem
        // como string YYYY-MM-DD ou YYYY-MM)
        $period_str = substr( (string) $r->report_date, 0, 7 ) . '-01';

        $extra_payload = $u_has_extra ? wp_json_encode([
            'assigned_to'        => $r->suggested_artist_telemovel,
            'assigned_via'       => 'match_review_backfill',
            'match_review_id'    => (int) $r->id,
            'assigned_amount'    => (float) $r->csv_amount_usd,
            'assigned_at'        => $r->reviewed_at ?: current_time('mysql'),
            'assigned_by'        => $r->reviewed_by ?: 'admin',
        ]) : null;

        $update_data = [ 'status' => 'assigned' ];
        $update_fmt  = [ '%s' ];
        if ( $u_has_extra && $extra_payload ) {
            $update_data['extra_data'] = $extra_payload;
            $update_fmt[]              = '%s';
        }

        if ( $u_has_hash && ! empty($r->import_hash) && $r->import_hash !== '(match_review)' ) {
            // Match exacto por hash
            $where     = [
                'isrc'        => $r->csv_isrc,
                'report_date' => $period_str,
                'import_hash' => $r->import_hash,
                'status'      => 'pending',
            ];
            $where_fmt = [ '%s', '%s', '%s', '%s' ];
        } else {
            // Fallback sem hash — match só por ISRC + período
            $where     = [
                'isrc'        => $r->csv_isrc,
                'report_date' => $period_str,
                'status'      => 'pending',
            ];
            $where_fmt = [ '%s', '%s', '%s' ];
        }

        $res = $wpdb->update( $t_unclaimed, $update_data, $where, $update_fmt, $where_fmt );
        if ( $res === false ) {
            $skipped++;
        } else if ( $res > 0 ) {
            $matched += $res;
        }
    }

    set_transient(
        'rmbz_unclaimed_backfill_notice',
        sprintf(
            '✓ Limpeza concluída: %d linha(s) de Não Reclamados marcadas como atribuídas (%d matches aprovados verificados).',
            $matched, count($rows)
        ),
        60 * 5
    );

    wp_safe_redirect(
        admin_url('admin.php?page=rmbz-admin-distribution&tab=match_review&msg=unclaimed_backfilled&n=' . $matched)
    );
    exit;
}
add_action('admin_post_rmbz_unclaimed_backfill_assigned', 'rmbz_unclaimed_backfill_assigned_handler');

add_action('admin_notices', function() {
    $msg = get_transient('rmbz_unclaimed_backfill_notice');
    if ( $msg && current_user_can('manage_options') ) {
        echo '<div class="notice notice-success is-dismissible"><p><strong>1MZ:</strong> ' . esc_html($msg) . '</p></div>';
        delete_transient('rmbz_unclaimed_backfill_notice');
    }
});
