<?php
// includes/distribution/distribution-security.php
//
// VERSÃO CORRIGIDA — 2026-04-22
// Alterações face ao original:
//   [BUG #1] Já NÃO remove os registos nopriv nem tenta filtrar por login WP —
//            o plugin usa sessão custom ($_SESSION['rmbz_user_telemovel']),
//            portanto para o WordPress o utilizador é sempre anónimo e
//            precisamos do wp_ajax_nopriv_*. A protecção real é feita dentro
//            de cada handler via rmbz_is_user_logged_in().
//   [BUG #3] rmbz_validate_release_date agora calcula o min_days com base na
//            timezone do WordPress (current_time) em vez de misturar com date().
//   [BUG #4] Handler aceita nonce tanto em 'nonce' como em '_wpnonce'.
//   [BUG #8] Schema patch vai garantir InnoDB; o START TRANSACTION aqui
//            fica seguro. Adicionado try/catch explícito.

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

/**
 * ----------------------------------------------------------------------------
 * VALIDADORES — reutilizáveis nos handlers
 * ----------------------------------------------------------------------------
 */

/**
 * Valida ISRC segundo a norma IFPI: CC-XXX-YY-NNNNN (12 chars sem hífens).
 * Ex: USRC17607839
 */
function rmbz_validate_isrc( $isrc ) {
    if ( empty($isrc) ) return true;
    $isrc = strtoupper( preg_replace('/[^A-Z0-9]/i', '', $isrc) );
    return (bool) preg_match('/^[A-Z]{2}[A-Z0-9]{3}\d{2}\d{5}$/', $isrc);
}

/**
 * Valida UPC/EAN: 12 ou 13 dígitos com checksum EAN.
 */
function rmbz_validate_upc( $upc ) {
    if ( empty($upc) ) return true;
    $upc = preg_replace('/\D/', '', $upc);
    if ( ! in_array( strlen($upc), [12, 13], true ) ) return false;

    $sum = 0;
    $digits = str_split($upc);
    $check  = array_pop($digits);
    $pattern = ( strlen($upc) === 13 ) ? [1, 3] : [3, 1];

    foreach ( $digits as $i => $d ) {
        $sum += (int)$d * $pattern[ $i % 2 ];
    }
    $calc_check = (10 - ($sum % 10)) % 10;
    return (int)$check === $calc_check;
}

/**
 * [BUG #3 CORRIGIDO]
 * Valida data >= hoje + min_days, usando a timezone do WordPress.
 * A comparação é feita em dias-calendário (não em segundos), portanto é
 * consistente mesmo próximo da meia-noite.
 */
function rmbz_validate_release_date( $date_str, $min_days = 7, $is_legacy = false ) {
    $d = DateTime::createFromFormat('Y-m-d', $date_str);
    if ( ! $d ) return false;
    $d->setTime(0, 0, 0);

    // [1.5.152] Catálogo legado: a data pode ser PASSADA. Apenas validamos que
    // a data é válida (parseable). O utilizador confirmou no modal que é
    // catálogo antigo, então aceita-se qualquer data válida não no futuro distante.
    if ( $is_legacy ) {
        // [1.5.156] Limite inferior: 1999 (era da música digital).
        // Música anterior a isto raramente está em formato distribuível.
        $min_year = new DateTime('1999-01-01');
        if ( $d < $min_year ) return false;
        // Limite superior: hoje + 90 dias (aceita mesmo se planeou re-distribuir
        // catálogo no futuro próximo, mas impede datas absurdas).
        $today = new DateTime( current_time('Y-m-d') );
        $upper = clone $today; $upper->modify('+90 days');
        if ( $d > $upper ) return false;
        return true;
    }

    $today = new DateTime( current_time('Y-m-d') );
    $today->setTime(0, 0, 0);

    $diff = (int) $today->diff($d)->format('%r%a');
    return $diff >= $min_days;
}

