<?php
// includes/admin/import-processor.php
//
// Processador de importação de relatórios de royalties (CSV).
// Lê o cabeçalho, detecta colunas (ISRC, amount, streams, store, country),
// vincula a faixas existentes pelo ISRC e credita os artistas.
//
// Protecções:
//   - Transacção MySQL (rollback se algo falhar a meio)
//   - Hash SHA-256 do ficheiro para rejeitar re-imports acidentais
//   - Splits automáticos (se a faixa tiver colaboradores em rmbz_track_splits)
//   - Unclaimed (black-box) para ISRCs sem dono identificado
//
// Requer: distribution-schema-patch.php aplicado (tabelas ledger, daily_stats,
// track_splits e UNIQUE em rmbz_distro_stats).

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

/* ============================================================================
 * SCHEMA PATCH — enriquecer rmbz_distro_unclaimed com metadata da faixa
 * ============================================================================
 * Colunas adicionadas (ALTER TABLE seguro, não falha se já existem):
 *   - album               (nome do álbum)
 *   - label               (record label)
 *   - platform            (DSP principal, para legacy)
 *   - country             (país principal, para legacy)
 *   - streams_total       (soma de streams/quantidade)
 *   - line_count          (quantas linhas do CSV foram agregadas neste registo)
 *   - stores              (JSON: {store_name: revenue_share})
 *   - countries           (JSON: {country_code: revenue_share})
 *   - claimed_by, claimed_at
 */
function rmbz_unclaimed_schema_install() {
    global $wpdb;
    $t = $wpdb->prefix . 'rmbz_distro_unclaimed';

    // NÃO usar early-return baseado em get_option — a flag pode estar
    // dessincronizada do estado real da tabela (caso típico: a BD foi
    // apagada/recriada mas a wp_options não, ou vice-versa).
    // Em vez disso, verificamos coluna a coluna sempre — operação rápida
    // (uma única SHOW COLUMNS) e idempotente (ALTER só corre se faltar).

    if ( $wpdb->get_var("SHOW TABLES LIKE '$t'") !== $t ) {
        // Tabela ainda não existe — nada a fazer aqui. O installer principal
        // (functions-setup.php) vai criá-la, e este patch corre depois.
        return;
    }

    $existing = $wpdb->get_col( "SHOW COLUMNS FROM $t" );
    if ( ! is_array($existing) || empty($existing) ) {
        // SHOW COLUMNS falhou — tabela existe mas inacessível. Não insistimos.
        error_log('[1MZ unclaimed_schema_install] SHOW COLUMNS devolveu vazio para ' . $t);
        return;
    }

    $to_add = [
        'album'            => "ADD COLUMN album varchar(255) DEFAULT NULL",
        'label'            => "ADD COLUMN label varchar(255) DEFAULT NULL",
        'platform'         => "ADD COLUMN platform varchar(100) DEFAULT NULL",
        'country'          => "ADD COLUMN country varchar(10) DEFAULT NULL",
        'streams_total'    => "ADD COLUMN streams_total bigint(20) DEFAULT 0 NOT NULL",
        'line_count'       => "ADD COLUMN line_count int(11) DEFAULT 1 NOT NULL",
        'stores'           => "ADD COLUMN stores longtext DEFAULT NULL",
        'countries'        => "ADD COLUMN countries longtext DEFAULT NULL",
        'claimed_by'       => "ADD COLUMN claimed_by varchar(20) DEFAULT NULL",
        'claimed_at'       => "ADD COLUMN claimed_at datetime DEFAULT NULL",
        // Estas duas eram criadas por rmbz_royalties_schema_install (mais abaixo),
        // mas se essa função tiver feito early-return por causa da flag de versão,
        // ficavam em falta. Replicamos aqui para garantir que aparecem também
        // quando este installer corre.
        'import_hash'      => "ADD COLUMN import_hash varchar(64) DEFAULT NULL",
        'pending_approval' => "ADD COLUMN pending_approval tinyint(1) DEFAULT 0 NOT NULL",
    ];
    foreach ( $to_add as $col => $ddl ) {
        if ( ! in_array($col, $existing, true) ) {
            $result = $wpdb->query( "ALTER TABLE $t $ddl" );
            if ( $result === false ) {
                error_log('[1MZ unclaimed_schema_install] Falhou ALTER de ' . $col . ': ' . $wpdb->last_error);
            }
        }
    }

    // Index útil para UPSERT por isrc+período
    $index_exists = $wpdb->get_var( "SHOW INDEX FROM $t WHERE Key_name = 'idx_isrc_period'" );
    if ( ! $index_exists ) {
        $wpdb->query( "ALTER TABLE $t ADD INDEX idx_isrc_period (isrc, report_date)" );
    }

    // Index para import_hash (necessário para localizar/reverter importações)
    $hash_index_exists = $wpdb->get_var( "SHOW INDEX FROM $t WHERE Key_name = 'idx_import_hash'" );
    if ( ! $hash_index_exists ) {
        // Só cria o index se a coluna existir mesmo (defensivo)
        $cols_after = $wpdb->get_col( "SHOW COLUMNS FROM $t" );
        if ( in_array('import_hash', (array) $cols_after, true) ) {
            $wpdb->query( "ALTER TABLE $t ADD INDEX idx_import_hash (import_hash)" );
        }
    }

    // Atualiza a flag para sinalizar "última execução completa" — informativo
    // apenas, já não usamos para early-return.
    update_option('rmbz_unclaimed_enriched_v', '1.1');
}
add_action('admin_init', 'rmbz_unclaimed_schema_install', 8);
add_action('init',       'rmbz_unclaimed_schema_install', 8);


/* ============================================================================
 * SCHEMA — Platform Commissions (registo de quanto a 1MZ ganhou por importação)
 * ============================================================================
 * Cada importação gera 1 linha aqui com:
 *   - valor bruto recebido (o que está no CSV)
 *   - valor pago aos artistas (70%)
 *   - comissão retida pela 1MZ (30%)
 *   - metadados da distribuidora, período, hash
 * Permite queries tipo: "quanto ganhei em Q1 2026?" / "margem média por distribuidora"
 */
function rmbz_platform_commissions_schema_install() {
    if ( get_option('rmbz_platform_commissions_v') === '1.0' ) return;

    global $wpdb;
    $table = $wpdb->prefix . 'rmbz_distro_platform_commissions';
    $charset = $wpdb->get_charset_collate();

    if ( $wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table ) {
        $sql = "CREATE TABLE $table (
            id bigint(20) NOT NULL AUTO_INCREMENT,
            import_hash varchar(64) NOT NULL,
            distributor_name varchar(100) NOT NULL,
            profile_id varchar(50) DEFAULT NULL,
            reporting_period varchar(10) NOT NULL,
            original_currency varchar(10) DEFAULT 'USD',
            exchange_rate decimal(12,6) DEFAULT 1.000000,
            gross_amount_original decimal(14,4) DEFAULT 0,
            gross_amount_usd decimal(14,4) DEFAULT 0,
            artist_share_pct decimal(5,2) DEFAULT 70.00,
            artist_total_usd decimal(14,4) DEFAULT 0,
            onemz_margin_usd decimal(14,4) DEFAULT 0,
            lines_processed int(11) DEFAULT 0,
            lines_unclaimed int(11) DEFAULT 0,
            created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
            PRIMARY KEY (id),
            KEY idx_period (reporting_period),
            KEY idx_distributor (distributor_name),
            UNIQUE KEY uniq_hash (import_hash)
        ) $charset;";

        if ( ! function_exists('dbDelta') ) require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta($sql);
    }

    update_option('rmbz_platform_commissions_v', '1.0');
}
add_action('admin_init', 'rmbz_platform_commissions_schema_install', 9);
add_action('init',       'rmbz_platform_commissions_schema_install', 9);


/* ============================================================================
 * SCHEMA — adicionar import_hash + reverted_at nas tabelas afectadas
 * Permite localizar todas as linhas de uma importação específica e anulá-las
 * ============================================================================ */
