<?php
/**
 * 1MZ For Artists — Releases Controller (read-only)
 *
 * Endpoints:
 *   GET /1mz/v1/releases                    Lista paginada
 *   GET /1mz/v1/releases/stats              Resumo por estado
 *   GET /1mz/v1/releases/{id}               Detalhe
 *   GET /1mz/v1/releases/{id}/tracks        Faixas
 *   GET /1mz/v1/releases/{id}/platforms     DSPs e territórios
 *
 * REGRAS:
 *  - Só releases do utilizador autenticado (filtra por artist_telemovel)
 *  - Não há criação/edição nesta versão (3a) — só leitura
 *
 * @package 1MZ_For_Artists
 * @since 1.6.2
 */

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

add_action( 'rest_api_init', 'rmbz_api_register_releases_routes' );

function rmbz_api_register_releases_routes() {

    register_rest_route( RMBZ_API_NAMESPACE, '/releases', [
        'methods'             => 'GET',
        'callback'            => 'rmbz_api_handle_list_releases',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'page'     => [ 'type' => 'integer', 'default' => 1, 'minimum' => 1 ],
            'per_page' => [ 'type' => 'integer', 'default' => 20, 'minimum' => 1, 'maximum' => 100 ],
            'status'   => [ 'type' => 'string' ],
        ],
    ] );

    register_rest_route( RMBZ_API_NAMESPACE, '/releases/stats', [
        'methods'             => 'GET',
        'callback'            => 'rmbz_api_handle_releases_stats',
        'permission_callback' => 'rmbz_api_require_auth',
    ] );

    register_rest_route( RMBZ_API_NAMESPACE, '/releases/(?P<id>\d+)', [
        'methods'             => 'GET',
        'callback'            => 'rmbz_api_handle_get_release',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'id' => [ 'type' => 'integer', 'required' => true ],
        ],
    ] );

    register_rest_route( RMBZ_API_NAMESPACE, '/releases/(?P<id>\d+)/tracks', [
        'methods'             => 'GET',
        'callback'            => 'rmbz_api_handle_get_release_tracks',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'id' => [ 'type' => 'integer', 'required' => true ],
        ],
    ] );

    register_rest_route( RMBZ_API_NAMESPACE, '/releases/(?P<id>\d+)/platforms', [
        'methods'             => 'GET',
        'callback'            => 'rmbz_api_handle_get_release_platforms',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'id' => [ 'type' => 'integer', 'required' => true ],
        ],
    ] );

    /* ============= [1.6.3] WRITE/ACTION ENDPOINTS ============= */

    // Listas auxiliares para popular formulários (idiomas, géneros, motivos)
    register_rest_route( RMBZ_API_NAMESPACE, '/releases/options', [
        'methods'             => 'GET',
        'callback'            => 'rmbz_api_handle_release_options',
        'permission_callback' => 'rmbz_api_require_auth',
    ] );

    // Editar metadados (só se incomplete ou returned)
    register_rest_route( RMBZ_API_NAMESPACE, '/releases/(?P<id>\d+)', [
        'methods'             => 'PUT',
        'callback'            => 'rmbz_api_handle_update_release',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'id' => [ 'type' => 'integer', 'required' => true ],
        ],
    ] );

    // Apagar (só se não estiver live/distribuido)
    register_rest_route( RMBZ_API_NAMESPACE, '/releases/(?P<id>\d+)', [
        'methods'             => 'DELETE',
        'callback'            => 'rmbz_api_handle_delete_release',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'id' => [ 'type' => 'integer', 'required' => true ],
        ],
    ] );

    // Submeter (incomplete → pendente)
    register_rest_route( RMBZ_API_NAMESPACE, '/releases/(?P<id>\d+)/submit', [
        'methods'             => 'POST',
        'callback'            => 'rmbz_api_handle_submit_release',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'id' => [ 'type' => 'integer', 'required' => true ],
        ],
    ] );

    // Re-submeter (returned → pendente)
    register_rest_route( RMBZ_API_NAMESPACE, '/releases/(?P<id>\d+)/resubmit', [
        'methods'             => 'POST',
        'callback'            => 'rmbz_api_handle_resubmit_release',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'id' => [ 'type' => 'integer', 'required' => true ],
        ],
    ] );

    // Pedir takedown (live → takedown_requested)
    register_rest_route( RMBZ_API_NAMESPACE, '/releases/(?P<id>\d+)/takedown', [
        'methods'             => 'POST',
        'callback'            => 'rmbz_api_handle_request_takedown',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'id' => [ 'type' => 'integer', 'required' => true ],
        ],
    ] );

    /* ============= [1.6.4] CRIAÇÃO E TRACKS ============= */

    // Criar release rascunho
    register_rest_route( RMBZ_API_NAMESPACE, '/releases', [
        'methods'             => 'POST',
        'callback'            => 'rmbz_api_handle_create_release',
        'permission_callback' => 'rmbz_api_require_auth',
    ] );

    // Atualizar capa (depois do upload)
    register_rest_route( RMBZ_API_NAMESPACE, '/releases/(?P<id>\d+)/cover', [
        'methods'             => 'PUT',
        'callback'            => 'rmbz_api_handle_update_cover',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'id' => [ 'type' => 'integer', 'required' => true ],
        ],
    ] );

    // CRUD de tracks
    register_rest_route( RMBZ_API_NAMESPACE, '/releases/(?P<id>\d+)/tracks', [
        'methods'             => 'POST',
        'callback'            => 'rmbz_api_handle_create_track',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'id' => [ 'type' => 'integer', 'required' => true ],
        ],
    ] );

    register_rest_route( RMBZ_API_NAMESPACE, '/releases/(?P<id>\d+)/tracks/(?P<track_id>\d+)', [
        'methods'             => 'PUT',
        'callback'            => 'rmbz_api_handle_update_track',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'id'       => [ 'type' => 'integer', 'required' => true ],
            'track_id' => [ 'type' => 'integer', 'required' => true ],
        ],
    ] );

    register_rest_route( RMBZ_API_NAMESPACE, '/releases/(?P<id>\d+)/tracks/(?P<track_id>\d+)', [
        'methods'             => 'DELETE',
        'callback'            => 'rmbz_api_handle_delete_track',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'id'       => [ 'type' => 'integer', 'required' => true ],
            'track_id' => [ 'type' => 'integer', 'required' => true ],
        ],
    ] );
}

/* ----------------------------------------------------------------------- */
/* LISTA                                                                   */
/* ----------------------------------------------------------------------- */

function rmbz_api_handle_list_releases( WP_REST_Request $request ) {
    $telemovel = (string) $request->get_param( 'rmbz_telemovel' );
    $page      = max( 1, (int) $request->get_param( 'page' ) );
    $per_page  = min( 100, max( 1, (int) $request->get_param( 'per_page' ) ) );
    $status    = (string) $request->get_param( 'status' );
    $type      = (string) $request->get_param( 'type' );  // [1.6.56] filtro por release_type
    $offset    = ( $page - 1 ) * $per_page;

    global $wpdb;
    $where  = $wpdb->prepare( 'WHERE artist_telemovel = %s', $telemovel );
    if ( $status !== '' ) {
        $where .= $wpdb->prepare( ' AND status = %s', $status );
    }
    // [1.6.56] Filtro por tipo de lançamento — só aceitar valores
    // conhecidos para evitar SQL malformado/abuso.
    if ( $type !== '' && in_array( $type, [ 'single', 'ep', 'album' ], true ) ) {
        $where .= $wpdb->prepare( ' AND release_type = %s', $type );
    }

    $total = (int) $wpdb->get_var(
        "SELECT COUNT(*) FROM {$wpdb->prefix}rmbz_distro_releases {$where}"
    );

    $rows = $wpdb->get_results( $wpdb->prepare(
        "SELECT id, title, release_type, cover_image, primary_genre, language,
                release_date, status, rejection_reason, takedown_requested,
                live_at, submitted_at, returned_at, return_count, created_at
         FROM {$wpdb->prefix}rmbz_distro_releases
         {$where}
         ORDER BY created_at DESC
         LIMIT %d OFFSET %d",
        $per_page, $offset
    ) );

    // Para cada release, contar quantas tracks tem
    $items = array_map( function ( $r ) use ( $wpdb ) {
        $track_count = (int) $wpdb->get_var( $wpdb->prepare(
            "SELECT COUNT(*) FROM {$wpdb->prefix}rmbz_distro_tracks WHERE release_id = %d",
            $r->id
        ) );
        return rmbz_api_format_release_summary( $r, $track_count );
    }, $rows ?: [] );

    return rmbz_api_ok( [
        'items' => $items,
        'pagination' => [
            'page'        => $page,
            'per_page'    => $per_page,
            'total'       => $total,
            'total_pages' => $per_page > 0 ? (int) ceil( $total / $per_page ) : 1,
        ],
    ] );
}