/**
 * [1.5.146/152] Funções `rmbz_validate_original_release_date` removidas.
 * Catálogo legado agora usa só `release_date` + flag `is_legacy=1`.
 * A coluna `is_legacy` na BD é preenchida quando utilizador confirma
 * "Sim, é catálogo antigo" no modal do date picker. Coluna
 * `original_release_date` na BD existe mas não é preenchida pelo wizard.
 */

/**
 * Verifica MIME real de um ficheiro carregado.
 */
function rmbz_verify_upload_mime( $tmp_path, array $allowed ) {
    if ( ! is_uploaded_file( $tmp_path ) ) return 'Upload inválido.';
    if ( ! function_exists('finfo_open') )  return true;

    $f = finfo_open( FILEINFO_MIME_TYPE );
    $mime = finfo_file( $f, $tmp_path );
    finfo_close( $f );

    if ( ! in_array( $mime, $allowed, true ) ) {
        return sprintf( 'Tipo de ficheiro não permitido (%s).', esc_html($mime) );
    }
    return true;
}

/**
 * Rate limit simples baseado em transient.
 */
function rmbz_rate_limit( $key, $max_per_minute = 20 ) {
    $tkey = 'rmbz_rl_' . md5( $key );
    $hits = (int) get_transient( $tkey );
    if ( $hits >= $max_per_minute ) return false;
    set_transient( $tkey, $hits + 1, 60 );
    return true;
}

/**
 * [BUG #4 CORRIGIDO]
 * Helper: valida nonce aceitando quer em 'nonce' quer em '_wpnonce'.
 */
function rmbz_distro_check_nonce() {
    return check_ajax_referer('rmbz_distro_nonce', 'nonce', false)
        || check_ajax_referer('rmbz_distro_nonce', '_wpnonce', false);
}

/**
 * Regra central de acesso às operações de distribuição do artista.
 *
 * @param string $telemovel  Telefone da sessão do artista.
 * @param bool   $allow_somas Permite resolver um pedido SOMAS pendente/devolvido.
 * @return string Mensagem de bloqueio ou string vazia quando o acesso é permitido.
 */
function rmbz_distro_artist_access_error( $telemovel, $allow_somas = false ) {
    if ( current_user_can( 'manage_options' ) ) return '';
    if ( empty( $telemovel ) ) return 'Sessão expirada. Faça login novamente.';

    if ( ! function_exists( 'rmbz_user_has_active_subscription' )
        || ! rmbz_user_has_active_subscription( $telemovel ) ) {
        return 'Precisas de uma subscrição VIP ativa para usar a Distribuição Digital.';
    }

    global $wpdb;
    $extra_raw = $wpdb->get_var( $wpdb->prepare(
        "SELECT extra_data FROM {$wpdb->prefix}rmbz_wallets WHERE user_telemovel = %s",
        $telemovel
    ) );
    $extra = $extra_raw ? json_decode( $extra_raw, true ) : [];
    if ( is_array( $extra ) && ! empty( $extra['block_distribution'] ) ) {
        return 'O teu acesso à Distribuição Digital foi bloqueado pela administração.';
    }

    $permissions = function_exists( 'rmbz_vip_plan_permissions' )
        ? rmbz_vip_plan_permissions( $telemovel, is_array( $extra ) ? $extra : [] )
        : ( isset( $extra['vip_permissions'] ) && is_array( $extra['vip_permissions'] )
            ? $extra['vip_permissions']
            : [ 'distribute' => 1 ] );
    if ( empty( $permissions['distribute'] ) ) {
        return 'O teu plano VIP atual não inclui Distribuição Digital. Escolhe um plano com este serviço para continuar.';
    }

    if ( function_exists( 'rmbz_is_globally_blocked' ) && rmbz_is_globally_blocked( 'distribution' ) ) {
        return 'A Distribuição Digital está temporariamente suspensa. Tenta novamente mais tarde.';
    }

    if ( ! $allow_somas && function_exists( 'rmbz_licensing_should_block_route' )
        && rmbz_licensing_should_block_route( $telemovel, 'dashboard' ) ) {
        return 'O teu pedido SOMAS precisa de ser concluído antes de continuares a distribuição.';
    }

    return '';
}