function rmbz_royalties_schema_install() {
    // NÃO usar early-return baseado em get_option — a flag pode estar
    // dessincronizada do estado real das tabelas (caso típico: BD apagada
    // mas wp_options preservada). Em vez disso, verificamos coluna a
    // coluna sempre — operação rápida e idempotente.

    global $wpdb;

    // Ledger: import_hash + reverted_at + pending_approval
    $t_ledger = $wpdb->prefix . 'rmbz_distro_ledger';
    if ( $wpdb->get_var("SHOW TABLES LIKE '$t_ledger'") === $t_ledger ) {
        $cols = $wpdb->get_col("SHOW COLUMNS FROM $t_ledger");
        if ( ! in_array('import_hash', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_ledger ADD COLUMN import_hash varchar(64) DEFAULT NULL AFTER reference_id");
            $wpdb->query("ALTER TABLE $t_ledger ADD INDEX idx_import_hash (import_hash)");
        }
        // [DS 1.5.104] import_log_id discrimina entre múltiplos imports do
        // mesmo ficheiro (mesmo file_hash). Linhas antigas ficam NULL e
        // continuam a ser identificadas pelo hash (fallback).
        if ( ! in_array('import_log_id', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_ledger ADD COLUMN import_log_id bigint(20) DEFAULT NULL");
            $wpdb->query("ALTER TABLE $t_ledger ADD INDEX idx_import_log_id (import_log_id)");
        }
        if ( ! in_array('reverted_at', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_ledger ADD COLUMN reverted_at datetime DEFAULT NULL AFTER created_at");
            $wpdb->query("ALTER TABLE $t_ledger ADD INDEX idx_reverted (reverted_at)");
        }
        if ( ! in_array('pending_approval', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_ledger ADD COLUMN pending_approval tinyint(1) DEFAULT 0 NOT NULL");
            $wpdb->query("ALTER TABLE $t_ledger ADD INDEX idx_pending (pending_approval)");
        }
        // [DS 1.5.115] ISRC no ledger — necessário para detecção de
        // "já importado" no fluxo de re-importação adicional.
        if ( ! in_array('isrc', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_ledger ADD COLUMN isrc varchar(20) DEFAULT NULL");
            $wpdb->query("ALTER TABLE $t_ledger ADD INDEX idx_isrc (isrc)");

            // Backfill: popular ISRC nas linhas existentes cruzando com
            // a tabela `rmbz_distro_unclaimed` (que tem ISRC + import_hash).
            // Isto preenche a maior parte do histórico sem precisar de
            // re-processar imports.
            $t_unclaimed = $wpdb->prefix . 'rmbz_distro_unclaimed';
            if ( $wpdb->get_var("SHOW TABLES LIKE '$t_unclaimed'") === $t_unclaimed ) {
                $wpdb->query(
                    "UPDATE $t_ledger l
                     INNER JOIN (
                        SELECT import_hash, isrc, MIN(id) AS first_id
                        FROM $t_unclaimed
                        WHERE isrc IS NOT NULL AND isrc != ''
                        GROUP BY import_hash, isrc
                     ) u ON u.import_hash = l.import_hash
                     SET l.isrc = u.isrc
                     WHERE l.isrc IS NULL
                       AND l.import_hash IS NOT NULL
                       AND l.import_hash != ''"
                );
            }
        }
    }

    // Daily stats: import_hash + import_log_id + pending_approval
    $t_daily = $wpdb->prefix . 'rmbz_distro_daily_stats';
    if ( $wpdb->get_var("SHOW TABLES LIKE '$t_daily'") === $t_daily ) {
        $cols = $wpdb->get_col("SHOW COLUMNS FROM $t_daily");
        if ( ! in_array('import_hash', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_daily ADD COLUMN import_hash varchar(64) DEFAULT NULL");
            $wpdb->query("ALTER TABLE $t_daily ADD INDEX idx_import_hash (import_hash)");
        }
        if ( ! in_array('import_log_id', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_daily ADD COLUMN import_log_id bigint(20) DEFAULT NULL");
            $wpdb->query("ALTER TABLE $t_daily ADD INDEX idx_import_log_id (import_log_id)");
        }
        if ( ! in_array('pending_approval', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_daily ADD COLUMN pending_approval tinyint(1) DEFAULT 0 NOT NULL");
        }
    }

    // Unclaimed: import_hash + import_log_id + pending_approval
    $t_unclaimed = $wpdb->prefix . 'rmbz_distro_unclaimed';
    if ( $wpdb->get_var("SHOW TABLES LIKE '$t_unclaimed'") === $t_unclaimed ) {
        $cols = $wpdb->get_col("SHOW COLUMNS FROM $t_unclaimed");
        if ( ! in_array('import_hash', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_unclaimed ADD COLUMN import_hash varchar(64) DEFAULT NULL");
            $wpdb->query("ALTER TABLE $t_unclaimed ADD INDEX idx_import_hash (import_hash)");
        }
        if ( ! in_array('import_log_id', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_unclaimed ADD COLUMN import_log_id bigint(20) DEFAULT NULL");
            $wpdb->query("ALTER TABLE $t_unclaimed ADD INDEX idx_import_log_id (import_log_id)");
        }
        if ( ! in_array('pending_approval', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_unclaimed ADD COLUMN pending_approval tinyint(1) DEFAULT 0 NOT NULL");
        }
    }

    // Platform commissions: reverted_at + import_log_id
    $t_comm = $wpdb->prefix . 'rmbz_distro_platform_commissions';
    if ( $wpdb->get_var("SHOW TABLES LIKE '$t_comm'") === $t_comm ) {
        $cols = $wpdb->get_col("SHOW COLUMNS FROM $t_comm");
        if ( ! in_array('reverted_at', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_comm ADD COLUMN reverted_at datetime DEFAULT NULL");
        }
        if ( ! in_array('import_log_id', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_comm ADD COLUMN import_log_id bigint(20) DEFAULT NULL");
            $wpdb->query("ALTER TABLE $t_comm ADD INDEX idx_import_log_id (import_log_id)");
        }
    }

    // Import logs: status (active/reverted) + approval workflow
    $t_logs = $wpdb->prefix . 'rmbz_distro_import_logs';
    if ( $wpdb->get_var("SHOW TABLES LIKE '$t_logs'") === $t_logs ) {
        $cols = $wpdb->get_col("SHOW COLUMNS FROM $t_logs");
        if ( ! in_array('status', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_logs ADD COLUMN status varchar(20) DEFAULT 'active' NOT NULL");
        }
        if ( ! in_array('reverted_at', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_logs ADD COLUMN reverted_at datetime DEFAULT NULL");
        }
        if ( ! in_array('reverted_by', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_logs ADD COLUMN reverted_by varchar(60) DEFAULT NULL");
        }
        // Workflow de aprovação em 2 fases
        if ( ! in_array('approval_status', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_logs ADD COLUMN approval_status varchar(20) DEFAULT 'pending_approval' NOT NULL");
            $wpdb->query("ALTER TABLE $t_logs ADD INDEX idx_approval (approval_status)");
        }
        if ( ! in_array('approved_by', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_logs ADD COLUMN approved_by varchar(60) DEFAULT NULL");
        }
        if ( ! in_array('approved_at', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_logs ADD COLUMN approved_at datetime DEFAULT NULL");
        }
        if ( ! in_array('rejection_reason', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_logs ADD COLUMN rejection_reason text DEFAULT NULL");
        }
        // [DS 1.5.108] Arquivo do CSV original para permitir "Desfazer Anulação"
        if ( ! in_array('archived_path', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_logs ADD COLUMN archived_path varchar(500) DEFAULT NULL");
        }
        if ( ! in_array('unrevert_replaced_by', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_logs ADD COLUMN unrevert_replaced_by bigint(20) DEFAULT NULL");
        }
        if ( ! in_array('unrevert_replaces', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_logs ADD COLUMN unrevert_replaces bigint(20) DEFAULT NULL");
        }
        // [DS 1.5.109] Marcador de quando o CSV físico foi limpo (retention)
        if ( ! in_array('archive_purged_at', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_logs ADD COLUMN archive_purged_at datetime DEFAULT NULL");
        }
        // [DS 1.5.115] Importação adicional: aponta para o log original quando
        // re-importamos do mesmo CSV para apanhar linhas que tinham sido
        // desmarcadas no preview anterior.
        if ( ! in_array('additional_import_of', $cols, true) ) {
            $wpdb->query("ALTER TABLE $t_logs ADD COLUMN additional_import_of bigint(20) DEFAULT NULL");
        }
    }

    // Payouts table (pagamentos aos artistas)
    $t_payouts = $wpdb->prefix . 'rmbz_distro_payouts';
    if ( $wpdb->get_var("SHOW TABLES LIKE '$t_payouts'") !== $t_payouts ) {
        $charset = $wpdb->get_charset_collate();
        $sql = "CREATE TABLE $t_payouts (
            id bigint(20) NOT NULL AUTO_INCREMENT,
            artist_telemovel varchar(20) NOT NULL,
            amount_usd decimal(12,4) NOT NULL,
            amount_original decimal(12,4) DEFAULT NULL,
            original_currency varchar(3) DEFAULT 'USD',
            exchange_rate decimal(10,6) DEFAULT 1.000000,
            method varchar(30) NOT NULL DEFAULT 'manual',
            reference_number varchar(100) DEFAULT NULL,
            proof_file varchar(500) DEFAULT NULL,
            status varchar(20) DEFAULT 'completed' NOT NULL,
            notes text,
            paid_by varchar(60) DEFAULT NULL,
            paid_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
            reverted_at datetime DEFAULT NULL,
            PRIMARY KEY  (id),
            KEY idx_artist (artist_telemovel),
            KEY idx_status (status),
            KEY idx_date (paid_at)
        ) $charset;";
        if ( ! function_exists('dbDelta') ) require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta($sql);
    }

    update_option('rmbz_royalties_schema_v', '1.3');

    // Migration retroactiva — primeira vez que corremos, marcamos TUDO como pendente
    // para o admin rever o que já estava no sistema. Os artistas deixam de ver o dinheiro
    // até o admin aprovar manualmente cada importação existente.
    if ( get_option('rmbz_retroactive_pending_applied') !== 'yes' ) {
        $t_logs = $wpdb->prefix . 'rmbz_distro_import_logs';
        $t_ledger = $wpdb->prefix . 'rmbz_distro_ledger';
        $t_daily = $wpdb->prefix . 'rmbz_distro_daily_stats';
        $t_unclaimed = $wpdb->prefix . 'rmbz_distro_unclaimed';
        $t_stats = $wpdb->prefix . 'rmbz_distro_stats';

        if ( $wpdb->get_var("SHOW TABLES LIKE '$t_logs'") === $t_logs ) {
            $wpdb->query( "UPDATE $t_logs SET approval_status = 'pending_approval'
                           WHERE (approval_status IS NULL OR approval_status = '')
                             AND (status IS NULL OR status != 'reverted')" );
        }
        if ( $wpdb->get_var("SHOW TABLES LIKE '$t_ledger'") === $t_ledger ) {
            $lcols = $wpdb->get_col("SHOW COLUMNS FROM $t_ledger");
            if ( in_array('pending_approval', $lcols, true) && in_array('import_hash', $lcols, true) ) {
                $wpdb->query( "UPDATE $t_ledger SET pending_approval = 1
                               WHERE import_hash IS NOT NULL
                                 AND (reverted_at IS NULL)
                                 AND type IN ('royalty_payout','royalty_split')" );
            }
        }
        if ( $wpdb->get_var("SHOW TABLES LIKE '$t_daily'") === $t_daily ) {
            $dcols = $wpdb->get_col("SHOW COLUMNS FROM $t_daily");
            if ( in_array('pending_approval', $dcols, true) && in_array('import_hash', $dcols, true) ) {
                $wpdb->query( "UPDATE $t_daily SET pending_approval = 1 WHERE import_hash IS NOT NULL" );
            }
        }
        if ( $wpdb->get_var("SHOW TABLES LIKE '$t_unclaimed'") === $t_unclaimed ) {
            $ucols = $wpdb->get_col("SHOW COLUMNS FROM $t_unclaimed");
            if ( in_array('pending_approval', $ucols, true) && in_array('import_hash', $ucols, true) ) {
                $wpdb->query( "UPDATE $t_unclaimed SET pending_approval = 1
                               WHERE import_hash IS NOT NULL AND status = 'pending'" );
            }
        }
        // Zerar stats — são recalculadas à medida que o admin aprova cada importação
        if ( $wpdb->get_var("SHOW TABLES LIKE '$t_stats'") === $t_stats ) {
            $wpdb->query( "UPDATE $t_stats SET total_earnings = 0, total_streams = 0, last_updated = NOW()" );
        }

        update_option('rmbz_retroactive_pending_applied', 'yes');
    }
}
add_action('admin_init', 'rmbz_royalties_schema_install', 10);
add_action('init',       'rmbz_royalties_schema_install', 10);


/* ============================================================================
 * HELPER — percentagem de partilha artista (hardcoded 70%, futuramente configurável)
 * ============================================================================ */
function rmbz_get_artist_share_pct() {
    $pct = (float) get_option('rmbz_artist_share_pct', 70);
    if ( $pct <= 0 || $pct > 100 ) $pct = 70;
    return $pct;
}
function rmbz_get_artist_share_factor() {
    return rmbz_get_artist_share_pct() / 100.0;
}

/**
 * Devolve a hash SHA-256 de um ficheiro (para detectar re-imports).
 */
function rmbz_file_hash( $path ) {
    return @hash_file('sha256', $path);
}

/**
 * Devolve true se este hash de ficheiro já foi importado.
 */
function rmbz_csv_already_imported( $hash ) {
    $imported = get_option( 'rmbz_imported_csv_hashes', [] );
    return in_array( $hash, $imported, true );
}

function rmbz_mark_csv_imported( $hash, $filename ) {
    $imported = get_option( 'rmbz_imported_csv_hashes', [] );
    $imported[] = $hash;
    // Manter só os últimos 100 para não inflacionar a wp_options
    if ( count($imported) > 100 ) $imported = array_slice( $imported, -100 );
    update_option( 'rmbz_imported_csv_hashes', $imported, false );
    // Log mais descritivo
    $log = get_option( 'rmbz_import_log', [] );
    $log[] = [ 'hash' => $hash, 'file' => $filename, 'date' => current_time('mysql') ];
    if ( count($log) > 50 ) $log = array_slice( $log, -50 );
    update_option( 'rmbz_import_log', $log, false );
}


/**
 * Regista o import na tabela `rmbz_distro_import_logs` para que apareça
 * na página WP-Admin → Royalties → Importações, pendente de aprovação.
 *
 * Estava em falta — tínhamos a chamada `rmbz_log_csv_import(...)` mas a
 * função nunca fora definida. Resultado: os imports completavam a nível
 * de dados (ledger, unclaimed, commissions) mas NUNCA apareciam na lista
 * de imports para o admin aprovar/anular. O dinheiro caía direto nas
 * wallets sem o workflow de aprovação.
 *
 * Agora cria o registo com approval_status='pending_approval', e o admin
 * tem de ir à página Importações para Aprovar explicitamente antes dos
 * artistas verem o dinheiro (o handler de approval é que faz o UPDATE
 * que desbloqueia as wallets).
 */
/**
 * [DS 1.5.104] Constrói a cláusula WHERE para filtrar linhas de tabelas
 * derivadas (ledger, daily_stats, unclaimed, commissions) ligadas a um
 * import específico.
 *
 * Resolve o problema histórico: o mesmo ficheiro pode ser importado várias
 * vezes (após limpeza de hash duplicado), criando múltiplas linhas em
 * import_logs com o mesmo file_hash. Filtrar só por import_hash apaga/altera
 * dados de TODOS os imports daquele ficheiro, não só do que se quer.
 *
 * Estratégia:
 *   1. Se a tabela tem coluna `import_log_id` E o registo tem essa coluna
 *      populada → usa `import_log_id = X` (preciso, sem ambiguidade)
 *   2. Senão (linhas antigas pre-1.5.104) → usa `import_hash = X AND
 *      reporting_period = Y` (defensivo: período é um discriminador
 *      adicional comum entre imports do mesmo ficheiro feitos em meses
 *      diferentes — não perfeito mas reduz drasticamente o blast radius)
 *
 * Devolve array `[where_sql, params_array]` para usar com $wpdb->prepare.
 *
 * @param object $log    Registo da tabela rmbz_distro_import_logs
 * @param string $table  Nome da tabela onde a query vai correr
 * @return array         [where_sql, [params...]]
 */
function rmbz_build_import_filter( $log, $table ) {
    global $wpdb;
    if ( ! $log || empty($log->id) ) {
        // Defensive — devolve filtro impossível para evitar UPDATE/DELETE em massa
        return [ '1=0', [] ];
    }
    // Verificar quais colunas existem na tabela. Cache por execução.
    static $col_cache = [];
    if ( ! isset($col_cache[$table]) ) {
        $col_cache[$table] = (array) $wpdb->get_col("SHOW COLUMNS FROM $table");
    }
    $cols = $col_cache[$table];
    $has_log_id = in_array('import_log_id', $cols, true);
    $has_period = in_array('reporting_period', $cols, true);
    $has_date   = in_array('report_date', $cols, true);

    // Construir cláusula de período (defensiva para linhas antigas).
    // ledger usa reporting_period (varchar tipo "2026-01");
    // daily_stats usa report_date (date), comparamos com YEAR-MONTH do log.
    $period_clause = '';
    $period_params = [];
    $period = (string) ($log->reporting_period ?? '');
    if ( $period !== '' ) {
        if ( $has_period ) {
            $period_clause = ' AND reporting_period = %s';
            $period_params[] = $period;
        } elseif ( $has_date ) {
            // report_date é tipo date (YYYY-MM-DD). period é "YYYY-MM".
            $period_clause = ' AND DATE_FORMAT(report_date, "%%Y-%%m") = %s';
            $period_params[] = $period;
        }
    }

    if ( $has_log_id ) {
        // Filtro preciso: import_log_id OU (linhas antigas NULL com mesmo hash+período)
        $where = "( import_log_id = %d OR ( import_log_id IS NULL AND import_hash = %s" . $period_clause . " ) )";
        $params = array_merge([ (int) $log->id, $log->file_hash ], $period_params);
        return [ $where, $params ];
    }
    // Tabela ainda sem import_log_id → filtro defensivo
    $where = "( import_hash = %s" . $period_clause . " )";
    $params = array_merge([ $log->file_hash ], $period_params);
    return [ $where, $params ];
}

function rmbz_log_csv_import( $hash, $filename, $source_name, $period, $stats ) {
    global $wpdb;
    $t_logs = $wpdb->prefix . 'rmbz_distro_import_logs';

    // Se a tabela ainda não existe, cria-a (redundante com o schema_install
    // mas defensivo para o caso do admin ainda não ter visitado a página).
    if ( $wpdb->get_var("SHOW TABLES LIKE '$t_logs'") !== $t_logs ) {
        $charset = $wpdb->get_charset_collate();
        $sql = "CREATE TABLE $t_logs (
            id bigint(20) NOT NULL AUTO_INCREMENT,
            file_hash varchar(64) NOT NULL,
            file_name varchar(255) DEFAULT NULL,
            source_name varchar(100) DEFAULT NULL,
            reporting_period varchar(7) DEFAULT NULL,
            processed_rows int(11) DEFAULT 0,
            unclaimed_rows int(11) DEFAULT 0,
            skipped_rows int(11) DEFAULT 0,
            total_amount_usd decimal(15,4) DEFAULT 0.0000,
            imported_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
            imported_by varchar(60) DEFAULT NULL,
            status varchar(20) DEFAULT 'active' NOT NULL,
            reverted_at datetime DEFAULT NULL,
            reverted_by varchar(60) DEFAULT NULL,
            approval_status varchar(20) DEFAULT 'pending_approval' NOT NULL,
            approved_by varchar(60) DEFAULT NULL,
            approved_at datetime DEFAULT NULL,
            rejection_reason text,
            PRIMARY KEY  (id),
            KEY idx_hash (file_hash),
            KEY idx_approval (approval_status),
            KEY idx_imported (imported_at)
        ) $charset;";
        if ( ! function_exists('dbDelta') ) require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta($sql);
    }

    // [DS 1.5.91] Auto-migration agora corre no admin_init (ver função
    // rmbz_log_csv_import_ensure_columns abaixo) — fora de qualquer
    // transação. Aqui apenas verificamos quais colunas existem para
    // saber que campos podemos popular no insert.
    $cols_existing = (array) $wpdb->get_col("SHOW COLUMNS FROM $t_logs");
    $has_errors_json   = in_array('errors_json', $cols_existing, true);
    $has_excluded_rows = in_array('excluded_rows', $cols_existing, true);

    $processed   = (int)   ($stats['processed']        ?? 0);
    $unclaimed   = (int)   ($stats['unclaimed']        ?? 0);
    $skipped     = (int)   ($stats['skipped']          ?? 0);
    $excluded    = (int)   ($stats['excluded']         ?? 0);
    $errors_arr  = isset($stats['errors']) && is_array($stats['errors']) ? $stats['errors'] : [];
    $total_usd   = (float) ($stats['total_usd']
                         ?? $stats['gross_total_usd']
                         ?? $stats['artist_total_usd']
                         ?? 0);

    $current_user = function_exists('wp_get_current_user') ? wp_get_current_user() : null;
    $imported_by  = ($current_user && $current_user->user_login)
        ? $current_user->user_login
        : 'system';

    // Construir array dinâmico — só inclui colunas novas se existirem
    $insert_data = [
        'file_hash'        => $hash,
        'file_name'        => $filename,
        'source_name'      => $source_name,
        'reporting_period' => $period,
        'processed_rows'   => $processed,
        'unclaimed_rows'   => $unclaimed,
        'skipped_rows'     => $skipped,
        'total_amount_usd' => $total_usd,
        'imported_at'      => current_time('mysql'),
        'imported_by'      => $imported_by,
        'status'           => 'active',
        'approval_status'  => 'pending_approval',
    ];
    $insert_format = [ '%s','%s','%s','%s','%d','%d','%d','%f','%s','%s','%s','%s' ];

    if ( $has_excluded_rows ) {
        $insert_data['excluded_rows'] = $excluded;
        $insert_format[] = '%d';
    }
    if ( $has_errors_json && ! empty($errors_arr) ) {
        $insert_data['errors_json'] = wp_json_encode( array_slice($errors_arr, 0, 500) );
        $insert_format[] = '%s';
    }

    $wpdb->insert( $t_logs, $insert_data, $insert_format );

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

    // [DS 1.5.108] Action para outros módulos saberem que um log foi criado
    // (usado pelo unrevert para ligar bidireccionalmente).
    do_action( 'rmbz_after_log_csv_import', $new_id, $hash, $filename );

    return $new_id;
}

/**
 * [DS 1.5.91] Auto-migration das colunas novas do log de imports.
 * Corre em admin_init (fora de qualquer transação) e só faz ALTER se
 * as colunas ainda não existirem. Idempotente, seguro contra dupla
 * execução.
 */
function rmbz_log_csv_import_ensure_columns() {
    global $wpdb;
    $t_logs = $wpdb->prefix . 'rmbz_distro_import_logs';

    // Skip se a tabela ainda não existe (será criada na primeira importação)
    if ( $wpdb->get_var( $wpdb->prepare("SHOW TABLES LIKE %s", $t_logs) ) !== $t_logs ) {
        return;
    }

    $cols = (array) $wpdb->get_col("SHOW COLUMNS FROM $t_logs");
    if ( ! in_array('errors_json', $cols, true) ) {
        $wpdb->query("ALTER TABLE $t_logs ADD COLUMN errors_json longtext DEFAULT NULL");
    }
    if ( ! in_array('excluded_rows', $cols, true) ) {
        $wpdb->query("ALTER TABLE $t_logs ADD COLUMN excluded_rows int(11) DEFAULT 0");
    }
}
add_action( 'admin_init', 'rmbz_log_csv_import_ensure_columns', 25 );

/**
 * Aplica splits (se existirem) e devolve um array de [telemovel => valor] a creditar.
 * Se não houver splits, devolve [owner_telemovel => valor].
 *
 * Lê da tabela rmbz_track_splits (criada pelo módulo distribution-track-editor.php).
 * Colunas usadas: collaborator_telemovel, percentage, invite_status.
 *
 * Splits contados:
 *   - 'accepted'      → creditados na wallet via ledger normal
 *   - 'external_hold' → devolvidos separadamente em $external_holds para
 *                       serem guardados em rmbz_splits_holdings (colaborador
 *                       externo que ainda não se registou na 1MZ)
 *   - 'pending' / 'rejected' → ignorados, valor vai para o owner (remainder)
 *
 * @param int    $track_id
 * @param string $owner_telemovel
 * @param float  $amount_usd
 * @param array  &$external_holds  (output) Array de entradas
 *               [ ['split_id'=>int, 'telemovel'=>str, 'amount'=>float], ... ]
 * @return array [telemovel => valor] — entradas a creditar via ledger
 */
function rmbz_compute_splits( $track_id, $owner_telemovel, $amount_usd, &$external_holds = [] ) {
    global $wpdb;
    $tbl = $wpdb->prefix . 'rmbz_track_splits';
    $external_holds = [];

    // Ignorar se a tabela não existir
    if ( $wpdb->get_var( "SHOW TABLES LIKE '$tbl'" ) !== $tbl ) {
        return [ $owner_telemovel => $amount_usd ];
    }

    // Ler accepted + external_hold (ambos "reservam" percentagem)
    $rows = $wpdb->get_results( $wpdb->prepare(
        "SELECT id, collaborator_telemovel AS payee_telemovel, percentage, invite_status
         FROM $tbl
         WHERE track_id = %d
           AND invite_status IN ('accepted', 'external_hold')
           AND collaborator_telemovel IS NOT NULL
           AND collaborator_telemovel != ''",
        $track_id
    ) );

    if ( empty($rows) ) return [ $owner_telemovel => $amount_usd ];

    $result = [];
    $total_reserved = 0.0;
    foreach ( $rows as $r ) {
        $share = round( $amount_usd * ( (float) $r->percentage / 100 ), 4 );
        $total_reserved += $share;

        if ( $r->invite_status === 'external_hold' ) {
            // NÃO entra em $result (para não creditar na wallet).
            // Vai para $external_holds, tratado pelo call-site que regista
            // em rmbz_splits_holdings.
            $external_holds[] = [
                'split_id'  => (int) $r->id,
                'telemovel' => $r->payee_telemovel,
                'amount'    => $share,
            ];
        } else {
            // accepted — credito normal
            $result[ $r->payee_telemovel ] = ( $result[ $r->payee_telemovel ] ?? 0 ) + $share;
        }
    }

    // Se splits (accepted + external_hold) não somam 100, o restante vai para o owner.
    // IMPORTANTE: não é o "remainder" do $result — é do $amount_usd total menos o
    // já reservado (accepted + hold). Caso contrário o owner recebia também a parte
    // que devia estar em hold.
    $remainder = round( $amount_usd - $total_reserved, 4 );
    if ( $remainder > 0 ) {
        $result[ $owner_telemovel ] = ( $result[ $owner_telemovel ] ?? 0 ) + $remainder;
    }
    return $result;
}

/**
 * Novo processador, mais seguro e correcto.
 */
function rmbz_handle_csv_import_final() {
    if ( ! current_user_can('manage_options') ) wp_die('Sem permissão.');
    check_admin_referer('rmbz_import_final_nonce');

    global $wpdb;

    $temp_filename = sanitize_file_name( $_POST['temp_file'] ?? '' );
    $source_name   = sanitize_text_field( $_POST['source_name'] ?? 'Manual' );
    $period        = sanitize_text_field( $_POST['reporting_period'] ?? '' );
    $currency_orig = sanitize_text_field( $_POST['original_currency'] ?? 'USD' );
    $exchange_rate = isset($_POST['exchange_rate']) ? floatval($_POST['exchange_rate']) : 1.0;
    $profile_id    = sanitize_key( $_POST['profile_id'] ?? 'auto' );
    $profile       = function_exists('rmbz_get_profile') ? rmbz_get_profile($profile_id) : null;

    if ( $currency_orig === 'USD' ) $exchange_rate = 1.0;
    if ( $exchange_rate <= 0 )      $exchange_rate = 1.0;

    // Validar período formato YYYY-MM
    if ( ! preg_match('/^\d{4}-\d{2}$/', $period) ) {
        wp_die('Período de relatório inválido. Use YYYY-MM (ex: 2025-11).');
    }

    $upload_dir = wp_upload_dir();
    $file_path  = $upload_dir['basedir'] . '/rmbz_temp_imports/' . $temp_filename;
    if ( ! file_exists($file_path) ) wp_die('Ficheiro expirou. Reupload necessário.');

    // Anti re-import — detectar se este CSV (por hash) já foi importado antes.
    // O admin pode forçar re-import via ?force=1 + nonce (útil quando uma
    // importação anterior falhou mas deixou o hash guardado, ou quando
    // se quer mesmo re-processar o mesmo ficheiro).
    $hash = rmbz_file_hash( $file_path );

    // [DS 1.5.115] Suporte a "additional import" (re-import de linhas que
    // tinham sido desmarcadas no preview anterior). O hash do ficheiro é
    // o mesmo do original, então geramos um hash sintético para evitar
    // colisões com UNIQUE KEYs (rmbz_distro_platform_commissions, etc.).
    // O CSV em si não é re-cobrado a comissão — só as linhas novas geram
    // ledger entries com o novo hash.
    $is_additional = false;
    $additional_ctx = null;
    if ( function_exists('rmbz_get_additional_import_context') ) {
        $additional_ctx = rmbz_get_additional_import_context( $temp_filename );
        if ( is_array($additional_ctx) && ! empty($additional_ctx['original_log_id']) ) {
            $is_additional = true;
            // Hash sintético: original + sufixo único = não colide
            $hash = $hash . '_add_' . $additional_ctx['original_log_id'] . '_' . time();
        }
    }

    $force = isset($_GET['force']) && $_GET['force'] === '1'
          && isset($_GET['_force_nonce'])
          && wp_verify_nonce( $_GET['_force_nonce'], 'rmbz_force_reimport_' . $hash );

    if ( ! $is_additional && rmbz_csv_already_imported( $hash ) && ! $force ) {
        // Construir URL de "forçar" com nonce
        $force_nonce = wp_create_nonce( 'rmbz_force_reimport_' . $hash );
        $force_url = add_query_arg([
            'temp_filename' => $temp_filename,
            'source_name'   => $source_name ?? 'Manual',
            'period'        => $_POST['period'] ?? '',
            'profile_id'    => $profile_id ?? '',
            'force'         => 1,
            '_force_nonce'  => $force_nonce,
        ], admin_url('admin-post.php?action=rmbz_processar_importacao_final'));

        // Para o botão "Forçar" funcionar via POST (que é o método original
        // do formulário), damos um form submit button em vez de GET.
        // Mas como o handler usa $_POST, vamos reconstruir via form.

        $cancel_url = admin_url('admin.php?page=rmbz-admin-distribution&tab=royalties&rsub=imports');

        // Opção 2: limpar o hash da BD (permite re-upload normal)
        $clear_hash_url = wp_nonce_url(
            admin_url('admin-post.php?action=rmbz_clear_import_hash&hash=' . $hash),
            'rmbz_clear_hash_' . $hash
        );

        @unlink( $file_path );

        // Output HTML bonito em vez de wp_die brutal
        wp_die(
            '<div style="max-width:640px; margin:40px auto; padding:30px; background:#fff; border:1px solid #ddd; border-radius:8px; font-family:-apple-system,BlinkMacSystemFont,sans-serif;">' .
                '<h1 style="margin:0 0 16px 0; font-size:20px; color:#333;">⚠ Ficheiro já importado</h1>' .
                '<p style="color:#555; line-height:1.5;">Este CSV foi processado anteriormente e o sistema tem-no marcado como já importado na BD.</p>' .
                '<p style="color:#555; line-height:1.5; font-size:13px;">Hash: <code style="background:#f4f4f4; padding:2px 6px;">' . substr($hash, 0, 12) . '...</code></p>' .
                '<h3 style="margin-top:24px; font-size:15px; color:#333;">O que fazer?</h3>' .
                '<ul style="color:#555; line-height:1.7;">' .
                    '<li><strong>Se os dados já estão na BD:</strong> não precisas de re-importar. Vai a <a href="' . esc_url($cancel_url) . '">Royalties → Importações</a> para ver a lista.</li>' .
                    '<li><strong>Se a importação anterior falhou e queres tentar de novo:</strong> clica no botão abaixo para limpar o hash e voltar a fazer upload.</li>' .
                '</ul>' .
                '<div style="margin-top:20px; display:flex; gap:10px; flex-wrap:wrap;">' .
                    '<a href="' . esc_url($clear_hash_url) . '" class="button button-primary" onclick="return confirm(\'Limpar o hash deste ficheiro? Isto permite re-importar o mesmo CSV. Se os dados já foram processados, vais ter duplicados.\');" style="padding:8px 16px; background:var(--wp-admin-theme-color, #2271b1); color:#fff; text-decoration:none; border-radius:4px; border:0;">🔄 Limpar hash e permitir re-upload</a>' .
                    '<a href="' . esc_url($cancel_url) . '" class="button" style="padding:8px 16px; background:#f6f7f7; color:#333; text-decoration:none; border-radius:4px; border:1px solid #ccc;">← Voltar</a>' .
                '</div>' .
            '</div>',
            'Ficheiro já importado',
            [ 'response' => 200, 'back_link' => false ]
        );
    }

    // DESPACHO por tipo de perfil — agregados mensais têm fluxo próprio
    if ( $profile && $profile['type'] === 'aggregate_monthly' && function_exists('rmbz_process_tunecore_data_report') ) {
        $handle = fopen( $file_path, 'r' );
        if ( ! $handle ) wp_die('Erro ao abrir ficheiro.');

        $header = fgetcsv($handle);
        if ( isset($header[0]) ) $header[0] = preg_replace('/^\xEF\xBB\xBF/', '', $header[0]);
        $header_lower = array_map('strtolower', array_map('trim', $header));

        $stats = [ 'processed' => 0, 'unclaimed' => 0, 'skipped' => 0, 'total_usd' => 0.0, 'splits' => 0 ];

        $wpdb->query('START TRANSACTION');
        try {
            $result = rmbz_process_tunecore_data_report( $handle, $header_lower, $period, $stats, $hash );
            fclose($handle);
            if ( isset($result['error']) ) {
                $wpdb->query('ROLLBACK');
                wp_die('Erro: ' . esc_html($result['error']));
            }
            $stats = $result;
            // [DS 1.5.104] Capturar ID do log para poder discriminar este
            // import dos outros que partilhem file_hash (após limpezas).
            $import_log_id = rmbz_log_csv_import( $hash, basename($temp_filename), $source_name, $period, $stats );

            // Linhas já criadas em ledger/daily_stats/unclaimed dentro da
            // transação ainda não foram populadas com import_log_id (o ID
            // só existe agora). Atualizar essas linhas em massa.
            if ( $import_log_id > 0 ) {
                $t_ledger_x    = $wpdb->prefix . 'rmbz_distro_ledger';
                $t_daily_x     = $wpdb->prefix . 'rmbz_distro_daily_stats';
                $t_unclaimed_x = $wpdb->prefix . 'rmbz_distro_unclaimed';
                // Cada UPDATE actualiza só as linhas DESTE import (criadas
                // agora com hash + reporting_period + import_log_id IS NULL)
                if ( $wpdb->get_var("SHOW COLUMNS FROM $t_ledger_x LIKE 'import_log_id'") ) {
                    $wpdb->query( $wpdb->prepare(
                        "UPDATE $t_ledger_x SET import_log_id = %d
                         WHERE import_hash = %s AND reporting_period = %s AND import_log_id IS NULL",
                        $import_log_id, $hash, $period
                    ) );
                }
                if ( $wpdb->get_var("SHOW COLUMNS FROM $t_daily_x LIKE 'import_log_id'") ) {
                    $wpdb->query( $wpdb->prepare(
                        "UPDATE $t_daily_x SET import_log_id = %d
                         WHERE import_hash = %s AND DATE_FORMAT(report_date, '%%Y-%%m') = %s AND import_log_id IS NULL",
                        $import_log_id, $hash, $period
                    ) );
                }
                if ( $wpdb->get_var("SHOW COLUMNS FROM $t_unclaimed_x LIKE 'import_log_id'") ) {
                    $wpdb->query( $wpdb->prepare(
                        "UPDATE $t_unclaimed_x SET import_log_id = %d
                         WHERE import_hash = %s AND import_log_id IS NULL",
                        $import_log_id, $hash
                    ) );
                }
            }

            // Registo de comissão também para agregados (o artista resolve no Black Box)
            $gross_total = (float) ($stats['total_usd'] ?? 0);
            $artist_share_pct = rmbz_get_artist_share_pct();
            $artist_total = round( $gross_total * ($artist_share_pct / 100), 4 );

            // [DS 1.5.104] Construir array de insert dinamicamente para
            // incluir import_log_id apenas se a coluna existir
            $comm_data = [
                'import_hash'           => $hash,
                'distributor_name'      => $source_name,
                'profile_id'            => $profile_id,
                'reporting_period'      => $period,
                'original_currency'     => $currency_orig,
                'exchange_rate'         => $exchange_rate,
                'gross_amount_original' => $exchange_rate > 0 ? round($gross_total / $exchange_rate, 4) : $gross_total,
                'gross_amount_usd'      => $gross_total,
                'artist_share_pct'      => $artist_share_pct,
                'artist_total_usd'      => $artist_total,
                'onemz_margin_usd'      => round($gross_total - $artist_total, 4),
                'lines_processed'       => 0,
                'lines_unclaimed'       => (int) $stats['unclaimed'],
                'created_at'            => current_time('mysql'),
            ];
            $t_comm_x = $wpdb->prefix . 'rmbz_distro_platform_commissions';
            if ( $import_log_id > 0 && $wpdb->get_var("SHOW COLUMNS FROM $t_comm_x LIKE 'import_log_id'") ) {
                $comm_data['import_log_id'] = $import_log_id;
            }
            $wpdb->insert( $t_comm_x, $comm_data );

            $wpdb->query('COMMIT');
        } catch ( Exception $e ) {
            $wpdb->query('ROLLBACK');
            @unlink( $file_path );
            wp_die('Erro ao processar: ' . esc_html($e->getMessage()));
        }

        // [DS 1.5.108] Em vez de apagar o CSV, arquivá-lo para permitir
        // "Desfazer Anulação" caso seja preciso voltar atrás.
        if ( $import_log_id > 0 && function_exists('rmbz_archive_import_csv') ) {
            $archived = rmbz_archive_import_csv( $file_path, $import_log_id, $hash );
            if ( ! empty($archived) ) {
                $wpdb->update(
                    $wpdb->prefix . 'rmbz_distro_import_logs',
                    [ 'archived_path' => $archived ],
                    [ 'id' => $import_log_id ],
                    [ '%s' ], [ '%d' ]
                );
            } else {
                // Falha ao arquivar — apagar para não deixar lixo
                @unlink( $file_path );
            }
        } else {
            @unlink( $file_path );
        }
        wp_safe_redirect( admin_url('admin.php?page=rmbz-admin-distribution&tab=import&import_status=done&profile=' . $profile_id . '&bb=' . $stats['unclaimed']) );
        exit;
    }

    $handle = fopen( $file_path, 'r' );
    if ( ! $handle ) wp_die('Erro ao abrir o ficheiro.');

    // Header + BOM strip
    $header = fgetcsv( $handle );
    if ( ! $header ) { fclose($handle); wp_die('CSV vazio.'); }
    if ( isset($header[0]) ) $header[0] = preg_replace('/^\xEF\xBB\xBF/', '', $header[0]);

    $map = array_map( 'strtolower', array_map( 'trim', $header ) );

    // Resolver índices de colunas usando o perfil (com fallbacks genéricos)
    $idx = function_exists('rmbz_resolve_column_indices')
        ? rmbz_resolve_column_indices($map, $profile)
        : [
            'isrc' => array_search('isrc', $map),
            'amount' => false, 'store' => false, 'country' => false, 'streams' => false,
            'track_name' => false, 'artist' => false, 'upc' => false, 'album' => false,
            'label' => false, 'currency' => false,
        ];

    if ( $idx['isrc'] === false || $idx['amount'] === false ) {
        fclose($handle);
        wp_die('Colunas obrigatórias (isrc + amount) não encontradas. Cabeçalho lido: ' . implode(', ', $map));
    }

    $stats = [
        'processed' => 0,
        'unclaimed' => 0,
        'skipped'   => 0,
        'excluded'  => 0,  // [DS 1.5.90] Linhas excluídas por desselecção do admin
        'errors'    => [],  // [DS 1.5.90] Detalhe de linhas com problemas (linha + mensagem)
        'total_usd' => 0.00,
        'splits'    => 0,
        'silent_seen' => 0, // [FIX 1.6.18] Linhas com receita zero (mas com track válido)
                            //              vistas no CSV — usadas para actualizar
                            //              last_seen_in_csv ainda assim, para não
                            //              aparecerem em "Faixas Silenciosas".
    ];
    $isrc_cache = [];

    // [FIX 1.6.18] Faixas que foram skipadas por amount <= 0 (microcêntimos)
    // mas têm match válido por ISRC. Mapeamento track_id => max(report_date)
    // — usado no batch update final para actualizar last_seen_in_csv.
    //
    // Sem isto, faixas como "Mimo em Pessoa" (que reportou 7 streams no TikTok
    // com royalty €0.0000003) ficariam para sempre como "Silenciosas" porque
    // nunca entravam em rmbz_distro_daily_stats.
    $silent_seen_tracks = []; // [ track_id => 'YYYY-MM-01' ]
    $silent_isrc_cache  = []; // [ isrc_normalized => track_id ] — cache de lookup

    // [DS 1.5.90] Selecção parcial — admin pode escolher quais ISRCs importar
    // e o que fazer com os não-selecionados.
    //
    //   $selected_isrcs   = array de ISRC keys (uppercase, sem hífens) que o
    //                       admin marcou para importar. Vazio = importar todos.
    //   $unsel_destination = 'unclaimed' (default) | 'exclude'
    $selected_raw = isset($_POST['selected_isrcs']) && is_array($_POST['selected_isrcs'])
        ? $_POST['selected_isrcs'] : [];
    $selected_isrcs = [];
    foreach ( $selected_raw as $sel ) {
        $clean = strtoupper( preg_replace('/[^A-Z0-9]/i', '', (string)$sel) );
        if ( $clean ) $selected_isrcs[ $clean ] = true;
    }
    $partial_selection_active = ! empty( $selected_isrcs );

    // [FIX 1.6.15] DEFESA SERVER-SIDE — re-importação não pode duplicar.
    //
    // Mesmo que o admin force (via dev tools / scripts) a marcação de um
    // checkbox de uma faixa com cadeado 🔒, o servidor tem que recusar a
    // entrada. Carregamos a lista autoritativa de ISRCs já importados a
    // partir do log original e removemos qualquer um deles do $selected_isrcs.
    //
    // A normalização (sem hífens, maiúsculas) é a mesma usada na preview
    // — ver admin-import-royalties.php linha ~310.
    $blocked_already_imported = []; // [isrc_norm => true] — para log
    if ( $is_additional && ! empty( $additional_ctx['original_log_id'] )
         && function_exists('rmbz_get_already_imported_keys') ) {
        $orig_log_id = (int) $additional_ctx['original_log_id'];
        $keys = rmbz_get_already_imported_keys( $orig_log_id );
        $already_set = [];
        foreach ( $keys as $key => $_v ) {
            $parts = explode('|', $key);
            if ( ! empty($parts[0]) ) {
                $isrc_norm = strtoupper( preg_replace('/[^A-Za-z0-9]/', '', $parts[0]) );
                if ( $isrc_norm !== '' ) {
                    $already_set[ $isrc_norm ] = true;
                }
            }
        }
        if ( ! empty( $already_set ) ) {
            foreach ( array_keys( $selected_isrcs ) as $sel_key ) {
                if ( isset( $already_set[ $sel_key ] ) ) {
                    unset( $selected_isrcs[ $sel_key ] );
                    $blocked_already_imported[ $sel_key ] = true;
                }
            }
            // Se o admin desligou tudo e a selecção ficou vazia depois deste
            // filtro, mantemos $partial_selection_active TRUE para que nada
            // seja importado (em vez do default "importar todos").
            $partial_selection_active = $partial_selection_active || ! empty( $blocked_already_imported );

            if ( ! empty( $blocked_already_imported ) ) {
                error_log( sprintf(
                    '[1MZ re-import] Defesa server-side bloqueou %d ISRC(s) já importado(s) no log #%d: %s',
                    count( $blocked_already_imported ),
                    $orig_log_id,
                    implode( ',', array_slice( array_keys( $blocked_already_imported ), 0, 10 ) ) .
                    ( count( $blocked_already_imported ) > 10 ? '…' : '' )
                ) );
            }
        }
    }

    $unsel_destination = sanitize_key( $_POST['unselected_destination'] ?? 'unclaimed' );
    if ( ! in_array( $unsel_destination, ['unclaimed', 'exclude'], true ) ) {
        $unsel_destination = 'unclaimed';
    }

    $wpdb->query('START TRANSACTION');

    // [1.5.203] Optimização de memória para imports grandes:
    //   - wp_suspend_cache_addition(true): impede que o WP cacheie objects
    //     internos durante a transação. Cada query SELECT/UPDATE deixa de
    //     ser guardada em memória depois de processada.
    //   - $wpdb->queries: WP guarda log de todas as queries se SAVEQUERIES
    //     estiver activo (em debug). Podes ver isto a explodir em CSVs grandes.
    if ( function_exists('wp_suspend_cache_addition') ) {
        wp_suspend_cache_addition( true );
    }
    $wpdb->queries = [];

    // [1.5.203] Cap nos errors acumulados para evitar OOM em CSVs grandes com
    // muitas linhas inválidas. Acima do limite, contamos mas não guardamos detalhes.
    $errors_cap = 200;

    try {
        $line = 1;
        while ( ($row = fgetcsv($handle)) !== FALSE ) {
            $line++;
            if ( count($row) < count($header) ) {
                $stats['skipped']++;
                if ( count($stats['errors'] ?? []) < $errors_cap ) {
                    $stats['errors'][] = ['line' => $line, 'msg' => 'Linha incompleta — número de colunas não bate com header'];
                }
                continue;
            }

            $isrc = strtoupper( trim( $row[ $idx['isrc'] ] ) );
            if ( empty($isrc) ) {
                $stats['skipped']++;
                if ( count($stats['errors'] ?? []) < $errors_cap ) {
                    $stats['errors'][] = ['line' => $line, 'msg' => 'ISRC em falta'];
                }
                continue;
            }

            // [1.5.204] BUG CRÍTICO CORRIGIDO — extrair report_date REAL do CSV
            //
            // ANTES (bug 1.5.203 e anteriores):
            //   $report_date = $period . '-01'  ← TODAS as linhas do CSV usavam
            //   o mesmo $period do form admin. Mas um único CSV pode ter dados
            //   de 6+ meses diferentes (ex: Amadea CSV com Jul-Dez 2025).
            //   Resultado: dados de Setembro 2025 ficavam gravados como 2026-04-01.
            //   "Faixas Silenciosas" mostrava informação errada porque
            //   last_seen_in_csv era sempre o $period (data de submissão).
            //
            // AGORA: lemos a coluna $idx['date'] do CSV (Sale_Start_Date no
            // Amadea) e parseamos para YYYY-MM-DD. Se a coluna não existe ou
            // o valor não é parseable, fallback ao $period do form.
            //
            // Isto afecta apenas o REPORT_DATE individual de cada linha.
            // O RECORD principal (rmbz_distro_import_logs.reporting_period)
            // continua a usar $period — esse representa "quando foi submetido",
            // não "qual é a data dos dados".
            $report_date = $period . '-01';  // fallback default
            if ( isset($idx['date']) && $idx['date'] !== false && $idx['date'] !== null ) {
                $raw_date = trim( (string) ( $row[ $idx['date'] ] ?? '' ) );
                if ( $raw_date !== '' ) {
                    // Tenta parse — strtotime apanha "2025-12-01 00:00:00", "2025-12",
                    // "Dec 2025", etc. Se falhar, mantém fallback.
                    $ts = strtotime( $raw_date );
                    if ( $ts !== false ) {
                        // Validar range razoável (2015-presente+1ano)
                        $ts_min = strtotime('2015-01-01');
                        $ts_max = strtotime('+1 year');
                        if ( $ts >= $ts_min && $ts <= $ts_max ) {
                            // Truncar para o primeiro dia do mês (consistência
                            // com o resto do sistema que agrega por mês)
                            $report_date = date( 'Y-m-01', $ts );
                        }
                    }
                }
            }

            // [DS 1.5.90] Aplicar selecção parcial — se admin filtrou, respeitar.
            $isrc_key = strtoupper( preg_replace('/[^A-Z0-9]/i', '', $isrc) );
            if ( $partial_selection_active && ! isset($selected_isrcs[$isrc_key]) ) {
                if ( $unsel_destination === 'exclude' ) {
                    $stats['excluded']++;
                    continue;
                }
                // 'unclaimed' → deixa cair pelo caminho normal — o match não vai
                // bater (artificialmente) e a linha vai para unclaimed. Mas para
                // forçarmos que vá mesmo, marcamos com flag $force_unclaimed.
                $force_unclaimed = true;
            } else {
                $force_unclaimed = false;
            }

            // Limpeza do valor
            $raw_val     = $row[ $idx['amount'] ] ?? '0';
            $amount_orig = floatval( str_replace( [',', '$', '€', '£', ' '], ['.','','','',''], (string)$raw_val ) );
            $amount_usd_gross = round( $amount_orig * $exchange_rate, 4 );
            if ( $amount_usd_gross <= 0 ) {
                // [FIX 1.6.18] Antes de skipar, registar que a faixa foi VISTA
                // no CSV (mesmo que o valor seja microcêntimo). Isto evita
                // que apareça em "Faixas Silenciosas" injustamente — o sistema
                // sabe agora que a loja reportou, simplesmente o valor foi 0.
                //
                // Match rápido só por ISRC directo (sem chamar rmbz_match_track
                // completo — ficaria caro para microcêntimos). Se ISRC bate, é
                // suficiente para actualizar last_seen.
                if ( ! empty( $isrc ) ) {
                    $isrc_lookup_key = strtoupper( preg_replace('/[^A-Z0-9]/i', '', $isrc) );
                    if ( $isrc_lookup_key !== '' ) {
                        // Cache por chave normalizada — evita query duplicada
                        if ( ! isset( $silent_isrc_cache[ $isrc_lookup_key ] ) ) {
                            $silent_isrc_cache[ $isrc_lookup_key ] = (int) $wpdb->get_var( $wpdb->prepare(
                                "SELECT id FROM {$wpdb->prefix}rmbz_distro_tracks
                                  WHERE UPPER(REPLACE(REPLACE(isrc_code, '-', ''), ' ', '')) = %s
                                  LIMIT 1",
                                $isrc_lookup_key
                            ) );
                        }
                        $silent_tid = $silent_isrc_cache[ $isrc_lookup_key ];
                        if ( $silent_tid > 0 ) {
                            // Manter o report_date mais recente para esta track
                            $prev = $silent_seen_tracks[ $silent_tid ] ?? '1900-01-01';
                            if ( $report_date > $prev ) {
                                $silent_seen_tracks[ $silent_tid ] = $report_date;
                            }
                            $stats['silent_seen']++;
                        }
                    }
                }
                $stats['skipped']++;
                continue;
            }

            // Aplicar taxa 1MusicMoz — artistas recebem X% do bruto (default 70%)
            $artist_factor = rmbz_get_artist_share_factor();
            $amount_usd = round( $amount_usd_gross * $artist_factor, 4 );

            // Agregar totais para a tabela de comissões no fim da importação
            if ( ! isset($stats['gross_total_usd']) ) $stats['gross_total_usd'] = 0.0;
            if ( ! isset($stats['artist_total_usd']) ) $stats['artist_total_usd'] = 0.0;
            $stats['gross_total_usd']  += $amount_usd_gross;
            $stats['artist_total_usd'] += $amount_usd;

            $store   = ($idx['store']   !== false) ? sanitize_text_field( $row[$idx['store']]   ?? 'Unknown' ) : 'Unknown';
            $country = ($idx['country'] !== false) ? sanitize_text_field( $row[$idx['country']] ?? 'UNK' )     : 'UNK';
            $streams = ($idx['streams'] !== false) ? max(1, (int)( $row[$idx['streams']] ?? 1 )) : 1;

            // Capturar metadata da faixa (para Black Box manter info rica)
            $track_title_csv = ($idx['track_name'] !== false) ? sanitize_text_field( $row[$idx['track_name']] ?? '' ) : '';
            $artist_csv      = ($idx['artist']     !== false) ? sanitize_text_field( $row[$idx['artist']]     ?? '' ) : '';
            $upc_csv         = ($idx['upc']        !== false) ? sanitize_text_field( $row[$idx['upc']]        ?? '' ) : '';
            $album_csv       = ($idx['album']      !== false) ? sanitize_text_field( $row[$idx['album']]      ?? '' ) : '';
            $label_csv       = ($idx['label']      !== false) ? sanitize_text_field( $row[$idx['label']]      ?? '' ) : '';
            $currency_csv    = ($idx['currency']   !== false) ? strtoupper(sanitize_text_field( $row[$idx['currency']] ?? '' )) : '';

            // Cache de lookup
            //
            // Antes: lookup APENAS por ISRC (WHERE t.isrc_code = %s).
            // Problema: CSVs com ISRC errado/em falta marcavam tudo como
            // unclaimed, mesmo com UPC válido na BD. TuneCore por exemplo
            // muitas vezes manda "Optional ISRC" igual ao "TC Song ID"
            // (TCAHB...) que NÃO é ISRC verdadeiro.
            //
            // Agora: detector multi-camada (ISRC → UPC+título → fuzzy).
            // Matches por UPC ou fuzzy vão para fila de revisão admin
            // (rmbz_distro_match_review) em vez de aplicar imediatamente.
            $cache_key = $isrc . '|' . $upc_csv . '|' . $track_title_csv . '|' . $artist_csv;
            if ( ! isset($isrc_cache[$cache_key]) ) {
                // [DS 1.5.90] Verificar override manual ANTES do match normal.
                // Se admin já mapeou este CSV ISRC para uma faixa, usa essa
                // mapeamento e trata como match auto (confiança 100%).
                //
                // [DS 1.5.96] Também verificamos UPC override agora — algumas
                // distribuidoras dão UPCs diferentes ao mesmo álbum, então
                // o admin pode mapear "este UPC do CSV → faixa interna X".
                $override = function_exists('rmbz_lookup_isrc_override')
                    ? rmbz_lookup_isrc_override( $isrc )
                    : null;
                if ( ! $override && ! empty($upc_csv) && function_exists('rmbz_lookup_upc_alias') ) {
                    $override = rmbz_lookup_upc_alias( $upc_csv );
                }
                if ( $override ) {
                    $isrc_cache[$cache_key] = $override;
                } elseif ( function_exists('rmbz_match_track') ) {
                    $match_result = rmbz_match_track([
                        'isrc'        => $isrc,
                        'upc'         => $upc_csv,
                        'track_title' => $track_title_csv,
                        'artist'      => $artist_csv,
                        'album'       => $album_csv,
                    ]);

                    // Só matches por ISRC (confiança 100%) são aplicados
                    // automaticamente. Os outros vão para revisão e a
                    // linha continua a entrar em unclaimed até admin
                    // confirmar.
                    if ( $match_result['method'] === 'isrc' && $match_result['track'] ) {
                        $isrc_cache[$cache_key] = $match_result['track'];
                    } else {
                        $isrc_cache[$cache_key] = null;
                        // Se há sugestão (UPC ou fuzzy), guardar para
                        // revisão admin
                        if ( $match_result['needs_review'] ) {
                            rmbz_match_save_for_review(
                                [
                                    'isrc'        => $isrc,
                                    'upc'         => $upc_csv,
                                    'track_title' => $track_title_csv,
                                    'artist'      => $artist_csv,
                                    'album'       => $album_csv,
                                    'label'       => $label_csv,
                                    'store'       => $store,
                                    'country'     => $country,
                                    'streams'     => $streams,
                                    'amount_usd'  => $amount_usd,
                                ],
                                $match_result,
                                $hash,
                                $report_date
                            );
                        }
                    }
                } else {
                    // Fallback: lookup simples (caso o ficheiro do matcher
                    // não esteja carregado)
                    $isrc_cache[$cache_key] = $wpdb->get_row( $wpdb->prepare(
                        "SELECT r.artist_telemovel, t.id AS track_id, t.release_id
                         FROM {$wpdb->prefix}rmbz_distro_tracks t
                         JOIN {$wpdb->prefix}rmbz_distro_releases r ON t.release_id = r.id
                         WHERE t.isrc_code = %s
                         LIMIT 1",
                        $isrc
                    ) );
                }
            }
            $track = $isrc_cache[$cache_key];

            // --- BLACK BOX ---
            // [DS 1.5.90] Se admin desselecionou esta faixa e escolheu "unclaimed",
            // força ir para unclaimed mesmo se houvesse match.
            if ( $force_unclaimed ) {
                $track = null;
            }
            if ( ! $track ) {
                // UPSERT: agregar por ISRC + report_date para evitar milhares de linhas duplicadas
                // Primeiro tenta encontrar registo existente com mesmo ISRC+período
                $t_unclaimed = $wpdb->prefix . 'rmbz_distro_unclaimed';
                $existing = $wpdb->get_row( $wpdb->prepare(
                    "SELECT id, amount, streams_total, line_count, stores, countries
                     FROM $t_unclaimed
                     WHERE isrc = %s AND report_date = %s AND status = 'pending'
                     LIMIT 1",
                    $isrc, $report_date
                ) );

                if ( $existing ) {
                    // Acumular lojas e países no JSON existente
                    $stores_arr = [];
                    $countries_arr = [];
                    if ( $existing->stores ) {
                        $d = json_decode($existing->stores, true);
                        if ( is_array($d) ) $stores_arr = $d;
                    }
                    if ( $existing->countries ) {
                        $d = json_decode($existing->countries, true);
                        if ( is_array($d) ) $countries_arr = $d;
                    }
                    $stores_arr[$store]       = ($stores_arr[$store] ?? 0)       + $amount_usd;
                    $countries_arr[$country]  = ($countries_arr[$country] ?? 0)  + $amount_usd;

                    $wpdb->update( $t_unclaimed,
                        [
                            'amount'        => (float) $existing->amount + $amount_usd,
                            'streams_total' => (int)   $existing->streams_total + $streams,
                            'line_count'    => (int)   $existing->line_count + 1,
                            'stores'        => wp_json_encode($stores_arr),
                            'countries'     => wp_json_encode($countries_arr),
                        ],
                        [ 'id' => (int) $existing->id ],
                        [ '%f', '%d', '%d', '%s', '%s' ],
                        [ '%d' ]
                    );
                } else {
                    // Primeiro registo desta combinação
                    $wpdb->insert(
                        $t_unclaimed,
                        [
                            'isrc'          => $isrc,
                            'isrc_code'     => $isrc,
                            'upc_code'      => $upc_csv,
                            'track_title'   => $track_title_csv,
                            'artist_name'   => $artist_csv,
                            'album'         => $album_csv,
                            'label'         => $label_csv,
                            'amount'        => $amount_usd,
                            'streams_total' => $streams,
                            'line_count'    => 1,
                            'platform'      => $store,
                            'country'       => $country,
                            'stores'        => wp_json_encode([ $store => $amount_usd ]),
                            'countries'     => wp_json_encode([ $country => $amount_usd ]),
                            'report_date'   => $report_date,
                            'status'        => 'pending',
                            'import_hash'   => $hash,
                            'pending_approval' => 1,
                        ]
                    );
                }
                $stats['unclaimed']++;
                continue;
            }

            // --- PAGAR com SPLITS ---
            // $amount_usd já é o valor líquido (70% do bruto, com taxa da 1MZ descontada)
            $external_holds = [];
            $shares = rmbz_compute_splits( (int)$track->track_id, $track->artist_telemovel, $amount_usd, $external_holds );
            if ( count($shares) > 1 ) $stats['splits']++;

            // Registar holdings para colaboradores externos (ainda não registados na 1MZ).
            // Estes NÃO entram no ledger — ficam em rmbz_splits_holdings com
            // pending_approval=1, alinhado com o ciclo de aprovação do ledger.
            // Quando o admin aprovar, passa a pending_approval=0 e o claim-on-login
            // credita-os na wallet assim que o colaborador se registar.
            if ( ! empty($external_holds) && function_exists('rmbz_splits_record_holding') ) {
                if ( ! isset($stats['holds']) ) $stats['holds'] = 0;
                $holds_desc = sprintf('%s · %s · %s · %s · %s',
                    $source_name, $store, $country, $isrc, $period
                );
                foreach ( $external_holds as $hold ) {
                    rmbz_splits_record_holding(
                        (int) $track->track_id,
                        (int) $track->release_id,
                        (int) $hold['split_id'],
                        (string) $hold['telemovel'],
                        (float) $hold['amount'],
                        $holds_desc,
                        $hash,      // import_hash — liga ao fluxo de aprovação
                        true        // pending_approval
                    );
                    $stats['holds']++;
                }
            }

            $artist_pct = rmbz_get_artist_share_pct();

            foreach ( $shares as $payee => $share ) {

                // 1. Ledger com descrição detalhada mostrando bruto → taxa → líquido
                $desc = sprintf(
                    '%s: %s (%s) — %s | Bruto: $%.4f · Taxa 1MZ (%d%%): -$%.4f · Líquido: $%.4f',
                    $source_name, $store, $country, $isrc,
                    $amount_usd_gross,
                    100 - (int) $artist_pct,
                    $amount_usd_gross - $amount_usd,
                    $share
                );
                $wpdb->insert(
                    $wpdb->prefix . 'rmbz_distro_ledger',
                    [
                        'artist_telemovel'  => $payee,
                        'amount'            => $share,
                        'currency'          => 'USD',
                        'original_amount'   => $amount_orig,
                        'original_currency' => $currency_orig,
                        'exchange_rate'     => $exchange_rate,
                        'source_name'       => $source_name,
                        'reporting_period'  => $period,
                        'type'              => ($payee === $track->artist_telemovel) ? 'royalty_payout' : 'royalty_split',
                        'description'       => $desc,
                        'import_hash'       => $hash,
                        'isrc'              => $isrc,                      // [DS 1.5.115]
                        'pending_approval'  => 1,
                    ]
                );

                // 2. Stats agregadas — NÃO actualizar aqui (só após aprovação)
                // Comentado intencionalmente — o admin tem de aprovar a importação
                // primeiro para o dinheiro aparecer nas wallets dos artistas.
                // Ver: royalties-approval-handler.php → rmbz_handle_approve_import()

                // 3. Daily stats — inserir com pending_approval=1.
                //
                // [1.5.164] CHAVE ÚNICA AGORA INCLUI import_hash:
                // (artist_telemovel, release_id, track_id, report_date, country_code, store, import_hash)
                //
                // Resultado: cada import tem as suas próprias linhas, separadas das
                // linhas de imports anteriores. O ON DUPLICATE KEY UPDATE só dispara
                // se for EXACTAMENTE o mesmo import (re-tentativa) — caso raro.
                //
                // ANTES (bug 1.5.163-): sem import_hash na unique key, o UPSERT
                // somava valores pendentes em linhas já aprovadas, fazendo dinheiro
                // pendente aparecer ao artista sem o admin aprovar.
                $wpdb->query( $wpdb->prepare(
                    "INSERT INTO {$wpdb->prefix}rmbz_distro_daily_stats
                        (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, (int)$track->release_id, (int)$track->track_id,
                    $report_date, $streams, $share, $country, $store, $hash,
                    $streams, $share
                ) );

                // [1.5.190 → 1.5.203] last_seen_in_csv actualizado em batch
                // depois do loop principal terminar (1 query agregada em vez
                // de N queries inline). Comentário acima preservado para histórico.
            }

            $stats['total_usd'] += $amount_usd;
            $stats['processed']++;
        }

        // [1.5.203] BATCH UPDATE de last_seen_in_csv
        //
        // ANTES (1.5.190): UPDATE inline a cada faixa dentro do loop principal.
        // Em CSVs grandes (33MB / ~200k linhas / ~50k tracks únicas) isto
        // gerava 50k+ UPDATEs num só request, esgotando memória PHP.
        //
        // AGORA: 1 query agregada que olha para o que acabou de ser inserido
        // em daily_stats e actualiza o MAX(report_date) por track. Funciona
        // O(1) queries em vez de O(N).
        //
        // GREATEST() garante que não regredimos para datas mais antigas
        // (proteção quando admin importa um CSV antigo retroactivamente).
        $t_tracks_x = $wpdb->prefix . 'rmbz_distro_tracks';
        $t_daily_x  = $wpdb->prefix . 'rmbz_distro_daily_stats';
        // Verificar se a coluna existe antes de tentar UPDATE (idempotente)
        $cols_t = $wpdb->get_col( "SHOW COLUMNS FROM $t_tracks_x" );
        if ( in_array('last_seen_in_csv', $cols_t, true) ) {
            $wpdb->query( $wpdb->prepare(
                "UPDATE $t_tracks_x t
                 INNER JOIN (
                     SELECT track_id, MAX(report_date) AS max_date
                     FROM $t_daily_x
                     WHERE import_hash = %s
                       AND track_id IS NOT NULL
                       AND track_id > 0
                     GROUP BY track_id
                 ) d ON d.track_id = t.id
                 SET t.last_seen_in_csv = GREATEST(
                     IFNULL(t.last_seen_in_csv, '1900-01-01'),
                     d.max_date
                 )
                 WHERE d.max_date > IFNULL(t.last_seen_in_csv, '1900-01-01')",
                $hash
            ) );

            // [FIX 1.6.18] Update adicional para faixas que foram VISTAS no CSV
            // mas não entraram em daily_stats (skipadas por microcêntimo zero).
            // Sem este passo, ficariam para sempre em "Faixas Silenciosas"
            // mesmo tendo aparecido no relatório.
            if ( ! empty( $silent_seen_tracks ) ) {
                // Construir lista (track_id, date) como VALUES para um UPDATE
                // com CASE/JOIN seria complexo — fazemos UPDATEs por batches
                // (CASE WHEN). Para volumes típicos (<1000 silent tracks) é OK.
                $silent_count = 0;
                foreach ( $silent_seen_tracks as $tid => $max_date ) {
                    $tid = (int) $tid;
                    if ( $tid <= 0 ) continue;
                    $wpdb->query( $wpdb->prepare(
                        "UPDATE $t_tracks_x
                            SET last_seen_in_csv = GREATEST(
                                IFNULL(last_seen_in_csv, '1900-01-01'),
                                %s
                            )
                          WHERE id = %d
                            AND ( last_seen_in_csv IS NULL OR last_seen_in_csv < %s )",
                        $max_date, $tid, $max_date
                    ) );
                    $silent_count++;
                }
                if ( $silent_count > 0 ) {
                    error_log( sprintf(
                        '[1MZ import] Silent-seen: %d faixa(s) marcadas como vistas no CSV apesar de royalties = 0',
                        $silent_count
                    ) );
                }
            }
        }

        // Registo na tabela de comissões — auditoria financeira da 1MZ
        $gross_total  = (float) ($stats['gross_total_usd']  ?? 0);
        $artist_total = (float) ($stats['artist_total_usd'] ?? 0);
        $onemz_margin = round( $gross_total - $artist_total, 4 );
        $gross_orig   = $exchange_rate > 0 ? round( $gross_total / $exchange_rate, 4 ) : $gross_total;

        // UPSERT em vez de INSERT cego — se já existir um registo com este
        // import_hash (caso de re-import forçado depois de revert/anular),
        // actualiza os valores em vez de rebentar com "Duplicate entry".
        // A tabela tem UNIQUE KEY uniq_hash (import_hash) que protege
        // contra duplicados acidentais; o UPSERT respeita-o.
        $t_comm = $wpdb->prefix . 'rmbz_distro_platform_commissions';
        $wpdb->query( $wpdb->prepare(
            "INSERT INTO $t_comm
                (import_hash, distributor_name, profile_id, reporting_period,
                 original_currency, exchange_rate, gross_amount_original,
                 gross_amount_usd, artist_share_pct, artist_total_usd,
                 onemz_margin_usd, lines_processed, lines_unclaimed, created_at)
             VALUES (%s, %s, %s, %s, %s, %f, %f, %f, %f, %f, %f, %d, %d, %s)
             ON DUPLICATE KEY UPDATE
                distributor_name      = VALUES(distributor_name),
                profile_id            = VALUES(profile_id),
                reporting_period      = VALUES(reporting_period),
                original_currency     = VALUES(original_currency),
                exchange_rate         = VALUES(exchange_rate),
                gross_amount_original = VALUES(gross_amount_original),
                gross_amount_usd      = VALUES(gross_amount_usd),
                artist_share_pct      = VALUES(artist_share_pct),
                artist_total_usd      = VALUES(artist_total_usd),
                onemz_margin_usd      = VALUES(onemz_margin_usd),
                lines_processed       = VALUES(lines_processed),
                lines_unclaimed       = VALUES(lines_unclaimed),
                created_at            = VALUES(created_at),
                reverted_at           = NULL",
            $hash, $source_name, $profile_id, $period,
            $currency_orig, $exchange_rate, $gross_orig,
            $gross_total, rmbz_get_artist_share_pct(), $artist_total,
            $onemz_margin, (int) $stats['processed'], (int) $stats['unclaimed'],
            current_time('mysql')
        ) );

        $wpdb->query('COMMIT');

        // [1.5.203] Restaurar cache normal depois da transação
        if ( function_exists('wp_suspend_cache_addition') ) {
            wp_suspend_cache_addition( false );
        }

    } catch ( Exception $e ) {
        $wpdb->query('ROLLBACK');
        // [1.5.203] Restaurar cache mesmo em caso de erro
        if ( function_exists('wp_suspend_cache_addition') ) {
            wp_suspend_cache_addition( false );
        }
        fclose( $handle );
        wp_die( 'Erro crítico durante a importação: ' . esc_html( $e->getMessage() ) . ' — nenhuma alteração foi guardada.' );
    }

    fclose( $handle );
    // [DS 1.5.108] Não apagamos ainda — primeiro precisamos do import_log_id
    // para arquivar com nome correcto. Apagamos só se arquivar falhar.

    // Marcar como importado SÓ depois do commit com sucesso
    rmbz_mark_csv_imported( $hash, $temp_filename );

    // Registar na tabela rmbz_distro_import_logs para aparecer em
    // WP-Admin → Royalties → Importações, pendente de aprovação.
    // (Bug anterior: a chamada existia no caminho Data Report mas estava
    // em falta aqui — o caminho principal dos track reports nunca criava
    // o log, pelo que os imports não apareciam na fila de aprovação.)
    $import_log_id = rmbz_log_csv_import( $hash, basename($temp_filename), $source_name, $period, $stats );

    // [DS 1.5.108] Arquivar o CSV agora que temos o ID
    if ( $import_log_id > 0 && function_exists('rmbz_archive_import_csv') ) {
        $archived = rmbz_archive_import_csv( $file_path, $import_log_id, $hash );
        if ( ! empty($archived) ) {
            $wpdb->update(
                $wpdb->prefix . 'rmbz_distro_import_logs',
                [ 'archived_path' => $archived ],
                [ 'id' => $import_log_id ],
                [ '%s' ], [ '%d' ]
            );
        } else {
            @unlink( $file_path );
        }
    } else {
        @unlink( $file_path );
    }

    // [DS 1.5.104] Popular import_log_id em ledger/daily_stats/unclaimed/
    // commissions criados acima. Resolve ambiguidade quando o mesmo file_hash
    // existe em vários imports (após limpeza de hash duplicado).
    if ( $import_log_id > 0 ) {
        $t_ledger_x    = $wpdb->prefix . 'rmbz_distro_ledger';
        $t_daily_x     = $wpdb->prefix . 'rmbz_distro_daily_stats';
        $t_unclaimed_x = $wpdb->prefix . 'rmbz_distro_unclaimed';
        $t_comm_x      = $wpdb->prefix . 'rmbz_distro_platform_commissions';
        if ( $wpdb->get_var("SHOW COLUMNS FROM $t_ledger_x LIKE 'import_log_id'") ) {
            $wpdb->query( $wpdb->prepare(
                "UPDATE $t_ledger_x SET import_log_id = %d
                 WHERE import_hash = %s AND reporting_period = %s AND import_log_id IS NULL",
                $import_log_id, $hash, $period
            ) );
        }
        if ( $wpdb->get_var("SHOW COLUMNS FROM $t_daily_x LIKE 'import_log_id'") ) {
            $wpdb->query( $wpdb->prepare(
                "UPDATE $t_daily_x SET import_log_id = %d
                 WHERE import_hash = %s AND DATE_FORMAT(report_date, '%%Y-%%m') = %s AND import_log_id IS NULL",
                $import_log_id, $hash, $period
            ) );
        }
        if ( $wpdb->get_var("SHOW COLUMNS FROM $t_unclaimed_x LIKE 'import_log_id'") ) {
            $wpdb->query( $wpdb->prepare(
                "UPDATE $t_unclaimed_x SET import_log_id = %d
                 WHERE import_hash = %s AND import_log_id IS NULL",
                $import_log_id, $hash
            ) );
        }
        if ( $wpdb->get_var("SHOW COLUMNS FROM $t_comm_x LIKE 'import_log_id'") ) {
            $wpdb->query( $wpdb->prepare(
                "UPDATE $t_comm_x SET import_log_id = %d
                 WHERE import_hash = %s AND reporting_period = %s AND import_log_id IS NULL",
                $import_log_id, $hash, $period
            ) );
        }
    }

    // Redirect com resumo
    $redirect_args = [
        'import_status' => 'pending_approval',
        'processed'     => $stats['processed'],
        'total'         => number_format( $stats['total_usd'], 2 ),
        'errors'        => $stats['unclaimed'],
        'splits'        => $stats['splits'],
        'skipped'       => $stats['skipped'],
        'new_hash'      => $hash,
    ];
    // [FIX 1.6.15] Sinalizar quantos ISRCs foram bloqueados por já terem
    // sido importados — admin é notificado na próxima página.
    if ( ! empty( $blocked_already_imported ) ) {
        $redirect_args['blocked_duplicates'] = count( $blocked_already_imported );
    }
    $redirect_url = add_query_arg( $redirect_args,
        admin_url('admin.php?page=rmbz-admin-distribution&tab=royalties&rsub=imports') );

    wp_safe_redirect( $redirect_url );
    exit;
}

add_action( 'admin_post_rmbz_processar_importacao_final', 'rmbz_handle_csv_import_final' );


/**
 * Handler para limpar hash de import duplicado — permite re-upload do
 * mesmo ficheiro. Útil quando uma importação anterior falhou a meio mas
 * o hash ficou guardado, bloqueando novas tentativas.
 *
 * Requer nonce `rmbz_clear_hash_{hash}`.
 */
function rmbz_handle_clear_import_hash() {
    if ( ! current_user_can('manage_options') ) wp_die('Sem permissão.');

    $hash = isset($_GET['hash']) ? sanitize_text_field( $_GET['hash'] ) : '';
    if ( ! $hash || strlen($hash) < 10 ) wp_die('Hash inválido.');

    check_admin_referer( 'rmbz_clear_hash_' . $hash );

    // Remover da lista de imported hashes
    $imported = get_option( 'rmbz_imported_csv_hashes', [] );
    $imported = array_values( array_filter( (array) $imported, function($h) use ($hash) {
        return $h !== $hash;
    } ) );
    update_option( 'rmbz_imported_csv_hashes', $imported, false );

    // Remover do log descritivo também
    $log = get_option( 'rmbz_import_log', [] );
    $log = array_values( array_filter( (array) $log, function($entry) use ($hash) {
        return ! isset($entry['hash']) || $entry['hash'] !== $hash;
    } ) );
    update_option( 'rmbz_import_log', $log, false );

    // CORREÇÃO: também apagar registos órfãos do mesmo hash em
    // platform_commissions — caso contrário a UNIQUE KEY (uniq_hash)
    // bloqueia a re-importação com "Duplicate entry".
    // Esta é precisamente a situação que ocorre quando se anula uma
    // importação (que só faz UPDATE reverted_at sem apagar a linha de
    // commissions) e depois se tenta re-importar o mesmo CSV.
    global $wpdb;
    $t_comm = $wpdb->prefix . 'rmbz_distro_platform_commissions';
    if ( $wpdb->get_var("SHOW TABLES LIKE '$t_comm'") === $t_comm ) {
        $deleted = $wpdb->query( $wpdb->prepare(
            "DELETE FROM $t_comm WHERE import_hash = %s",
            $hash
        ) );
        if ( $deleted > 0 ) {
            error_log('[1MZ clear_hash] Apagadas ' . $deleted . ' linha(s) órfãs em platform_commissions para hash ' . substr($hash, 0, 12));
        }
    }

    // Redirect para a página de upload com mensagem de sucesso
    $redirect = add_query_arg(
        'hash_cleared',
        substr($hash, 0, 12),
        admin_url('admin.php?page=rmbz-admin-distribution&tab=royalties&rsub=imports')
    );
    wp_safe_redirect( $redirect );
    exit;
}
add_action( 'admin_post_rmbz_clear_import_hash', 'rmbz_handle_clear_import_hash' );