/* ----------------------------------------------------------------------- */
/* STATS                                                                   */
/* ----------------------------------------------------------------------- */

function rmbz_api_handle_releases_stats( WP_REST_Request $request ) {
    $telemovel = (string) $request->get_param( 'rmbz_telemovel' );

    global $wpdb;
    $rows = $wpdb->get_results( $wpdb->prepare(
        "SELECT status, COUNT(*) AS n
         FROM {$wpdb->prefix}rmbz_distro_releases
         WHERE artist_telemovel = %s
         GROUP BY status",
        $telemovel
    ) );

    $by_status = [];
    $total = 0;
    foreach ( $rows ?: [] as $r ) {
        $by_status[ $r->status ] = (int) $r->n;
        $total += (int) $r->n;
    }

    return rmbz_api_ok( [
        'total'     => $total,
        'by_status' => $by_status,
    ] );
}

/* ----------------------------------------------------------------------- */
/* DETALHE                                                                 */
/* ----------------------------------------------------------------------- */

function rmbz_api_handle_get_release( WP_REST_Request $request ) {
    $telemovel = (string) $request->get_param( 'rmbz_telemovel' );
    $id        = (int) $request->get_param( 'id' );

    global $wpdb;
    $r = $wpdb->get_row( $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}rmbz_distro_releases
         WHERE id = %d AND artist_telemovel = %s",
        $id, $telemovel
    ) );

    if ( ! $r ) {
        return rmbz_api_error( 'Release não encontrado.', 404, 'not_found' );
    }

    $track_count = (int) $wpdb->get_var( $wpdb->prepare(
        "SELECT COUNT(*) FROM {$wpdb->prefix}rmbz_distro_tracks WHERE release_id = %d",
        $id
    ) );

    return rmbz_api_ok( rmbz_api_format_release_full( $r, $track_count ) );
}

/* ----------------------------------------------------------------------- */
/* TRACKS                                                                  */
/* ----------------------------------------------------------------------- */

function rmbz_api_handle_get_release_tracks( WP_REST_Request $request ) {
    $telemovel = (string) $request->get_param( 'rmbz_telemovel' );
    $id        = (int) $request->get_param( 'id' );

    global $wpdb;
    // Confirmar que o release pertence ao utilizador
    $owns = (int) $wpdb->get_var( $wpdb->prepare(
        "SELECT 1 FROM {$wpdb->prefix}rmbz_distro_releases
         WHERE id = %d AND artist_telemovel = %s",
        $id, $telemovel
    ) );
    if ( ! $owns ) {
        return rmbz_api_error( 'Release não encontrado.', 404, 'not_found' );
    }

    // [1.6.77] Incluir `extra_data` no SELECT para que `rmbz_api_format_track`
    // tenha acesso aos campos avançados (songwriters, lyrics, cover, mood,
    // preview_start_seconds, etc.). Antes este handler tinha código duplicado
    // que ignorava `extra_data` — por isso os campos avançados desapareciam
    // ao reabrir o release editado mesmo estando guardados na BD.
    $rows = $wpdb->get_results( $wpdb->prepare(
        "SELECT id, track_title, feat_artists, audio_url, isrc_code,
                is_explicit, track_order, extra_data
         FROM {$wpdb->prefix}rmbz_distro_tracks
         WHERE release_id = %d
         ORDER BY track_order ASC, id ASC",
        $id
    ) );

    // [1.6.77] Usar o helper canónico em vez de mapping inline.
    // Garante consistência: GET /tracks devolve a mesma estrutura que
    // PUT /tracks/{id} e POST /tracks (todos passam por format_track).
    $items = array_map( 'rmbz_api_format_track', $rows ?: [] );

    return rmbz_api_ok( [ 'tracks' => $items ] );
}

/* ----------------------------------------------------------------------- */
/* PLATAFORMAS                                                             */
/* ----------------------------------------------------------------------- */

function rmbz_api_handle_get_release_platforms( WP_REST_Request $request ) {
    $telemovel = (string) $request->get_param( 'rmbz_telemovel' );
    $id        = (int) $request->get_param( 'id' );

    global $wpdb;
    $owns = (int) $wpdb->get_var( $wpdb->prepare(
        "SELECT 1 FROM {$wpdb->prefix}rmbz_distro_releases
         WHERE id = %d AND artist_telemovel = %s",
        $id, $telemovel
    ) );
    if ( ! $owns ) {
        return rmbz_api_error( 'Release não encontrado.', 404, 'not_found' );
    }

    $row = $wpdb->get_row( $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}rmbz_distro_release_platforms
         WHERE release_id = %d",
        $id
    ) );

    if ( ! $row ) {
        // Release ainda não tem plataformas configuradas.
        // [FIX 1.6.12] exclusives DEVE ser objecto, nunca array vazio,
        // senão json_encode produz '[]' e a app falha a desserializar.
        // [DEBUG 1.6.12.1] _api_version no payload para confirmar deploy.
        return rmbz_api_ok( [
            'configured'    => false,
            'territory_mode' => null,
            'territories'   => [],
            'dsps'          => [],
            'exclusives'    => (object) [
                'youtube_content_id'    => (object) [ 'status' => 'not_requested', 'note' => null ],
                'facebook_monetization' => (object) [ 'status' => 'not_requested', 'note' => null ],
                'tiktok_monetization'   => (object) [ 'status' => 'not_requested', 'note' => null ],
            ],
            '_api_version' => '1.6.12.1',
        ] );
    }

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

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

    // Resolver nomes amigáveis dos DSPs se o catálogo estiver disponível
    $dsps_with_names = [];
    if ( function_exists( 'rmbz_distro_dsp_catalog' ) ) {
        $catalog = rmbz_distro_dsp_catalog();
        foreach ( $dsp_list as $slug ) {
            $dsps_with_names[] = [
                'slug' => $slug,
                'name' => $catalog[ $slug ]['name'] ?? $slug,
                'tier' => $catalog[ $slug ]['tier'] ?? null,
            ];
        }
    } else {
        // Fallback se o helper não existir
        foreach ( $dsp_list as $slug ) {
            $dsps_with_names[] = [ 'slug' => $slug, 'name' => $slug, 'tier' => null ];
        }
    }

    return rmbz_api_ok( [
        'configured'     => true,
        'territory_mode' => $row->territory_mode,
        'territories'    => $territory_list,
        'dsps'           => $dsps_with_names,
        'exclusives' => [
            'youtube_content_id' => [
                'status' => $row->yt_content_id_status,
                'note'   => $row->yt_content_id_decision_note,
            ],
            'facebook_monetization' => [
                'status' => $row->fb_monet_status,
                'note'   => $row->fb_monet_decision_note,
            ],
            'tiktok_monetization' => [
                'status' => $row->tt_monet_status,
                'note'   => $row->tt_monet_decision_note,
            ],
        ],
        'user_confirmed' => (bool) $row->user_confirmed,
        'updated_at'     => mysql_to_rfc3339( $row->updated_at ),
    ] );
}

/* ----------------------------------------------------------------------- */
/* HELPERS DE FORMATAÇÃO                                                   */
/* ----------------------------------------------------------------------- */

/**
 * Resumo (lista) — não inclui campos pesados como extra_data, return_problems
 */
function rmbz_api_format_release_summary( $r, $track_count ) {
    return [
        'id'                  => (int) $r->id,
        'title'               => $r->title,
        'release_type'        => $r->release_type,
        'cover_image'         => $r->cover_image ? rmbz_api_safe_url( $r->cover_image ) : null,
        'primary_genre'       => $r->primary_genre,
        'language'            => $r->language,
        'release_date'        => $r->release_date,
        'status'              => $r->status,
        'rejection_reason'    => $r->rejection_reason,
        'takedown_requested'  => (bool) $r->takedown_requested,
        'track_count'         => $track_count,
        'live_at'             => $r->live_at ? mysql_to_rfc3339( $r->live_at ) : null,
        'submitted_at'        => $r->submitted_at ? mysql_to_rfc3339( $r->submitted_at ) : null,
        'returned_at'         => $r->returned_at ? mysql_to_rfc3339( $r->returned_at ) : null,
        'return_count'        => (int) $r->return_count,
        'created_at'          => mysql_to_rfc3339( $r->created_at ),
    ];
}

/**
 * Detalhe completo — inclui tudo o resumo + campos extra
 */