/** Protege todas as mutações AJAX de distribuição, incluindo chamadas nopriv. */
function rmbz_distro_ajax_access_guard() {
    if ( ! function_exists( 'rmbz_is_user_logged_in' ) || ! rmbz_is_user_logged_in() ) {
        wp_send_json_error( 'Sessão expirada. Faça login novamente.', 401 );
    }
    $telemovel = function_exists( 'rmbz_get_current_user_telemovel' )
        ? rmbz_get_current_user_telemovel()
        : '';
    $error = rmbz_distro_artist_access_error( $telemovel );
    if ( $error !== '' ) wp_send_json_error( $error, 403 );
}

$rmbz_distro_mutation_actions = [
    'onemz_save_release_draft',
    'onemz_finalize_release',
    'rmbz_save_release_platforms',
    'onemz_save_distro_track',
    'rmbz_track_editor_save',
    'onemz_delete_distro_track',
    'onemz_reorder_distro_track',
    'rmbz_upload_release_cover',
    'rmbz_artist_profile_save',
    'rmbz_request_takedown',
    'rmbz_splits_frontend_save',
    'rmbz_splits_request_change',
    'rmbz_release_type_override',
    'rmbz_submit_license',
    'rmbz_validate_licensing_coupon',
    'rmbz_contentid_submit',
];
foreach ( $rmbz_distro_mutation_actions as $rmbz_distro_action ) {
    add_action( 'wp_ajax_' . $rmbz_distro_action, 'rmbz_distro_ajax_access_guard', 1 );
    add_action( 'wp_ajax_nopriv_' . $rmbz_distro_action, 'rmbz_distro_ajax_access_guard', 1 );
}
unset( $rmbz_distro_mutation_actions, $rmbz_distro_action );

/** O upload genérico só é protegido quando está a receber uma faixa da distribuição. */
function rmbz_distro_audio_upload_access_guard() {
    if ( isset( $_POST['is_distro_track'] ) && (string) $_POST['is_distro_track'] === '1' ) {
        rmbz_distro_ajax_access_guard();
    }
}
add_action( 'wp_ajax_onemz_upload_music_chunk', 'rmbz_distro_audio_upload_access_guard', 1 );
add_action( 'wp_ajax_nopriv_onemz_upload_music_chunk', 'rmbz_distro_audio_upload_access_guard', 1 );

/**
 * ----------------------------------------------------------------------------
 * HANDLER SEGURO: salvar rascunho
 * ----------------------------------------------------------------------------
 */
function rmbz_safe_save_release_draft() {
    if ( ! rmbz_distro_check_nonce() ) {
        wp_send_json_error('Falha de segurança (nonce).', 403);
    }
    if ( ! function_exists('rmbz_is_user_logged_in') || ! rmbz_is_user_logged_in() ) {
        wp_send_json_error('Sessão expirada. Faça login novamente.', 401);
    }

    $telemovel = rmbz_get_current_user_telemovel();

    $access_error = rmbz_distro_artist_access_error( $telemovel );
    if ( $access_error !== '' ) wp_send_json_error( $access_error, 403 );

    if ( ! rmbz_rate_limit( 'save_draft_' . $telemovel, 20 ) ) {
        wp_send_json_error('Demasiados pedidos. Tente novamente num minuto.', 429);
    }

    global $wpdb;
    $table_releases = $wpdb->prefix . 'rmbz_distro_releases';

    $release_id = isset($_POST['release_id']) ? absint($_POST['release_id']) : 0;
    $title      = isset($_POST['release_title']) ? sanitize_text_field( wp_unslash($_POST['release_title']) ) : '';
    $lang       = isset($_POST['language']) ? sanitize_text_field($_POST['language']) : '';
    $genre      = isset($_POST['primary_genre']) ? sanitize_text_field($_POST['primary_genre']) : '';
    $date       = isset($_POST['release_date']) ? sanitize_text_field($_POST['release_date']) : '';
    $label      = isset($_POST['label_name']) ? sanitize_text_field($_POST['label_name']) : '';
    $loc        = isset($_POST['recording_location']) ? sanitize_text_field($_POST['recording_location']) : '';
    $upc        = isset($_POST['upc_code']) ? sanitize_text_field($_POST['upc_code']) : '';

    // [1.5.152] Catálogo legado — flag vem do date picker.
    // Vem '1' se utilizador confirmou no modal "Sim, é catálogo antigo",
    // caso contrário vem vazio. Defensivo: aceita "1" ou 1, qualquer outro = 0.
    $is_legacy = ( isset($_POST['is_legacy']) && (string) $_POST['is_legacy'] === '1' ) ? 1 : 0;

    // [WIZ-RT5] Tipo declarado pelo artista no Passo 1.
    // Usar default 'single' se vier vazio ou inválido (compat. com forms antigos).
    $release_type = isset($_POST['release_type']) ? sanitize_key($_POST['release_type']) : 'single';
    if ( ! in_array($release_type, ['single', 'ep', 'album'], true) ) {
        $release_type = 'single';
    }

    if ( mb_strlen($title) < 2 || mb_strlen($title) > 150 ) {
        wp_send_json_error('Título inválido (2–150 caracteres).');
    }
    $allowed_langs  = ['Portuguese', 'English', 'Spanish'];
    $allowed_genres = ['Hip Hop','Afrobeat','Pandza','Kizomba','R&B','Gospel','Pop','Eletrónica','Outro'];
    if ( $lang && ! in_array($lang, $allowed_langs, true) )    wp_send_json_error('Idioma inválido.');
    if ( $genre && ! in_array($genre, $allowed_genres, true) ) wp_send_json_error('Género inválido.');

    // [1.5.152] Validação sensível à flag is_legacy:
    //   - Releases NORMAIS  → release_date >= hoje + 7 dias
    //   - Releases LEGADOS  → release_date pode ser passado/presente/futuro próximo
    if ( $date && ! rmbz_validate_release_date($date, 7, (bool) $is_legacy) ) {
        if ( $is_legacy ) {
            wp_send_json_error('Data de lançamento inválida (catálogo antigo: 1999 até hoje+90 dias).');
        } else {
            wp_send_json_error('Data de lançamento inválida ou muito próxima (mín. 7 dias).');
        }
    }

    if ( $upc && ! rmbz_validate_upc($upc) ) {
        wp_send_json_error('UPC/EAN inválido. Use 12 ou 13 dígitos.');
    }

    // Upload de capa
    $cover_image_url = '';
    if ( ! empty( $_FILES['cover_art_file']['name'] ) ) {
        $tmp  = $_FILES['cover_art_file']['tmp_name'];
        $size = (int) $_FILES['cover_art_file']['size'];

        if ( $size > 10 * 1024 * 1024 ) {
            wp_send_json_error('Capa demasiado grande (máx. 10 MB).');
        }
        $mime_check = rmbz_verify_upload_mime( $tmp, ['image/jpeg', 'image/png'] );
        if ( $mime_check !== true ) wp_send_json_error( $mime_check );

        $size_info = getimagesize( $tmp );
        if ( $size_info && ( $size_info[0] < 1400 || $size_info[1] < 1400 ) ) {
            wp_send_json_error('Capa tem de ter pelo menos 1400×1400px.');
        }

        $uploaded = rmbz_handle_proof_upload('cover_art_file');
        if ( ! $uploaded ) wp_send_json_error('Falha ao carregar a capa.');
        $cover_image_url = $uploaded;
    }

    $data = [
        'artist_telemovel'   => $telemovel,
        'title'              => $title,
        'language'           => $lang,
        'primary_genre'      => $genre,
        'release_date'       => $date,
        'label_name'         => $label,
        'recording_location' => $loc,
        'upc_code'           => $upc,
        'release_type'       => $release_type, // [WIZ-RT5] declarado pelo artista
        'is_legacy'          => $is_legacy,    // [1.5.152] flag catálogo antigo
        'status'             => 'incomplete',
    ];
    $fmt = ['%s','%s','%s','%s','%s','%s','%s','%s','%s','%d','%s'];
    if ( $cover_image_url ) { $data['cover_image'] = $cover_image_url; $fmt[] = '%s'; }

    if ( $release_id > 0 ) {
        $owner = $wpdb->get_row( $wpdb->prepare(
            "SELECT id, status, label_name, recording_location, upc_code, extra_data
             FROM $table_releases WHERE id = %d AND artist_telemovel = %s",
            $release_id, $telemovel
        ) );
        if ( ! $owner ) wp_send_json_error('Lançamento não encontrado.', 404);

        $cur_status = (string) $owner->status;
        if ( ! in_array( $cur_status, ['incomplete', 'draft', 'returned'], true ) ) {
            wp_send_json_error('Este lançamento já foi submetido e não pode ser editado.');
        }

        // Uma correção devolvida continua devolvida até o artista clicar em enviar.
        $data['status'] = $cur_status === 'returned' ? 'returned' : 'incomplete';
        if ( ! isset( $_POST['label_name'] ) ) $data['label_name'] = (string) $owner->label_name;
        if ( ! isset( $_POST['recording_location'] ) ) $data['recording_location'] = (string) $owner->recording_location;
        if ( ! isset( $_POST['upc_code'] ) ) $data['upc_code'] = (string) $owner->upc_code;

        // [WIZ-RT5] Preservar extra_data existente e marcar release_type_manual=1.
        // Sem este flag o classificador automático sobreporia a declaração do
        // artista quando o status mudar (ver rmbz_release_type_on_status_change).
        $cur_extra_raw = (string) $owner->extra_data;
        $cur_extra = [];
        if ( ! empty($cur_extra_raw) ) {
            $dec = json_decode($cur_extra_raw, true);
            if ( is_array($dec) ) $cur_extra = $dec;
        }
        $cur_extra['release_type_manual'] = 1;
        $data['extra_data'] = wp_json_encode($cur_extra);
        $fmt[] = '%s';

        $wpdb->update( $table_releases, $data, ['id' => $release_id], $fmt, ['%d'] );
        wp_send_json_success([
            'message' => 'Rascunho atualizado',
            'release_id' => $release_id,
            'release_type' => $release_type,
        ]);
    } else {
        if ( ! isset($data['cover_image']) ) { $data['cover_image'] = ''; $fmt[] = '%s'; }
        // [WIZ-RT5] release_type_manual=1 desde o início para releases novos.
        $data['extra_data'] = wp_json_encode(['release_type_manual' => 1]);
        $fmt[] = '%s';

        $wpdb->insert( $table_releases, $data, $fmt );
        wp_send_json_success([
            'message' => 'Rascunho criado',
            'release_id' => $wpdb->insert_id,
            'release_type' => $release_type,
        ]);
    }
}