function rmbz_api_format_release_full( $r, $track_count ) {
    $base = rmbz_api_format_release_summary( $r, $track_count );

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

    return array_merge( $base, [
        'label_name'         => $r->label_name,
        'recording_location' => $r->recording_location,
        'upc_code'           => $r->upc_code,
        'rejection_note'     => $r->rejection_note,
        'takedown_reason'    => $r->takedown_reason,
        'return_deadline'    => $r->return_deadline ? mysql_to_rfc3339( $r->return_deadline ) : null,
        'return_problems'    => $r->return_problems ? json_decode( $r->return_problems, true ) : null,
        'extra'              => $extra,
    ] );
}

/**
 * Garante que URLs guardados no plugin (que podem ser caminhos relativos)
 * vêm sempre como URL absoluto para a app.
 */
function rmbz_api_safe_url( $url ) {
    if ( empty( $url ) ) return null;
    // Se já é URL completo
    if ( preg_match( '#^https?://#i', $url ) ) {
        return $url;
    }
    // Se é caminho relativo
    return rtrim( site_url(), '/' ) . '/' . ltrim( $url, '/' );
}

/* =============================================================================
 * [1.6.3] CONSTANTES E HELPERS COMPARTILHADOS
 * ============================================================================= */

/**
 * Idiomas aceites (ver distribution-security.php linha 180).
 */
function rmbz_api_release_languages() {
    return [ 'Portuguese', 'English', 'Spanish' ];
}

/**
 * Géneros aceites (ver distribution-security.php linha 181).
 */
function rmbz_api_release_genres() {
    return [ 'Hip Hop', 'Afrobeat', 'Pandza', 'Kizomba', 'R&B', 'Gospel', 'Pop', 'Eletrónica', 'Outro' ];
}

/**
 * Motivos válidos de takedown (ver distribution-lifecycle.php linha 187).
 */
function rmbz_api_takedown_reason_codes() {
    return [
        'rights'         => 'Direitos de autor / licença expirada',
        'metadata_error' => 'Erro na faixa / metadata',
        'relaunch'       => 'Vou relançar com nova versão',
        'collaborator'   => 'Conflito com colaborador',
        'other'          => 'Outro',
    ];
}

/**
 * Faz hard-fetch de um release garantindo ownership.
 * Devolve a row ou null.
 */
function rmbz_api_get_owned_release( $id, $telemovel ) {
    global $wpdb;
    return $wpdb->get_row( $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}rmbz_distro_releases
         WHERE id = %d AND artist_telemovel = %s",
        $id, $telemovel
    ) );
}

/* =============================================================================
 * GET /releases/options — listas para formulários
 * ============================================================================= */

function rmbz_api_handle_release_options( WP_REST_Request $request ) {
    $reason_codes = rmbz_api_takedown_reason_codes();
    $reasons_arr = [];
    foreach ( $reason_codes as $code => $label ) {
        $reasons_arr[] = [ 'code' => $code, 'label' => $label ];
    }

    return rmbz_api_ok( [
        'languages' => rmbz_api_release_languages(),
        'genres'    => rmbz_api_release_genres(),
        'release_types' => [
            [ 'code' => 'single', 'label' => 'Single (1-3 faixas)' ],
            [ 'code' => 'ep',     'label' => 'EP (4-6 faixas)' ],
            [ 'code' => 'album',  'label' => 'Álbum (7+ faixas)' ],
        ],
        'takedown_reasons'  => $reasons_arr,
        'takedown_min_days' => (int) get_option( 'rmbz_takedown_min_days_after_live', 30 ),
    ] );
}

/* =============================================================================
 * PUT /releases/{id} — editar metadados
 * ============================================================================= */

function rmbz_api_handle_update_release( WP_REST_Request $request ) {
    $telemovel = (string) $request->get_param( 'rmbz_telemovel' );
    $id = (int) $request->get_param( 'id' );

    $rel = rmbz_api_get_owned_release( $id, $telemovel );
    if ( ! $rel ) {
        return rmbz_api_error( 'Release não encontrado.', 404, 'not_found' );
    }

    // [1.5.284] Só editável se incomplete/draft ou returned (devolvido).
    // 'rejected' = rejeitado DEFINITIVAMENTE (sem volta) — NÃO é editável.
    $editable_statuses = [ 'incomplete', 'draft', 'returned' ];
    if ( ! in_array( $rel->status, $editable_statuses, true ) ) {
        $msg = $rel->status === 'rejected'
            ? 'Este lançamento foi rejeitado definitivamente e não pode ser editado. Cria um novo lançamento.'
            : 'Este release não pode ser editado no estado actual (' . $rel->status . ').';
        return rmbz_api_error( $msg, 409, 'not_editable' );
    }

    $body = $request->get_json_params();
    if ( ! is_array( $body ) ) $body = [];

    // Campos aceites — todos opcionais (PATCH-like)
    $update = [];
    $fmt    = [];
    $errors = [];

    if ( isset( $body['title'] ) ) {
        $title = sanitize_text_field( (string) $body['title'] );
        $len = mb_strlen( $title );
        if ( $len < 2 || $len > 150 ) {
            $errors[] = 'Título deve ter entre 2 e 150 caracteres.';
        } else {
            $update['title'] = $title;
            $fmt[] = '%s';
        }
    }

    if ( isset( $body['release_type'] ) ) {
        $rt = sanitize_key( (string) $body['release_type'] );
        if ( ! in_array( $rt, [ 'single', 'ep', 'album' ], true ) ) {
            $errors[] = 'Tipo inválido. Usa "single", "ep" ou "album".';
        } else {
            $update['release_type'] = $rt;
            $fmt[] = '%s';
        }
    }

    if ( isset( $body['language'] ) ) {
        $lang = sanitize_text_field( (string) $body['language'] );
        if ( $lang !== '' && ! in_array( $lang, rmbz_api_release_languages(), true ) ) {
            $errors[] = 'Idioma inválido.';
        } else {
            $update['language'] = $lang;
            $fmt[] = '%s';
        }
    }

    if ( isset( $body['primary_genre'] ) ) {
        $genre = sanitize_text_field( (string) $body['primary_genre'] );
        if ( $genre !== '' && ! in_array( $genre, rmbz_api_release_genres(), true ) ) {
            $errors[] = 'Género inválido.';
        } else {
            $update['primary_genre'] = $genre;
            $fmt[] = '%s';
        }
    }

    if ( isset( $body['release_date'] ) ) {
        $date = sanitize_text_field( (string) $body['release_date'] );
        if ( $date !== '' ) {
            $is_legacy = ! empty( $rel->is_legacy );
            if ( function_exists( 'rmbz_validate_release_date' ) ) {
                if ( ! rmbz_validate_release_date( $date, 7, $is_legacy ) ) {
                    $errors[] = $is_legacy
                        ? 'Data inválida (catálogo antigo: 1999 até hoje+90 dias).'
                        : 'Data deve estar pelo menos 7 dias no futuro.';
                }
            }
        }
        if ( ! $errors ) {
            $update['release_date'] = $date;
            $fmt[] = '%s';
        }
    }

    if ( isset( $body['label_name'] ) ) {
        $update['label_name'] = sanitize_text_field( (string) $body['label_name'] );
        $fmt[] = '%s';
    }

    if ( isset( $body['recording_location'] ) ) {
        $update['recording_location'] = sanitize_text_field( (string) $body['recording_location'] );
        $fmt[] = '%s';
    }

    if ( isset( $body['upc_code'] ) ) {
        $upc = sanitize_text_field( (string) $body['upc_code'] );
        if ( $upc !== '' && function_exists( 'rmbz_validate_upc' ) ) {
            if ( ! rmbz_validate_upc( $upc ) ) {
                $errors[] = 'UPC inválido. Use 12 ou 13 dígitos.';
            }
        }
        if ( ! in_array( 'UPC inválido. Use 12 ou 13 dígitos.', $errors, true ) ) {
            $update['upc_code'] = $upc;
            $fmt[] = '%s';
        }
    }

    if ( ! empty( $errors ) ) {
        return rmbz_api_error( implode( ' ', $errors ), 422, 'validation_error' );
    }

    if ( empty( $update ) ) {
        return rmbz_api_error( 'Nenhum campo para actualizar.', 400, 'no_changes' );
    }

    global $wpdb;
    $ok = $wpdb->update(
        $wpdb->prefix . 'rmbz_distro_releases',
        $update,
        [ 'id' => $id ],
        $fmt,
        [ '%d' ]
    );

    if ( $ok === false ) {
        return rmbz_api_error( 'Falha a actualizar release: ' . $wpdb->last_error, 500, 'db_error' );
    }

    // Devolver o release actualizado
    $updated = rmbz_api_get_owned_release( $id, $telemovel );
    $track_count = (int) $wpdb->get_var( $wpdb->prepare(
        "SELECT COUNT(*) FROM {$wpdb->prefix}rmbz_distro_tracks WHERE release_id = %d", $id
    ) );

    return rmbz_api_ok( rmbz_api_format_release_full( $updated, $track_count ) );
}

/* =============================================================================
 * DELETE /releases/{id} — apagar
 * ============================================================================= */

function rmbz_api_handle_delete_release( WP_REST_Request $request ) {
    $telemovel = (string) $request->get_param( 'rmbz_telemovel' );
    $id = (int) $request->get_param( 'id' );

    $rel = rmbz_api_get_owned_release( $id, $telemovel );
    if ( ! $rel ) {
        return rmbz_api_error( 'Release não encontrado.', 404, 'not_found' );
    }

    // Não permite apagar releases distribuídos — esses precisam de takedown primeiro
    $blocked_statuses = [ 'live', 'distribuido', 'takedown_requested', 'taken_down' ];
    if ( in_array( $rel->status, $blocked_statuses, true ) ) {
        return rmbz_api_error(
            'Releases publicados não podem ser apagados. Para remover das lojas, faz pedido de takedown.',
            409,
            'cannot_delete'
        );
    }

    global $wpdb;
    $wpdb->query( 'START TRANSACTION' );
    try {
        $wpdb->delete( $wpdb->prefix . 'rmbz_distro_tracks', [ 'release_id' => $id ], [ '%d' ] );
        $wpdb->delete( $wpdb->prefix . 'rmbz_distro_release_platforms', [ 'release_id' => $id ], [ '%d' ] );
        $wpdb->delete( $wpdb->prefix . 'rmbz_distro_releases', [ 'id' => $id ], [ '%d' ] );
        $wpdb->query( 'COMMIT' );
    } catch ( Exception $e ) {
        $wpdb->query( 'ROLLBACK' );
        return rmbz_api_error( 'Falha a apagar: ' . $e->getMessage(), 500, 'db_error' );
    }

    return rmbz_api_ok( [ 'deleted' => true, 'id' => $id ] );
}

/* =============================================================================
 * POST /releases/{id}/submit — incomplete → pendente
 * ============================================================================= */

function rmbz_api_handle_submit_release( WP_REST_Request $request ) {
    $telemovel = (string) $request->get_param( 'rmbz_telemovel' );
    $id = (int) $request->get_param( 'id' );

    // [1.6.339] Castigo de distribuição (per-user ou global).
    if ( function_exists( 'rmbz_service_blocked_message' ) ) {
        $blk = rmbz_service_blocked_message( $telemovel, 'block_distribution', 'distribution' );
        if ( $blk ) return rmbz_api_error( $blk, 403, 'distribution_blocked' );
    }

    $rel = rmbz_api_get_owned_release( $id, $telemovel );
    if ( ! $rel ) {
        return rmbz_api_error( 'Release não encontrado.', 404, 'not_found' );
    }

    if ( ! in_array( $rel->status, [ 'incomplete', 'draft' ], true ) ) {
        // [1.5.217] Mensagem mais útil: se for 'returned', dizer para usar /resubmit
        if ( $rel->status === 'returned' ) {
            return rmbz_api_error(
                'Este release foi devolvido pelo admin para correcções. ' .
                'Usa o endpoint /resubmit em vez de /submit.',
                409,
                'use_resubmit_endpoint'
            );
        }
        return rmbz_api_error(
            'Este release já foi submetido (status: ' . $rel->status . ').',
            409,
            'already_submitted'
        );
    }

    // Mesmas validações que o site (ver distribution-security.php rmbz_safe_finalize_release)
    $errors = rmbz_api_validate_release_submission( $rel );
    if ( ! empty( $errors ) ) {
        return rmbz_api_error( implode( ' ', $errors ), 422, 'incomplete_release' );
    }

    // Mudar status (não usar rmbz_distro_change_status porque essa função
    // espera contexto de admin; fazemos directo o update tal como o site faz)
    global $wpdb;
    $wpdb->query( 'START TRANSACTION' );
    try {
        $wpdb->update(
            $wpdb->prefix . 'rmbz_distro_releases',
            [ 'status' => 'pendente', 'submitted_at' => current_time( 'mysql' ) ],
            [ 'id' => $id ],
            [ '%s', '%s' ],
            [ '%d' ]
        );
        $wpdb->insert(
            $wpdb->prefix . 'rmbz_distro_status_history',
            [
                'release_id'  => $id,
                'from_status' => $rel->status,
                'to_status'   => 'pendente',
                'changed_by'  => 'app:' . $telemovel,
            ]
        );
        $wpdb->query( 'COMMIT' );
    } catch ( Exception $e ) {
        $wpdb->query( 'ROLLBACK' );
        return rmbz_api_error( 'Falha a submeter: ' . $e->getMessage(), 500, 'db_error' );
    }

    do_action( 'rmbz_distro_status_changed', $id, $rel->status, 'pendente', [] );

    return rmbz_api_ok( [
        'submitted' => true,
        'status'    => 'pendente',
        'message'   => 'Release submetido para revisão. Vais ser notificado quando for aprovado.',
    ] );
}

/* =============================================================================
 * POST /releases/{id}/resubmit — returned → pendente
 * ============================================================================= */

function rmbz_api_handle_resubmit_release( WP_REST_Request $request ) {
    $telemovel = (string) $request->get_param( 'rmbz_telemovel' );
    $id = (int) $request->get_param( 'id' );

    // [1.6.339] Castigo de distribuição (per-user ou global).
    if ( function_exists( 'rmbz_service_blocked_message' ) ) {
        $blk = rmbz_service_blocked_message( $telemovel, 'block_distribution', 'distribution' );
        if ( $blk ) return rmbz_api_error( $blk, 403, 'distribution_blocked' );
    }

    $rel = rmbz_api_get_owned_release( $id, $telemovel );
    if ( ! $rel ) {
        return rmbz_api_error( 'Release não encontrado.', 404, 'not_found' );
    }

    // [1.5.284] Só releases DEVOLVIDOS (returned) podem ser re-submetidos.
    // 'rejected' (rejeitado definitivamente) é terminal — sem re-submissão.
    if ( ! in_array( $rel->status, [ 'returned' ], true ) ) {
        $msg = $rel->status === 'rejected'
            ? 'Este lançamento foi rejeitado definitivamente e não pode ser reenviado. Cria um novo lançamento.'
            : 'Só podes re-submeter releases devolvidos (status actual: ' . $rel->status . ').';
        return rmbz_api_error( $msg, 409, 'not_resubmittable' );
    }

    // Validar de novo
    $errors = rmbz_api_validate_release_submission( $rel );
    if ( ! empty( $errors ) ) {
        return rmbz_api_error( implode( ' ', $errors ), 422, 'incomplete_release' );
    }

    global $wpdb;
    $wpdb->query( 'START TRANSACTION' );
    try {
        $wpdb->update(
            $wpdb->prefix . 'rmbz_distro_releases',
            [
                'status'         => 'pendente',
                'submitted_at'   => current_time( 'mysql' ),
                'rejection_reason' => null,
                'rejection_note'   => null,
                'returned_at'    => null,
                'return_problems' => null,
                'return_count'   => (int) $rel->return_count + 1,
            ],
            [ 'id' => $id ],
            [ '%s', '%s', '%s', '%s', '%s', '%s', '%d' ],
            [ '%d' ]
        );
        $wpdb->insert(
            $wpdb->prefix . 'rmbz_distro_status_history',
            [
                'release_id'  => $id,
                'from_status' => $rel->status,
                'to_status'   => 'pendente',
                'reason_code' => 'resubmit',
                'changed_by'  => 'app:' . $telemovel,
            ]
        );
        $wpdb->query( 'COMMIT' );
    } catch ( Exception $e ) {
        $wpdb->query( 'ROLLBACK' );
        return rmbz_api_error( 'Falha a re-submeter: ' . $e->getMessage(), 500, 'db_error' );
    }

    do_action( 'rmbz_distro_status_changed', $id, $rel->status, 'pendente', [] );

    return rmbz_api_ok( [
        'resubmitted' => true,
        'status'      => 'pendente',
        'message'     => 'Re-submetido com sucesso. A equipa vai rever as correcções.',
    ] );
}

/* =============================================================================
 * POST /releases/{id}/takedown — live → takedown_requested
 * ============================================================================= */