/**
 * [BUG #1 CORRIGIDO]
 * Regista os handlers seguros em AMBOS wp_ajax_ e wp_ajax_nopriv_, porque o
 * plugin tem sistema de sessão próprio (não usa wp_signon). Para o WordPress
 * os artistas são sempre "guests" e sem nopriv o endpoint responderia "0".
 *
 * A protecção real é feita dentro do handler via rmbz_is_user_logged_in().
 *
 * Usamos prioridade 5 para correr ANTES dos handlers originais em
 * form-handler.php (que estão a 10). Isto dá efectivamente substituição
 * sem precisar de remove_action (mais robusto face a ordem de carregamento).
 */
add_action( 'wp_ajax_onemz_save_release_draft',        'rmbz_safe_save_release_draft', 5 );
add_action( 'wp_ajax_nopriv_onemz_save_release_draft', 'rmbz_safe_save_release_draft', 5 );

// Remover explicitamente o handler antigo (ambos os contextos) depois do form-handler.php ter corrido
add_action( 'init', function () {
    remove_action( 'wp_ajax_onemz_save_release_draft',        'onemz_save_release_draft_ajax' );
    remove_action( 'wp_ajax_nopriv_onemz_save_release_draft', 'onemz_save_release_draft_ajax' );
}, 30 );