function rmbz_api_handle_request_takedown( WP_REST_Request $request ) {
    $telemovel = (string) $request->get_param( 'rmbz_telemovel' );
    $id = (int) $request->get_param( 'id' );

    $rel = rmbz_api_get_owned_release( $id, $telemovel );
    if ( ! $rel ) {
        return rmbz_api_error( 'Release não encontrado.', 404, 'not_found' );
    }

    // Só pode em estado live
    if ( ! in_array( $rel->status, [ 'live', 'distribuido' ], true ) ) {
        return rmbz_api_error(
            'Só podes pedir takedown de releases publicados (status actual: ' . $rel->status . ').',
            409,
            'not_takedownable'
        );
    }

    // Verificar idade mínima
    $min_days = (int) get_option( 'rmbz_takedown_min_days_after_live', 30 );
    if ( $min_days > 0 && ! empty( $rel->live_at ) ) {
        $live_ts = strtotime( $rel->live_at );
        $cutoff = $live_ts + ( $min_days * DAY_IN_SECONDS );
        if ( time() < $cutoff ) {
            $days_left = (int) ceil( ( $cutoff - time() ) / DAY_IN_SECONDS );
            return rmbz_api_error(
                sprintf( 'O takedown só pode ser pedido %d dias depois do release ficar publicado. Faltam %d dia(s).', $min_days, $days_left ),
                409,
                'too_early'
            );
        }
    }

    // Validar parâmetros
    $body = $request->get_json_params();
    if ( ! is_array( $body ) ) $body = [];

    $reason_code = isset( $body['reason_code'] ) ? sanitize_key( (string) $body['reason_code'] ) : '';
    $reason_text = isset( $body['reason_text'] ) ? sanitize_textarea_field( (string) $body['reason_text'] ) : '';

    $valid_reasons = rmbz_api_takedown_reason_codes();
    if ( ! isset( $valid_reasons[ $reason_code ] ) ) {
        return rmbz_api_error( 'Selecciona um motivo válido para o pedido.', 422, 'invalid_reason' );
    }

    if ( $reason_code === 'other' && mb_strlen( $reason_text ) < 10 ) {
        return rmbz_api_error( 'Para "Outro motivo" tens de explicar (mínimo 10 caracteres).', 422, 'reason_text_short' );
    }

    if ( mb_strlen( $reason_text ) > 1000 ) {
        return rmbz_api_error( 'Motivo demasiado longo (máximo 1000 caracteres).', 422, 'reason_text_long' );
    }

    // Construir o reason_text composto exactamente como o site faz
    $combined_reason = sprintf(
        '[%s] %s',
        $valid_reasons[ $reason_code ],
        $reason_text !== '' ? $reason_text : '(sem detalhes adicionais)'
    );

    // Mudança de status
    global $wpdb;
    $wpdb->query( 'START TRANSACTION' );
    try {
        $wpdb->update(
            $wpdb->prefix . 'rmbz_distro_releases',
            [
                'status'             => 'takedown_requested',
                'takedown_requested' => 1,
                'takedown_reason'    => $combined_reason,
            ],
            [ 'id' => $id ],
            [ '%s', '%d', '%s' ],
            [ '%d' ]
        );
        $wpdb->insert(
            $wpdb->prefix . 'rmbz_distro_status_history',
            [
                'release_id'  => $id,
                'from_status' => $rel->status,
                'to_status'   => 'takedown_requested',
                'reason_code' => 'artist_' . $reason_code,
                'reason_text' => $combined_reason,
                'changed_by'  => 'app:' . $telemovel,
            ]
        );
        $wpdb->query( 'COMMIT' );
    } catch ( Exception $e ) {
        $wpdb->query( 'ROLLBACK' );
        return rmbz_api_error( 'Falha a pedir takedown: ' . $e->getMessage(), 500, 'db_error' );
    }

    do_action( 'rmbz_distro_status_changed', $id, $rel->status, 'takedown_requested', [
        'reason_code' => 'artist_' . $reason_code,
        'reason_text' => $combined_reason,
    ] );

    return rmbz_api_ok( [
        'requested' => true,
        'status'    => 'takedown_requested',
        'message'   => 'Pedido de takedown enviado. A equipa vai rever em breve.',
    ] );
}

/* =============================================================================
 * VALIDAÇÃO COMPARTILHADA — usada pelos endpoints submit e resubmit
 * Reproduz as regras de rmbz_safe_finalize_release.
 * ============================================================================= */

function rmbz_api_validate_release_submission( $rel ) {
    global $wpdb;
    $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 — usa o site para fazer upload.';
    if ( empty( $rel->language ) )      $errors[] = 'Idioma em falta.';

    if ( empty( $rel->release_date ) ) {
        $errors[] = 'Data de lançamento em falta.';
    } elseif ( function_exists( 'rmbz_validate_release_date' ) ) {
        $is_legacy = ! empty( $rel->is_legacy );
        if ( ! rmbz_validate_release_date( $rel->release_date, 7, $is_legacy ) ) {
            $errors[] = $is_legacy
                ? 'Data inválida para catálogo antigo.'
                : 'Data inválida (mínimo 7 dias no futuro).';
        }
    }

    $track_count = (int) $wpdb->get_var( $wpdb->prepare(
        "SELECT COUNT(*) FROM {$wpdb->prefix}rmbz_distro_tracks WHERE release_id = %d",
        $rel->id
    ) );
    if ( $track_count < 1 ) {
        $errors[] = 'Adiciona pelo menos uma faixa (usa o site para fazer upload).';
    }

    // Validar tipo vs nº de faixas
    $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). Muda o tipo para EP/Álbum.', $track_count );
        }
        if ( $rt === 'ep' ) {
            if ( $track_count < 4 ) $errors[] = sprintf( 'EP exige pelo menos 4 faixas (tem %d).', $track_count );
            if ( $track_count > 6 ) $errors[] = sprintf( 'EP permite no máximo 6 faixas (tem %d).', $track_count );
        }
        if ( $rt === 'album' && $track_count < 7 ) {
            $errors[] = sprintf( 'Álbum exige pelo menos 7 faixas (tem %d).', $track_count );
        }
    }

    return $errors;
}

/* =============================================================================
 * [1.6.4] CRIAR RELEASE
 * ============================================================================= */

function rmbz_api_handle_create_release( WP_REST_Request $request ) {
    $telemovel = (string) $request->get_param( 'rmbz_telemovel' );

    // [1.6.339] Castigo de distribuição (per-user ou global).
    if ( function_exists( 'rmbz_service_blocked_message' ) ) {
        $blk = rmbz_service_blocked_message( $telemovel, 'block_distribution', 'distribution' );
        if ( $blk ) return rmbz_api_error( $blk, 403, 'distribution_blocked' );
    }

    // Rate limit
    if ( function_exists( 'rmbz_rate_limit' )
        && ! rmbz_rate_limit( 'create_release_' . $telemovel, 10 ) ) {
        return rmbz_api_error( 'Demasiados pedidos. Aguarda um minuto.', 429, 'rate_limit' );
    }

    $body = $request->get_json_params();
    if ( ! is_array( $body ) ) $body = [];

    $title = isset( $body['title'] ) ? sanitize_text_field( (string) $body['title'] ) : '';
    if ( mb_strlen( $title ) < 2 || mb_strlen( $title ) > 150 ) {
        return rmbz_api_error( 'Título deve ter entre 2 e 150 caracteres.', 422, 'invalid_title' );
    }

    $release_type = isset( $body['release_type'] ) ? sanitize_key( (string) $body['release_type'] ) : 'single';
    if ( ! in_array( $release_type, [ 'single', 'ep', 'album' ], true ) ) {
        $release_type = 'single';
    }

    // Campos opcionais
    $lang = isset( $body['language'] ) ? sanitize_text_field( (string) $body['language'] ) : '';
    if ( $lang !== '' && ! in_array( $lang, rmbz_api_release_languages(), true ) ) {
        return rmbz_api_error( 'Idioma inválido.', 422, 'invalid_language' );
    }

    $genre = isset( $body['primary_genre'] ) ? sanitize_text_field( (string) $body['primary_genre'] ) : '';
    if ( $genre !== '' && ! in_array( $genre, rmbz_api_release_genres(), true ) ) {
        return rmbz_api_error( 'Género inválido.', 422, 'invalid_genre' );
    }

    $date = isset( $body['release_date'] ) ? sanitize_text_field( (string) $body['release_date'] ) : '';
    if ( $date !== '' && function_exists( 'rmbz_validate_release_date' ) ) {
        if ( ! rmbz_validate_release_date( $date, 7, false ) ) {
            return rmbz_api_error( 'Data inválida (mínimo 7 dias no futuro).', 422, 'invalid_date' );
        }
    }

    $label = isset( $body['label_name'] ) ? sanitize_text_field( (string) $body['label_name'] ) : '';
    $location = isset( $body['recording_location'] ) ? sanitize_text_field( (string) $body['recording_location'] ) : '';

    $upc = isset( $body['upc_code'] ) ? sanitize_text_field( (string) $body['upc_code'] ) : '';
    if ( $upc !== '' && function_exists( 'rmbz_validate_upc' ) ) {
        if ( ! rmbz_validate_upc( $upc ) ) {
            return rmbz_api_error( 'UPC inválido. Use 12 ou 13 dígitos.', 422, 'invalid_upc' );
        }
    }

    $cover_url = isset( $body['cover_image'] ) ? esc_url_raw( (string) $body['cover_image'] ) : '';
    if ( $cover_url !== '' && ! rmbz_api_is_own_uploads_url( $cover_url ) ) {
        return rmbz_api_error( 'URL da capa não é válida (deve vir do nosso servidor).', 400, 'invalid_cover' );
    }

    global $wpdb;
    $data = [
        'artist_telemovel'   => $telemovel,
        'title'              => $title,
        'language'           => $lang,
        'primary_genre'      => $genre,
        'release_date'       => $date,
        'label_name'         => $label,
        'recording_location' => $location,
        'upc_code'           => $upc,
        'release_type'       => $release_type,
        'is_legacy'          => 0,
        'status'             => 'incomplete',
        'cover_image'        => $cover_url,
        'extra_data'         => wp_json_encode( [ 'release_type_manual' => 1 ] ),
    ];

    $ok = $wpdb->insert(
        $wpdb->prefix . 'rmbz_distro_releases',
        $data,
        [ '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s' ]
    );

    if ( $ok === false ) {
        return rmbz_api_error( 'Falha a criar release: ' . $wpdb->last_error, 500, 'db_error' );
    }

    $new_id = (int) $wpdb->insert_id;
    $created = rmbz_api_get_owned_release( $new_id, $telemovel );

    return rmbz_api_ok( rmbz_api_format_release_full( $created, 0 ) );
}