/**
 * ----------------------------------------------------------------------------
 * HANDLER SEGURO: finalizar release
 * ----------------------------------------------------------------------------
 */
function rmbz_safe_finalize_release() {
    if ( ! rmbz_distro_check_nonce() ) {
        wp_send_json_error('Falha de segurança.', 403);
    }
    if ( ! rmbz_is_user_logged_in() ) wp_send_json_error('Sessão expirada.', 401);

    $telemovel  = rmbz_get_current_user_telemovel();
    $access_error = rmbz_distro_artist_access_error( $telemovel );
    if ( $access_error !== '' ) wp_send_json_error( $access_error, 403 );
    $release_id = isset($_POST['release_id']) ? absint($_POST['release_id']) : 0;
    if ( $release_id <= 0 ) wp_send_json_error('ID inválido.');

    if ( ! rmbz_rate_limit( 'finalize_' . $telemovel, 5 ) ) {
        wp_send_json_error('Demasiados pedidos.', 429);
    }

    global $wpdb;
    $tbl_r = $wpdb->prefix . 'rmbz_distro_releases';
    $tbl_t = $wpdb->prefix . 'rmbz_distro_tracks';

    $rel = $wpdb->get_row( $wpdb->prepare(
        "SELECT * FROM $tbl_r WHERE id = %d AND artist_telemovel = %s",
        $release_id, $telemovel
    ) );
    if ( ! $rel ) wp_send_json_error('Lançamento não encontrado.', 404);

    // Pré-condições
    $errors = [];
    if ( empty($rel->title) )          $errors[] = 'Título em falta.';
    if ( empty($rel->primary_genre) )  $errors[] = 'Género em falta.';
    if ( empty($rel->cover_image) )    $errors[] = 'Capa em falta.';
    if ( empty($rel->release_date) || ! rmbz_validate_release_date($rel->release_date, 7, ! empty($rel->is_legacy)) ) {
        $errors[] = ! empty($rel->is_legacy)
            ? 'Data de lançamento inválida (catálogo legado).'
            : 'Data de lançamento inválida (mín. 7 dias).';
    }
    $track_count = (int) $wpdb->get_var( $wpdb->prepare(
        "SELECT COUNT(*) FROM $tbl_t WHERE release_id = %d", $release_id
    ) );
    if ( $track_count < 1 ) $errors[] = 'Adicione pelo menos uma faixa.';

    // [WIZ-RT7] Validar nº de faixas vs tipo declarado.
    // Single = 1-3 / EP = 4-6 / Álbum = 7+
    $rt = ! empty($rel->release_type) ? $rel->release_type : 'single';
    if ( in_array($rt, ['single','ep','album'], true) && $track_count >= 1 ) {
        if ( $rt === 'single' && $track_count > 3 ) {
            $errors[] = sprintf('Single permite no máximo 3 faixas (tem %d). Mude o tipo no Passo 1 para EP/Álbum.', $track_count);
        }
        if ( $rt === 'ep' ) {
            if ( $track_count < 4 ) $errors[] = sprintf('EP exige pelo menos 4 faixas (tem %d). Adicione mais ou mude o tipo para Single.', $track_count);
            if ( $track_count > 6 ) $errors[] = sprintf('EP permite no máximo 6 faixas (tem %d). Mude o tipo para Álbum.', $track_count);
        }
        if ( $rt === 'album' && $track_count < 7 ) {
            $errors[] = sprintf('Álbum exige pelo menos 7 faixas (tem %d). Adicione mais ou mude o tipo para EP.', $track_count);
        }
    }

    if ( ! in_array( $rel->status, ['incomplete', 'draft', 'returned'], true ) ) {
        $errors[] = 'Este lançamento já foi submetido.';
    }

    // [META-CHECK-4] Validar metadata por faixa (songwriters, lyrics se has_lyrics, etc).
    // Bloqueia submissão se alguma faixa estiver incompleta.
    if ( $track_count >= 1 && function_exists('rmbz_track_validate_metadata') ) {
        $track_rows = $wpdb->get_results( $wpdb->prepare(
            "SELECT * FROM $tbl_t WHERE release_id = %d ORDER BY track_order ASC",
            $release_id
        ) );
        foreach ( $track_rows as $idx => $tr ) {
            $v = rmbz_track_validate_metadata( $tr );
            if ( ! $v['is_complete'] ) {
                $title = $tr->track_title ?: 'Sem título';
                $errors[] = sprintf(
                    'Faixa %d (%s) incompleta — falta: %s.',
                    $idx + 1,
                    $title,
                    implode(', ', $v['missing'])
                );
            }
        }
    }

    if ( $errors ) wp_send_json_error( implode(' ', $errors) );

    // Releases devolvidos passam pelo ponto único de re-submissão para manter
    // os hooks de auditoria e limpar a notificação de correções pendentes.
    if ( $rel->status === 'returned' && function_exists( 'rmbz_distro_resubmit_release' ) ) {
        $resubmitted = rmbz_distro_resubmit_release( $release_id, $telemovel );
        if ( is_wp_error( $resubmitted ) ) {
            wp_send_json_error( $resubmitted->get_error_message() );
        }
        $wpdb->insert(
            $wpdb->prefix . 'rmbz_distro_status_history',
            [
                'release_id'  => $release_id,
                'from_status' => 'returned',
                'to_status'   => 'pendente',
                'reason_code' => 'resubmitted_by_artist',
                'changed_by'  => $telemovel,
            ]
        );
        wp_send_json_success( [ 'message' => 'Correções enviadas novamente para revisão!' ] );
    }

    // [BUG #8] A transacção só funciona se as tabelas forem InnoDB.
    // O schema patch (patch 04) garante InnoDB. Aqui usamos try/catch explícito
    // para capturar falhas do wpdb (que não lança excepções por default).
    $wpdb->query('START TRANSACTION');

    $updated = $wpdb->update(
        $tbl_r,
        [ 'status' => 'pendente', 'submitted_at' => current_time('mysql') ],
        [ 'id' => $release_id ],
        [ '%s', '%s' ],
        [ '%d' ]
    );

    if ( $updated === false ) {
        $wpdb->query('ROLLBACK');
        wp_send_json_error('Falha ao submeter: ' . $wpdb->last_error);
    }

    $inserted = $wpdb->insert(
        $wpdb->prefix . 'rmbz_distro_status_history',
        [
            'release_id'  => $release_id,
            'from_status' => $rel->status,
            'to_status'   => 'pendente',
            'reason_code' => 'submitted_by_artist',
            'changed_by'  => $telemovel,
        ]
    );

    if ( $inserted === false ) {
        $wpdb->query('ROLLBACK');
        wp_send_json_error('Falha ao registar histórico: ' . $wpdb->last_error);
    }

    $wpdb->query('COMMIT');

    wp_send_json_success(['message' => 'Lançamento enviado para revisão!']);
}

// [BUG #1] — também aqui: registar nos dois contextos
add_action( 'wp_ajax_onemz_finalize_release',        'rmbz_safe_finalize_release', 5 );
add_action( 'wp_ajax_nopriv_onemz_finalize_release', 'rmbz_safe_finalize_release', 5 );

add_action( 'init', function () {
    remove_action( 'wp_ajax_onemz_finalize_release',        'onemz_finalize_release_ajax' );
    remove_action( 'wp_ajax_nopriv_onemz_finalize_release', 'onemz_finalize_release_ajax' );
}, 30 );