/* =============================================================================
 * [1.6.4] ATUALIZAR CAPA (depois do upload)
 * ============================================================================= */

function rmbz_api_handle_update_cover( WP_REST_Request $request ) {
    $telemovel = (string) $request->get_param( 'rmbz_telemovel' );
    $id = (int) $request->get_param( 'id' );

    $rel = rmbz_api_get_owned_release( $id, $telemovel );
    if ( ! $rel ) {
        return rmbz_api_error( 'Release não encontrado.', 404, 'not_found' );
    }

    if ( ! in_array( $rel->status, [ 'incomplete', 'draft', 'returned' ], true ) ) {
        return rmbz_api_error(
            'A capa só pode ser mudada em rascunhos ou releases devolvidos.',
            409,
            'not_editable'
        );
    }

    $body = $request->get_json_params();
    $cover_url = isset( $body['cover_image'] ) ? esc_url_raw( (string) $body['cover_image'] ) : '';

    if ( $cover_url === '' || ! rmbz_api_is_own_uploads_url( $cover_url ) ) {
        return rmbz_api_error( 'URL da capa inválida.', 400, 'invalid_cover' );
    }

    global $wpdb;
    $wpdb->update(
        $wpdb->prefix . 'rmbz_distro_releases',
        [ 'cover_image' => $cover_url ],
        [ 'id' => $id ],
        [ '%s' ],
        [ '%d' ]
    );

    return rmbz_api_ok( [ 'cover_image' => $cover_url ] );
}

/* =============================================================================
 * [1.6.4] CRIAR TRACK (consome upload_token do áudio)
 * ============================================================================= */

function rmbz_api_handle_create_track( WP_REST_Request $request ) {
    $telemovel = (string) $request->get_param( 'rmbz_telemovel' );
    $release_id = (int) $request->get_param( 'id' );

    if ( function_exists( 'rmbz_rate_limit' )
        && ! rmbz_rate_limit( 'api_save_track_' . $telemovel, 30 ) ) {
        return rmbz_api_error( 'Demasiados pedidos. Aguarda um minuto.', 429, 'rate_limit' );
    }

    $rel = rmbz_api_get_owned_release( $release_id, $telemovel );
    if ( ! $rel ) {
        return rmbz_api_error( 'Release não encontrado.', 404, 'not_found' );
    }

    // Só pode adicionar tracks em rascunhos ou devolvidos
    if ( ! in_array( $rel->status, [ 'incomplete', 'draft', 'returned' ], true ) ) {
        return rmbz_api_error(
            'Não podes adicionar faixas a um release neste estado (' . $rel->status . ').',
            409,
            'not_editable'
        );
    }

    $body = $request->get_json_params();
    if ( ! is_array( $body ) ) $body = [];

    // Validar título
    $title = isset( $body['title'] ) ? sanitize_text_field( (string) $body['title'] ) : '';
    if ( mb_strlen( $title ) < 1 || mb_strlen( $title ) > 200 ) {
        return rmbz_api_error( 'Título da faixa deve ter entre 1 e 200 caracteres.', 422, 'invalid_title' );
    }

    // upload_token obrigatório
    $upload_token = isset( $body['upload_token'] ) ? sanitize_text_field( (string) $body['upload_token'] ) : '';
    $upload_token = preg_replace( '/[^A-Za-z0-9]/', '', $upload_token );
    if ( $upload_token === '' ) {
        return rmbz_api_error( 'Falta o upload_token do áudio.', 422, 'no_upload_token' );
    }

    $stash = get_transient( 'rmbz_upl_' . $upload_token );
    if ( ! is_array( $stash ) ) {
        return rmbz_api_error( 'Upload expirado ou inválido. Carrega o ficheiro novamente.', 400, 'token_expired' );
    }
    if ( empty( $stash['telemovel'] ) || $stash['telemovel'] !== $telemovel ) {
        error_log( '[1MZ-API] upload_token mismatch artista' );
        return rmbz_api_error( 'Token inválido para este utilizador.', 403, 'token_mismatch' );
    }

    $audio_url  = $stash['url'] ?? '';
    $audio_info = is_array( $stash['audio_info'] ?? null ) ? $stash['audio_info'] : [];

    if ( $audio_url === '' ) {
        return rmbz_api_error( 'Token sem ficheiro associado.', 400, 'token_no_file' );
    }

    // Token consumido (one-shot)
    delete_transient( 'rmbz_upl_' . $upload_token );

    // Outros campos opcionais
    $feat_artists = isset( $body['feat_artists'] ) ? sanitize_text_field( (string) $body['feat_artists'] ) : '';
    $isrc = isset( $body['isrc'] ) ? sanitize_text_field( (string) $body['isrc'] ) : '';
    $is_explicit = ! empty( $body['is_explicit'] ) ? 1 : 0;

    // Determinar próximo track_order
    global $wpdb;
    $next_order = (int) $wpdb->get_var( $wpdb->prepare(
        "SELECT COALESCE(MAX(track_order), 0) + 1
         FROM {$wpdb->prefix}rmbz_distro_tracks WHERE release_id = %d",
        $release_id
    ) );

    // [1.6.14] Aceitar songwriters do body (cada um com 'name')
    // Estrutura: songwriters: [{ "name": "João Silva" }, { "name": "Maria Costa" }]
    $songwriters_in = isset( $body['songwriters'] ) && is_array( $body['songwriters'] )
        ? $body['songwriters']
        : [];
    $songwriters = [];
    foreach ( $songwriters_in as $sw ) {
        if ( is_array( $sw ) && ! empty( $sw['name'] ) ) {
            $name = sanitize_text_field( (string) $sw['name'] );
            if ( $name !== '' && mb_strlen( $name ) <= 200 ) {
                $songwriters[] = [ 'name' => $name ];
            }
        }
    }

    // [1.6.77] Campos avançados (lyrics, cover, mood, preview).
    // Validação via helper partilhado com update_track.
    $advanced = rmbz_api_extract_track_advanced_fields( $body );
    if ( $advanced['error'] !== null ) {
        return rmbz_api_error( $advanced['error'], 422, 'invalid_advanced_field' );
    }

    // extra_data com info do áudio + songwriters + campos avançados
    $extra_data_arr = array_merge(
        [
            'audio_info'  => $audio_info,
            'songwriters' => $songwriters,
        ],
        $advanced['fields']
    );
    $extra_data = wp_json_encode( $extra_data_arr );

    $ok = $wpdb->insert(
        $wpdb->prefix . 'rmbz_distro_tracks',
        [
            'release_id'   => $release_id,
            'track_title'  => $title,
            'feat_artists' => $feat_artists,
            'audio_url'    => $audio_url,
            'isrc_code'    => $isrc,
            'is_explicit'  => $is_explicit,
            'track_order'  => $next_order,
            'extra_data'   => $extra_data,
        ],
        [ '%d', '%s', '%s', '%s', '%s', '%d', '%d', '%s' ]
    );

    if ( $ok === false ) {
        return rmbz_api_error( 'Falha a criar faixa: ' . $wpdb->last_error, 500, 'db_error' );
    }

    $track_id = (int) $wpdb->insert_id;
    $track = $wpdb->get_row( $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}rmbz_distro_tracks WHERE id = %d", $track_id
    ) );

    return rmbz_api_ok( rmbz_api_format_track( $track ) );
}

/* =============================================================================
 * [1.6.4] ATUALIZAR TRACK (título, ISRC, explicit, ordem)
 * ============================================================================= */

function rmbz_api_handle_update_track( WP_REST_Request $request ) {
    $telemovel = (string) $request->get_param( 'rmbz_telemovel' );
    $release_id = (int) $request->get_param( 'id' );
    $track_id = (int) $request->get_param( 'track_id' );

    $rel = rmbz_api_get_owned_release( $release_id, $telemovel );
    if ( ! $rel ) {
        return rmbz_api_error( 'Release não encontrado.', 404, 'not_found' );
    }
    if ( ! in_array( $rel->status, [ 'incomplete', 'draft', 'returned' ], true ) ) {
        return rmbz_api_error( 'Release não editável neste estado.', 409, 'not_editable' );
    }

    global $wpdb;
    $track = $wpdb->get_row( $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}rmbz_distro_tracks
         WHERE id = %d AND release_id = %d",
        $track_id, $release_id
    ) );
    if ( ! $track ) {
        return rmbz_api_error( 'Faixa não encontrada.', 404, 'track_not_found' );
    }

    $body = $request->get_json_params();
    if ( ! is_array( $body ) ) $body = [];

    $update = [];
    $fmt = [];

    if ( isset( $body['title'] ) ) {
        $title = sanitize_text_field( (string) $body['title'] );
        if ( mb_strlen( $title ) < 1 || mb_strlen( $title ) > 200 ) {
            return rmbz_api_error( 'Título inválido (1-200 chars).', 422, 'invalid_title' );
        }
        $update['track_title'] = $title;
        $fmt[] = '%s';
    }
    if ( isset( $body['feat_artists'] ) ) {
        $update['feat_artists'] = sanitize_text_field( (string) $body['feat_artists'] );
        $fmt[] = '%s';
    }
    if ( isset( $body['isrc'] ) ) {
        $update['isrc_code'] = sanitize_text_field( (string) $body['isrc'] );
        $fmt[] = '%s';
    }
    if ( isset( $body['is_explicit'] ) ) {
        $update['is_explicit'] = ! empty( $body['is_explicit'] ) ? 1 : 0;
        $fmt[] = '%d';
    }
    if ( isset( $body['order'] ) ) {
        $update['track_order'] = max( 1, (int) $body['order'] );
        $fmt[] = '%d';
    }

    // [1.6.14] Songwriters — vivem em extra_data (JSON), não em coluna directa.
    // Se vier no body, fazemos merge com o extra_data existente preservando
    // outros campos (audio_info, etc).
    $update_songwriters = false;
    $new_songwriters = [];
    if ( isset( $body['songwriters'] ) && is_array( $body['songwriters'] ) ) {
        foreach ( $body['songwriters'] as $sw ) {
            if ( is_array( $sw ) && ! empty( $sw['name'] ) ) {
                $name = sanitize_text_field( (string) $sw['name'] );
                if ( $name !== '' && mb_strlen( $name ) <= 200 ) {
                    $new_songwriters[] = [ 'name' => $name ];
                }
            }
        }
        $update_songwriters = true;
    }

    // [1.6.77] Campos avançados (lyrics, cover, mood, preview, etc.)
    // — também vivem em extra_data. Extraímos via helper e fazemos merge.
    $advanced = rmbz_api_extract_track_advanced_fields( $body );
    if ( $advanced['error'] !== null ) {
        return rmbz_api_error( $advanced['error'], 422, 'invalid_advanced_field' );
    }
    $has_advanced_changes = ! empty( $advanced['fields'] );

    if ( $update_songwriters || $has_advanced_changes ) {
        // Carregar extra_data actual e fazer merge (preservando audio_info etc.)
        $current_extra = [];
        if ( ! empty( $track->extra_data ) ) {
            $decoded = json_decode( $track->extra_data, true );
            if ( is_array( $decoded ) ) $current_extra = $decoded;
        }
        if ( $update_songwriters ) {
            $current_extra['songwriters'] = $new_songwriters;
        }
        if ( $has_advanced_changes ) {
            // Merge cada campo avançado individualmente (não substitui tudo)
            foreach ( $advanced['fields'] as $key => $value ) {
                $current_extra[ $key ] = $value;
            }
        }
        $update['extra_data'] = wp_json_encode( $current_extra );
        $fmt[] = '%s';
    }

    if ( empty( $update ) ) {
        return rmbz_api_error( 'Nenhum campo para actualizar.', 400, 'no_changes' );
    }

    $wpdb->update(
        $wpdb->prefix . 'rmbz_distro_tracks',
        $update,
        [ 'id' => $track_id ],
        $fmt,
        [ '%d' ]
    );

    $updated = $wpdb->get_row( $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}rmbz_distro_tracks WHERE id = %d", $track_id
    ) );

    return rmbz_api_ok( rmbz_api_format_track( $updated ) );
}

/* =============================================================================
 * [1.6.4] APAGAR TRACK
 * ============================================================================= */

function rmbz_api_handle_delete_track( WP_REST_Request $request ) {
    $telemovel = (string) $request->get_param( 'rmbz_telemovel' );
    $release_id = (int) $request->get_param( 'id' );
    $track_id = (int) $request->get_param( 'track_id' );

    $rel = rmbz_api_get_owned_release( $release_id, $telemovel );
    if ( ! $rel ) {
        return rmbz_api_error( 'Release não encontrado.', 404, 'not_found' );
    }
    if ( ! in_array( $rel->status, [ 'incomplete', 'draft', 'returned' ], true ) ) {
        return rmbz_api_error( 'Release não editável neste estado.', 409, 'not_editable' );
    }

    global $wpdb;
    $deleted = $wpdb->delete(
        $wpdb->prefix . 'rmbz_distro_tracks',
        [ 'id' => $track_id, 'release_id' => $release_id ],
        [ '%d', '%d' ]
    );

    if ( $deleted === false || $deleted === 0 ) {
        return rmbz_api_error( 'Faixa não encontrada.', 404, 'track_not_found' );
    }

    return rmbz_api_ok( [ 'deleted' => true, 'id' => $track_id ] );
}

/* =============================================================================
 * HELPERS
 * ============================================================================= */

/**
 * Verifica se uma URL aponta para wp-content/uploads/ DESTE site.
 * Protege contra hotlinking e usar URLs externas.
 */
function rmbz_api_is_own_uploads_url( $url ) {
    if ( empty( $url ) ) return false;
    $upload_dir = wp_upload_dir();
    if ( empty( $upload_dir['baseurl'] ) ) return false;
    if ( strpos( $url, '..' ) !== false ) return false;
    return strpos( $url, $upload_dir['baseurl'] ) === 0;
}

/**
 * [1.6.77] Devolve a estrutura completa da Track em JSON.
 * Inclui todos os campos avançados (lyrics, cover, mood, preview, etc.)
 * que vivem no `extra_data` JSON da BD.
 *
 * O cliente (app Android, web) deve usar este formato para reconstruir
 * o estado do form quando edita uma faixa existente.
 *
 * Campos derivados de colunas directas: id, title, feat_artists,
 * audio_url, isrc, is_explicit, order.
 * Campos derivados de `extra_data` JSON: songwriters, has_lyrics, lyrics,
 * previously_released, original_isrc, is_cover, cover_original_title,
 * cover_original_composer, preview_start_seconds, moods, duration_seconds.
 */
function rmbz_api_format_track( $t ) {
    // [1.6.14] Extrair songwriters do extra_data (JSON)
    // [1.6.77] Agora extraímos TODOS os campos avançados também.
    // [1.5.209] Incluir collaborators (featuring, produtor, mistura, etc.)
    $songwriters    = [];
    $collaborators  = [];
    $has_lyrics     = 0;
    $lyrics         = '';
    $prev_released  = 0;
    $original_isrc  = '';
    $is_cover       = 0;
    $cover_orig_t   = '';
    $cover_orig_c   = '';
    $preview_start  = 0;
    $moods          = [];
    $duration_secs  = 0;

    if ( ! empty( $t->extra_data ) ) {
        $decoded = json_decode( $t->extra_data, true );
        if ( is_array( $decoded ) ) {
            // [1.5.209] Collaborators — array de { name, role }
            if ( isset( $decoded['collaborators'] ) && is_array( $decoded['collaborators'] ) ) {
                foreach ( $decoded['collaborators'] as $c ) {
                    if ( is_array( $c ) && ! empty( $c['name'] ) ) {
                        $collaborators[] = [
                            'name' => (string) $c['name'],
                            'role' => (string) ( $c['role'] ?? 'feature' ),
                        ];
                    }
                }
            }
            // Songwriters (já existia)
            if ( isset( $decoded['songwriters'] ) && is_array( $decoded['songwriters'] ) ) {
                foreach ( $decoded['songwriters'] as $sw ) {
                    if ( is_array( $sw ) && ! empty( $sw['name'] ) ) {
                        $songwriters[] = [ 'name' => (string) $sw['name'] ];
                    }
                }
            }
            // Créditos (tab Créditos do modal)
            $has_lyrics    = ! empty( $decoded['has_lyrics'] ) ? 1 : 0;
            $lyrics        = isset( $decoded['lyrics'] ) ? (string) $decoded['lyrics'] : '';
            $prev_released = ! empty( $decoded['previously_released'] ) ? 1 : 0;
            $original_isrc = isset( $decoded['original_isrc'] ) ? (string) $decoded['original_isrc'] : '';
            $is_cover      = ! empty( $decoded['is_cover'] ) ? 1 : 0;
            $cover_orig_t  = isset( $decoded['cover_original_title'] ) ? (string) $decoded['cover_original_title'] : '';
            $cover_orig_c  = isset( $decoded['cover_original_composer'] ) ? (string) $decoded['cover_original_composer'] : '';
            // Avançado (tab Avançado do modal)
            $preview_start = isset( $decoded['preview_start_seconds'] ) ? (int) $decoded['preview_start_seconds'] : 0;
            if ( isset( $decoded['moods'] ) && is_array( $decoded['moods'] ) ) {
                foreach ( $decoded['moods'] as $mood ) {
                    $m = sanitize_text_field( (string) $mood );
                    if ( $m !== '' ) $moods[] = $m;
                }
            }
            // Duração capturada pelo módulo track-editor
            $duration_secs = isset( $decoded['duration_seconds'] ) ? (int) $decoded['duration_seconds'] : 0;
        }
    }

    return [
        'id'           => (int) $t->id,
        'title'        => $t->track_title,
        'feat_artists' => $t->feat_artists,
        'audio_url'    => $t->audio_url ? rmbz_api_safe_url( $t->audio_url ) : null,
        'isrc'         => $t->isrc_code,
        'is_explicit'  => (bool) $t->is_explicit,
        'order'        => (int) $t->track_order,
        'songwriters'  => $songwriters,
        'collaborators' => $collaborators, // [1.5.209]
        // [1.6.77] Campos avançados (tab Créditos + Avançado do modal)
        'has_lyrics'                => $has_lyrics,
        'lyrics'                    => $lyrics,
        'previously_released'       => $prev_released,
        'original_isrc'             => $original_isrc,
        'is_cover'                  => $is_cover,
        'cover_original_title'      => $cover_orig_t,
        'cover_original_composer'   => $cover_orig_c,
        'preview_start_seconds'     => $preview_start,
        'moods'                     => $moods,
        'duration_seconds'          => $duration_secs,
    ];
}

/**
 * [1.6.77] Helper — extrai e sanitiza os campos avançados (lyrics,
 * cover, mood, etc.) do body, devolvendo um array pronto a fazer merge
 * com o `extra_data` JSON existente da faixa.
 *
 * Cada campo é apenas adicionado ao array se vier no `$body` (com
 * `isset()`). Isto permite ao cliente fazer PATCH parcial — só os campos
 * enviados é que são alterados, os omitidos ficam intactos.
 *
 * Validações:
 *   - lyrics até 30 000 chars (limite generoso para letras compridas)
 *   - originalIsrc no formato XX-XXX-XX-XXXXX (12 chars) ou vazio
 *   - preview_start_seconds entre 0 e 600 (10 min)
 *   - moods: lista de strings, max 10 moods, cada um ≤ 50 chars
 *
 * Retorna `[ 'fields' => [...], 'error' => null|string ]`. Se houver
 * erro de validação devolve string em `error`; chamador deve responder
 * com 422.
 */
function rmbz_api_extract_track_advanced_fields( $body ) {
    $fields = [];

    // [1.5.209] Colaboradores (featuring, produtor, mistura, etc.)
    // Aceita array de objectos { name, role } onde role é uma das funções
    // suportadas. Validação: role inválido → fallback "feature".
    if ( isset( $body['collaborators'] ) && is_array( $body['collaborators'] ) ) {
        $valid_roles = [ 'primary', 'feature', 'producer', 'mix', 'master',
                         'vocals', 'remix', 'songwriter' ];
        $collaborators = [];
        foreach ( $body['collaborators'] as $c ) {
            if ( ! is_array( $c ) ) continue;
            $name = sanitize_text_field( (string) ( $c['name'] ?? '' ) );
            if ( $name === '' || mb_strlen( $name ) > 200 ) continue;
            $role = (string) ( $c['role'] ?? 'feature' );
            if ( ! in_array( $role, $valid_roles, true ) ) $role = 'feature';
            $collaborators[] = [ 'name' => $name, 'role' => $role ];
            if ( count( $collaborators ) >= 30 ) break; // limite sensato
        }
        $fields['collaborators'] = $collaborators;
    }

    if ( isset( $body['has_lyrics'] ) ) {
        $fields['has_lyrics'] = ! empty( $body['has_lyrics'] ) ? 1 : 0;
    }
    if ( isset( $body['lyrics'] ) ) {
        $lyrics = (string) $body['lyrics'];
        if ( mb_strlen( $lyrics ) > 30000 ) {
            return [ 'fields' => [], 'error' => 'Letra demasiado longa (máx 30 000 caracteres).' ];
        }
        // Permitir quebras de linha, remover só tags HTML maliciosos
        $fields['lyrics'] = wp_kses( $lyrics, [] );
    }
    if ( isset( $body['previously_released'] ) ) {
        $fields['previously_released'] = ! empty( $body['previously_released'] ) ? 1 : 0;
    }
    if ( isset( $body['original_isrc'] ) ) {
        $oi = sanitize_text_field( (string) $body['original_isrc'] );
        // Validação básica de formato (vazio é OK, ou 12 chars alfanuméricos)
        if ( $oi !== '' && ! preg_match( '/^[A-Z0-9\-]{1,15}$/i', $oi ) ) {
            return [ 'fields' => [], 'error' => 'ISRC original inválido.' ];
        }
        $fields['original_isrc'] = strtoupper( $oi );
    }
    if ( isset( $body['is_cover'] ) ) {
        $fields['is_cover'] = ! empty( $body['is_cover'] ) ? 1 : 0;
    }
    if ( isset( $body['cover_original_title'] ) ) {
        $t = sanitize_text_field( (string) $body['cover_original_title'] );
        if ( mb_strlen( $t ) > 200 ) {
            return [ 'fields' => [], 'error' => 'Título original do cover demasiado longo (máx 200).' ];
        }
        $fields['cover_original_title'] = $t;
    }
    if ( isset( $body['cover_original_composer'] ) ) {
        $c = sanitize_text_field( (string) $body['cover_original_composer'] );
        if ( mb_strlen( $c ) > 200 ) {
            return [ 'fields' => [], 'error' => 'Compositor original do cover demasiado longo (máx 200).' ];
        }
        $fields['cover_original_composer'] = $c;
    }
    if ( isset( $body['preview_start_seconds'] ) ) {
        $ps = (int) $body['preview_start_seconds'];
        if ( $ps < 0 || $ps > 600 ) {
            return [ 'fields' => [], 'error' => 'Preview start deve estar entre 0 e 600 segundos.' ];
        }
        $fields['preview_start_seconds'] = $ps;
    }
    if ( isset( $body['moods'] ) && is_array( $body['moods'] ) ) {
        $moods = [];
        foreach ( $body['moods'] as $m ) {
            $clean = sanitize_text_field( (string) $m );
            if ( $clean !== '' && mb_strlen( $clean ) <= 50 ) {
                $moods[] = $clean;
            }
            if ( count( $moods ) >= 10 ) break;
        }
        $fields['moods'] = $moods;
    }

    return [ 'fields' => $fields, 'error' => null ];
}
