<?php
/**
 * 1MZ Music Player — Assistente IA (add-on)
 *
 * O app NUNCA tem a chave da Anthropic. O plugin guarda a chave + o modelo
 * (Opus 4.8 ou 4.6) e faz de proxy para a Messages API da Claude:
 *   POST /1mzmusic/v1/ai/translate  — traduz a letra (Bearer)
 *   POST /1mzmusic/v1/ai/recommend  — recomenda uma música (Bearer)
 *
 * @package 1MZ_Music_Player
 */

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

add_action( 'rest_api_init', 'mp_register_ai_routes' );

function mp_register_ai_routes() {
    register_rest_route( MP_API_NAMESPACE, '/ai/translate', [
        'methods'             => 'POST',
        'callback'            => 'mp_handle_ai_translate',
        'permission_callback' => '__return_true',
    ] );
    register_rest_route( MP_API_NAMESPACE, '/ai/recommend', [
        'methods'             => 'POST',
        'callback'            => 'mp_handle_ai_recommend',
        'permission_callback' => '__return_true',
    ] );
    register_rest_route( MP_API_NAMESPACE, '/ai/playlist', [
        'methods'             => 'POST',
        'callback'            => 'mp_handle_ai_playlist',
        'permission_callback' => '__return_true',
    ] );
    register_rest_route( MP_API_NAMESPACE, '/ai/cover', [
        'methods'             => 'POST',
        'callback'            => 'mp_handle_ai_cover',
        'permission_callback' => '__return_true',
    ] );
    register_rest_route( MP_API_NAMESPACE, '/ai/tags', [
        'methods'             => 'POST',
        'callback'            => 'mp_handle_ai_tags',
        'permission_callback' => '__return_true',
    ] );
    register_rest_route( MP_API_NAMESPACE, '/ai/lookup', [
        'methods'             => 'POST',
        'callback'            => 'mp_handle_ai_lookup',
        'permission_callback' => '__return_true',
    ] );
    register_rest_route( MP_API_NAMESPACE, '/ai/match', [
        'methods'             => 'POST',
        'callback'            => 'mp_handle_ai_match',
        'permission_callback' => '__return_true',
    ] );
    register_rest_route( MP_API_NAMESPACE, '/ai/verify', [
        'methods'             => 'POST',
        'callback'            => 'mp_handle_ai_verify',
        'permission_callback' => '__return_true',
    ] );
    register_rest_route( MP_API_NAMESPACE, '/ai/diagnose', [
        'methods'             => 'POST',
        'callback'            => 'mp_handle_ai_diagnose',
        'permission_callback' => '__return_true',
    ] );
    register_rest_route( MP_API_NAMESPACE, '/ai/yturl', [
        'methods'             => 'POST',
        'callback'            => 'mp_handle_ai_yturl',
        'permission_callback' => '__return_true',
    ] );
}

/** POST /ai/diagnose - interpreta um relatorio tecnico sem credenciais. */
function mp_handle_ai_diagnose( WP_REST_Request $request ) {
    $row = mp_account_from_request( $request );
    if ( ! $row ) return rmbz_api_error( 'Inicia sessao.', 401, 'unauthorized' );
    if ( ! mp_ai_enabled() || ! get_option( 'mp_ai_diagnostics', 1 ) ) {
        return rmbz_api_error( 'Diagnostico por IA desativado.', 403, 'disabled' );
    }
    $report = (string) $request->get_param( 'report' );
    if ( trim( $report ) === '' ) return rmbz_api_error( 'Sem relatorio.', 400, 'empty' );
    $report = preg_replace( '#https?://\S+#i', '[url removida]', $report );
    $report = preg_replace( '/(?i)(bearer|token|password|secret|signature)[^,}\s]*/', '$1=[removido]', $report );
    $system = 'Es o agente de diagnostico tecnico do leitor 1MZ. Recebes JSON com testes do Android e eventos sem credenciais. '
            . 'Responde em portugues claro com: Estado geral; Causa mais provavel; Acoes por ordem. Maximo 180 palavras. '
            . 'Nao inventes testes, nao pecas palavra-passe ou token e distingue telefone, rede, servidor e YouTube. '
            . 'Se nao houver erro, confirma e menciona apenas avisos uteis. Sem links e sem markdown complexo.';
    $out = mp_ai_call( $system, mb_substr( $report, 0, 8000 ), 350 );
    if ( is_wp_error( $out ) ) return rmbz_api_error( $out->get_error_message(), 502, 'ai_error' );
    return rmbz_api_ok( [ 'analysis' => trim( (string) $out ) ] );
}

/**
 * POST /ai/yturl — analisa um texto partilhado (de qualquer app) e extrai a URL
 * do vídeo do YouTube. { url: "https://…" } ou 404 quando não há vídeo no texto.
 */
function mp_handle_ai_yturl( WP_REST_Request $request ) {
    $row = mp_account_from_request( $request );
    if ( ! $row ) {
        return rmbz_api_error( 'Inicia sessão.', 401, 'unauthorized' );
    }
    if ( ! mp_ai_enabled() ) {
        return rmbz_api_error( 'Assistente IA desativado.', 403, 'disabled' );
    }
    if ( ! get_option( 'mp_ai_lookup', 1 ) ) {
        return rmbz_api_error( 'Análise desativada.', 403, 'disabled' );
    }
    $text = sanitize_textarea_field( (string) $request->get_param( 'text' ) );
    if ( trim( $text ) === '' ) {
        return rmbz_api_error( 'Sem texto.', 400, 'empty' );
    }
    $system = 'Recebes um texto partilhado de outra aplicação. Extrai a URL de UM vídeo do YouTube '
            . '(formatos: youtube.com/watch?v=…, youtu.be/…, youtube.com/shorts/…, music.youtube.com/…). '
            . 'Limpa parâmetros de tracking (si, feature, list quando não for o vídeo). '
            . 'Responde APENAS com a URL completa, ou "NAO" se o texto não contiver nenhum vídeo do YouTube.';
    $out = mp_ai_call( $system, mb_substr( $text, 0, 1500 ), 100 );
    if ( is_wp_error( $out ) ) {
        return rmbz_api_error( $out->get_error_message(), 502, 'ai_error' );
    }
    $out = trim( (string) $out );
    if ( ! preg_match( '#https?://\S*(youtube\.com|youtu\.be)/\S+#i', $out, $m ) ) {
        return rmbz_api_error( 'Sem vídeo do YouTube no texto.', 404, 'not_found' );
    }
    return rmbz_api_ok( [ 'url' => $m[0] ] );
}

/**
 * POST /ai/verify — auditoria do restauro: recebe a lista "esperado vs aplicado"
 * e devolve um veredicto curto e humano. { verdict: "…" }
 */
function mp_handle_ai_verify( WP_REST_Request $request ) {
    $row = mp_account_from_request( $request );
    if ( ! $row ) {
        return rmbz_api_error( 'Inicia sessão.', 401, 'unauthorized' );
    }
    if ( ! mp_ai_enabled() ) {
        return rmbz_api_error( 'Assistente IA desativado.', 403, 'disabled' );
    }
    $report = sanitize_textarea_field( (string) $request->get_param( 'report' ) );
    if ( trim( $report ) === '' ) {
        return rmbz_api_error( 'Sem relatório.', 400, 'empty' );
    }
    $system = 'És o auditor de restauros de uma app de música (1MZ). Recebes o relatório item a item de um restauro '
            . '(o que era esperado e o que ficou aplicado no telefone). Responde em português, no máximo 2 frases: '
            . 'se tudo bate certo, confirma com confiança; se algum item falhou, diz exatamente qual e recomenda '
            . 'repetir só esse. Sem listas, sem markdown.';
    $out = mp_ai_call( $system, mb_substr( $report, 0, 3000 ), 200 );
    if ( is_wp_error( $out ) ) {
        return rmbz_api_error( $out->get_error_message(), 502, 'ai_error' );
    }
    return rmbz_api_ok( [ 'verdict' => trim( (string) $out ) ] );
}

/**
 * POST /ai/match — verificador de duplicados: a faixa A (nuvem) e a faixa B
 * (telefone) são a MESMA gravação? Usado pelo app quando a verificação local
 * fica em dúvida. Responde { same: true|false }.
 */
function mp_handle_ai_match( WP_REST_Request $request ) {
    $row = mp_account_from_request( $request );
    if ( ! $row ) {
        return rmbz_api_error( 'Inicia sessão.', 401, 'unauthorized' );
    }
    if ( ! mp_ai_enabled() ) {
        return rmbz_api_error( 'Assistente IA desativado.', 403, 'disabled' );
    }
    if ( ! get_option( 'mp_ai_lookup', 1 ) ) {
        return rmbz_api_error( 'Verificação desativada.', 403, 'disabled' );
    }
    $fmt = function ( $p ) use ( $request ) {
        $t = sanitize_text_field( (string) $request->get_param( $p . '_title' ) );
        $a = sanitize_text_field( (string) $request->get_param( $p . '_artist' ) );
        $d = max( 0, (int) $request->get_param( $p . '_dur' ) );
        return "título: {$t} | artista: {$a} | duração: " . ( $d > 0 ? round( $d / 1000 ) . 's' : 'desconhecida' );
    };
    $system = 'És um verificador de duplicados de músicas. Recebes duas faixas e decides se são a MESMA gravação '
            . '(o mesmo áudio, ainda que o título esteja escrito de forma diferente, com "(Official Video)", feat., etc.). '
            . 'Versões diferentes (remix, live, acústica, cover, sped up) NÃO são a mesma. Responde APENAS "SIM" ou "NAO".';
    $user = "Faixa A — " . $fmt( 'a' ) . "\nFaixa B — " . $fmt( 'b' );
    $out = mp_ai_call( $system, $user, 8 );
    if ( is_wp_error( $out ) ) {
        return rmbz_api_error( $out->get_error_message(), 502, 'ai_error' );
    }
    return rmbz_api_ok( [ 'same' => ( false !== stripos( (string) $out, 'sim' ) ) ] );
}

/** A melhoria de capas (HD) está ligada no admin? */
function mp_ai_covers_enabled() {
    return (bool) get_option( 'mp_ai_covers', 0 );
}

/** O «Pesquisar tags com IA» está ligado no admin? (ligado por defeito) */
function mp_ai_tags_enabled() {
    return (bool) get_option( 'mp_ai_tags', 1 );
}

/** Decodifica JSON mesmo que venha dentro de blocos markdown (```json … ```). */
function mp_ai_json( $text ) {
    $t = trim( (string) $text );
    // Remove cercas de código markdown.
    $t = preg_replace( '/^```[a-zA-Z]*\s*/', '', $t );
    $t = preg_replace( '/\s*```$/', '', $t );
    // Se ainda houver texto à volta, fica só com o primeiro objecto/array JSON.
    if ( preg_match( '/[\{\[].*[\}\]]/s', $t, $m ) ) {
        $t = $m[0];
    }
    $j = json_decode( trim( $t ), true );
    return is_array( $j ) ? $j : null;
}

/** O add-on de IA está ligado? */
function mp_ai_enabled() {
    foreach ( mp_get_features() as $f ) {
        if ( $f['key'] === 'ai_assistant' ) {
            return (bool) $f['enabled'];
        }
    }
    return false;
}

/** Modelo Claude escolhido no admin (default: Opus 4.8). */
function mp_ai_model() {
    $m = (string) get_option( 'mp_ai_model', 'claude-opus-4-8' );
    return in_array( $m, [ 'claude-opus-4-8', 'claude-opus-4-6' ], true ) ? $m : 'claude-opus-4-8';
}

/**
 * Chama a Messages API da Claude e devolve o texto, ou um WP_Error.
 * Usa wp_remote_post (HTTP API do WordPress) — a chave fica no servidor.
 */
function mp_ai_call( $system, $user_text, $max_tokens = 2000 ) {
    $key = trim( (string) get_option( 'mp_ai_api_key', '' ) );
    if ( $key === '' ) {
        return new WP_Error( 'no_key', 'Chave da IA não configurada.' );
    }

    $body = [
        'model'      => mp_ai_model(),
        'max_tokens' => (int) $max_tokens,
        'messages'   => [
            [ 'role' => 'user', 'content' => $user_text ],
        ],
    ];
    if ( $system !== '' ) {
        $body['system'] = $system;
    }

    $resp = wp_remote_post( 'https://api.anthropic.com/v1/messages', [
        'timeout' => 45,
        'headers' => [
            'content-type'      => 'application/json',
            'x-api-key'         => $key,
            'anthropic-version' => '2023-06-01',
        ],
        'body'    => wp_json_encode( $body ),
    ] );

    if ( is_wp_error( $resp ) ) {
        return $resp;
    }
    $code = (int) wp_remote_retrieve_response_code( $resp );
    $data = json_decode( wp_remote_retrieve_body( $resp ), true );
    if ( $code !== 200 ) {
        $msg = $data['error']['message'] ?? 'Erro da IA.';
        return new WP_Error( 'ai_http', $msg, [ 'status' => $code ] );
    }

    // A resposta é uma lista de blocos; junta os blocos de texto.
    $text = '';
    foreach ( (array) ( $data['content'] ?? [] ) as $block ) {
        if ( ( $block['type'] ?? '' ) === 'text' ) {
            $text .= $block['text'];
        }
    }
    return trim( $text );
}

/** Traduz a letra mantendo o número de linhas. */
function mp_handle_ai_translate( WP_REST_Request $request ) {
    $row = mp_account_from_request( $request );
    if ( ! $row ) {
        return rmbz_api_error( 'Inicia sessão.', 401, 'unauthorized' );
    }
    if ( ! mp_ai_enabled() ) {
        return rmbz_api_error( 'Assistente IA desativado.', 403, 'disabled' );
    }
    if ( ! get_option( 'mp_ai_translate', 1 ) ) {
        return rmbz_api_error( 'Tradução desativada.', 403, 'disabled' );
    }

    $lyrics = (string) $request->get_param( 'lyrics' );
    $lang   = sanitize_text_field( (string) $request->get_param( 'target_lang' ) );
    if ( $lang === '' ) {
        $lang = 'Português';
    }
    if ( trim( $lyrics ) === '' ) {
        return rmbz_api_error( 'Sem letra para traduzir.', 400, 'empty' );
    }
    $lyrics = mb_substr( $lyrics, 0, 6000 ); // limite de segurança

    $system = "És um tradutor de letras de música. Traduz para {$lang} preservando EXACTAMENTE o número de linhas e o sentido poético. "
            . "Devolve apenas as linhas traduzidas, uma por linha, na mesma ordem. Sem comentários, títulos nem numeração.";
    $out = mp_ai_call( $system, $lyrics, 2000 );
    if ( is_wp_error( $out ) ) {
        return rmbz_api_error( $out->get_error_message(), 502, 'ai_error' );
    }
    return rmbz_api_ok( [ 'translation' => $out, 'target_lang' => $lang ] );
}

/** Recomenda UMA música real parecida com o gosto do utilizador. */
function mp_handle_ai_recommend( WP_REST_Request $request ) {
    $row = mp_account_from_request( $request );
    if ( ! $row ) {
        return rmbz_api_error( 'Inicia sessão.', 401, 'unauthorized' );
    }
    if ( ! mp_ai_enabled() ) {
        return rmbz_api_error( 'Assistente IA desativado.', 403, 'disabled' );
    }
    if ( ! get_option( 'mp_ai_recommend', 1 ) ) {
        return rmbz_api_error( 'Recomendações desativadas.', 403, 'disabled' );
    }

    $seed    = sanitize_text_field( (string) $request->get_param( 'seed' ) );   // "Título — Artista"
    $liked   = sanitize_textarea_field( (string) $request->get_param( 'liked' ) );
    $avoid   = sanitize_textarea_field( (string) $request->get_param( 'avoid' ) );
    $session = sanitize_text_field( (string) $request->get_param( 'session' ) );
    $avoid   = mb_substr( $avoid, 0, 2000 );

    $system = "És um curador musical pessoal. Recomenda UMA música REAL (não inventes). A música atual é apenas contexto: "
            . "uma única audição nunca define sozinha o gosto do utilizador. Combina afinidade com descoberta, varia artistas "
            . "e NUNCA devolvas a própria música atual nem uma faixa da lista «Não repetir». "
            . "Responde só em JSON válido com as chaves \"title\", \"artist\" e \"reason\" (motivo curto em português). Nada fora do JSON.";
    $user = "Música atual (sinal suave): " . ( $seed !== '' ? $seed : 'desconhecida' )
          . "\nGostos confirmados e variados: " . ( $liked !== '' ? $liked : '—' )
          . ( $avoid !== '' ? "\nNão repetir:\n{$avoid}" : '' )
          . ( $session !== '' ? "\nSessão: {$session}" : '' );

    $out = mp_ai_call( $system, $user, 400 );
    if ( is_wp_error( $out ) ) {
        return rmbz_api_error( $out->get_error_message(), 502, 'ai_error' );
    }
    $j = mp_ai_json( $out );
    if ( is_array( $j ) && isset( $j['title'] ) ) {
        return rmbz_api_ok( [
            'title'  => (string) $j['title'],
            'artist' => (string) ( $j['artist'] ?? '' ),
            'reason' => (string) ( $j['reason'] ?? '' ),
        ] );
    }
    // Caso o modelo não devolva JSON, mostra o texto como motivo.
    return rmbz_api_ok( [ 'title' => '', 'artist' => '', 'reason' => trim( (string) $out ) ] );
}

/**
 * Editor de tags assistido por IA: a partir do título/artista (e nome do ficheiro),
 * a IA identifica a música e devolve as tags corretas.
 * Devolve { title, artist, album, year, genre }.
 */
function mp_handle_ai_tags( WP_REST_Request $request ) {
    $row = mp_account_from_request( $request );
    if ( ! $row ) {
        return rmbz_api_error( 'Inicia sessão.', 401, 'unauthorized' );
    }
    if ( ! mp_ai_enabled() ) {
        return rmbz_api_error( 'Assistente IA desativado.', 403, 'disabled' );
    }
    if ( ! mp_ai_tags_enabled() ) {
        return rmbz_api_error( 'Pesquisa de tags desativada.', 403, 'disabled' );
    }

    $title  = sanitize_text_field( (string) $request->get_param( 'title' ) );
    $artist = sanitize_text_field( (string) $request->get_param( 'artist' ) );
    $file   = sanitize_text_field( (string) $request->get_param( 'filename' ) );
    if ( $title === '' && $artist === '' && $file === '' ) {
        return rmbz_api_error( 'Sem dados para pesquisar.', 400, 'empty' );
    }

    $system = "És um especialista em metadados musicais. Identifica a música REAL a partir das pistas dadas (não inventes dados que não consigas identificar). "
            . "Corrige maiúsculas/grafia e devolve as tags corretas. "
            . "Responde APENAS em JSON válido com as chaves \"title\", \"artist\", \"album\", \"year\", \"genre\" "
            . "(year = ano de lançamento em texto, \"\" se desconhecido; genre em português). Nada fora do JSON.";
    $user = "Título atual: " . ( $title !== '' ? $title : '—' )
          . "\nArtista atual: " . ( $artist !== '' ? $artist : '—' )
          . "\nNome do ficheiro: " . ( $file !== '' ? $file : '—' );

    $out = mp_ai_call( $system, $user, 400 );
    if ( is_wp_error( $out ) ) {
        return rmbz_api_error( $out->get_error_message(), 502, 'ai_error' );
    }
    $j = mp_ai_json( $out );
    if ( is_array( $j ) && ( isset( $j['title'] ) || isset( $j['artist'] ) ) ) {
        return rmbz_api_ok( [
            'title'  => (string) ( $j['title'] ?? '' ),
            'artist' => (string) ( $j['artist'] ?? '' ),
            'album'  => (string) ( $j['album'] ?? '' ),
            'year'   => (string) ( $j['year'] ?? '' ),
            'genre'  => (string) ( $j['genre'] ?? '' ),
        ] );
    }
    return rmbz_api_error( 'A IA não conseguiu identificar a música.', 502, 'no_match' );
}

/**
 * Pesquisa avançada em VÁRIAS plataformas digitais (Apple/iTunes + Deezer):
 * traz tags oficiais + a capa em alta resolução. Combina o melhor de cada fonte.
 * Devolve { title, artist, album, year, genre, cover, source }.
 */
function mp_handle_ai_lookup( WP_REST_Request $request ) {
    $row = mp_account_from_request( $request );
    if ( ! $row ) {
        return rmbz_api_error( 'Inicia sessão.', 401, 'unauthorized' );
    }
    if ( ! mp_ai_enabled() ) {
        return rmbz_api_error( 'Assistente IA desativado.', 403, 'disabled' );
    }
    if ( ! get_option( 'mp_ai_lookup', 1 ) ) {
        return rmbz_api_error( 'Pesquisa avançada desativada.', 403, 'disabled' );
    }

    $title  = sanitize_text_field( (string) $request->get_param( 'title' ) );
    $artist = sanitize_text_field( (string) $request->get_param( 'artist' ) );
    $term   = trim( $artist . ' ' . $title );
    if ( $term === '' ) {
        return rmbz_api_error( 'Sem dados da faixa.', 400, 'empty' );
    }

    $out      = [ 'title' => '', 'artist' => '', 'album' => '', 'year' => '', 'genre' => '', 'cover' => '' ];
    $sources  = [];

    // --- Apple / iTunes (bom para género e ano) ---
    $it_url = add_query_arg( [ 'term' => $term, 'entity' => 'song', 'limit' => 1 ], 'https://itunes.apple.com/search' );
    $it = wp_remote_get( $it_url, [ 'timeout' => 20 ] );
    if ( ! is_wp_error( $it ) ) {
        $d = json_decode( wp_remote_retrieve_body( $it ), true );
        $r = $d['results'][0] ?? null;
        if ( $r ) {
            $out['title']  = (string) ( $r['trackName'] ?? '' );
            $out['artist'] = (string) ( $r['artistName'] ?? '' );
            $out['album']  = (string) ( $r['collectionName'] ?? '' );
            $out['year']   = substr( (string) ( $r['releaseDate'] ?? '' ), 0, 4 );
            $out['genre']  = (string) ( $r['primaryGenreName'] ?? '' );
            if ( ! empty( $r['artworkUrl100'] ) ) {
                $out['cover'] = str_replace( '100x100bb', '600x600bb', (string) $r['artworkUrl100'] );
            }
            $sources[] = 'Apple';
        }
    }

    // --- Deezer (capa em altíssima resolução: cover_xl 1000x1000) ---
    $dz_url = add_query_arg( [ 'q' => $term, 'limit' => 1 ], 'https://api.deezer.com/search' );
    $dz = wp_remote_get( $dz_url, [ 'timeout' => 20 ] );
    if ( ! is_wp_error( $dz ) ) {
        $d = json_decode( wp_remote_retrieve_body( $dz ), true );
        $r = $d['data'][0] ?? null;
        if ( $r ) {
            if ( $out['title'] === '' )  { $out['title']  = (string) ( $r['title'] ?? '' ); }
            if ( $out['artist'] === '' ) { $out['artist'] = (string) ( $r['artist']['name'] ?? '' ); }
            if ( $out['album'] === '' )  { $out['album']  = (string) ( $r['album']['title'] ?? '' ); }
            // Deezer dá capa maior — preferimos esta.
            $xl = (string) ( $r['album']['cover_xl'] ?? $r['album']['cover_big'] ?? '' );
            if ( $xl !== '' ) { $out['cover'] = $xl; }
            $sources[] = 'Deezer';
        }
    }

    if ( $out['title'] === '' && $out['artist'] === '' && $out['cover'] === '' ) {
        return rmbz_api_error( 'Não encontrámos esta música em nenhuma plataforma.', 404, 'not_found' );
    }
    $out['source'] = implode( ' + ', array_unique( $sources ) );
    return rmbz_api_ok( $out );
}

/**
 * Curadoria: a partir da biblioteca do utilizador (lista numerada), a IA
 * escolhe as melhores músicas e ordena-as numa playlist coerente (não aleatória).
 * Devolve { name, tracks:[índices 1..N escolhidos pela ordem da playlist] }.
 */
function mp_handle_ai_playlist( WP_REST_Request $request ) {
    $row = mp_account_from_request( $request );
    if ( ! $row ) {
        return rmbz_api_error( 'Inicia sessão.', 401, 'unauthorized' );
    }
    if ( ! mp_ai_enabled() ) {
        return rmbz_api_error( 'Assistente IA desativado.', 403, 'disabled' );
    }

    if ( ! get_option( 'mp_ai_playlist', 1 ) && ! get_option( 'mp_ai_shuffle', 1 ) ) {
        return rmbz_api_error( 'Curadoria/aleatório IA desativados.', 403, 'disabled' );
    }

    $library = (string) $request->get_param( 'library' ); // "1. Título — Artista\n2. ..."
    $vibe    = sanitize_text_field( (string) $request->get_param( 'vibe' ) );
    $avoid   = sanitize_textarea_field( (string) $request->get_param( 'avoid' ) ); // tocadas recentemente
    if ( trim( $library ) === '' ) {
        return rmbz_api_error( 'Biblioteca vazia.', 400, 'empty' );
    }
    $library = mb_substr( $library, 0, 12000 );
    $avoid   = mb_substr( $avoid, 0, 3000 );

    $system = "És um curador musical pessoal. A partir da biblioteca NUMERADA do utilizador, escolhe e ordena uma sequência coerente e agradável. "
            . "REGRAS: usa só músicas da lista; NUNCA repitas a mesma faixa; trata [favorita explícita] e [gosto confirmado] como sinais suaves, nunca como obrigação; "
            . "uma única faixa não pode dominar a sessão; mantém aproximadamente 40% familiar e 60% variedade/descoberta; intercala artistas, álbuns e estilos; "
            . "não coloques o mesmo artista nas 3 posições seguintes quando existirem alternativas; EVITA (ou deixa para o fim) as faixas de «Tocadas recentemente». "
            . "Entre 12 e 25 faixas se houver. "
            . "Responde APENAS em JSON: {\"name\":\"<nome curto>\",\"tracks\":[<números das músicas pela ordem>]}. Nada fora do JSON.";
    $user = ( $vibe !== '' ? "Vibe pretendida: {$vibe}\n" : '' )
          . ( $avoid !== '' ? "Tocadas recentemente (evita repetir):\n{$avoid}\n\n" : '' )
          . "Biblioteca:\n" . $library;

    $out = mp_ai_call( $system, $user, 700 );
    if ( is_wp_error( $out ) ) {
        return rmbz_api_error( $out->get_error_message(), 502, 'ai_error' );
    }
    $j = mp_ai_json( $out );
    $tracks = [];
    if ( is_array( $j ) && isset( $j['tracks'] ) && is_array( $j['tracks'] ) ) {
        foreach ( $j['tracks'] as $n ) {
            $n = (int) $n;
            if ( $n > 0 ) { $tracks[] = $n; }
        }
    }
    if ( empty( $tracks ) ) {
        return rmbz_api_error( 'A IA não devolveu uma playlist válida.', 502, 'ai_bad' );
    }
    $name = is_array( $j ) ? trim( (string) ( $j['name'] ?? '' ) ) : '';
    return rmbz_api_ok( [ 'name' => $name !== '' ? $name : 'Para ti (IA)', 'tracks' => array_values( array_unique( $tracks ) ) ] );
}

/**
 * Devolve a MESMA capa do álbum em alta resolução (catálogo iTunes), para
 * substituir capas de baixa qualidade. O ficheiro original do user NÃO é tocado;
 * o app apenas mostra esta nova imagem.
 */
function mp_handle_ai_cover( WP_REST_Request $request ) {
    $row = mp_account_from_request( $request );
    if ( ! $row ) {
        return rmbz_api_error( 'Inicia sessão.', 401, 'unauthorized' );
    }
    if ( ! mp_ai_enabled() || ! mp_ai_covers_enabled() ) {
        return rmbz_api_error( 'Melhoria de capas desativada.', 403, 'disabled' );
    }

    $title  = sanitize_text_field( (string) $request->get_param( 'title' ) );
    $artist = sanitize_text_field( (string) $request->get_param( 'artist' ) );
    $term   = trim( $artist . ' ' . $title );
    if ( $term === '' ) {
        return rmbz_api_error( 'Sem dados da faixa.', 400, 'empty' );
    }

    $url = add_query_arg( [
        'term'   => $term,
        'entity' => 'song',
        'limit'  => 1,
    ], 'https://itunes.apple.com/search' );

    $resp = wp_remote_get( $url, [ 'timeout' => 20 ] );
    if ( is_wp_error( $resp ) ) {
        return rmbz_api_error( 'Falha ao procurar a capa.', 502, 'fetch_error' );
    }
    $data = json_decode( wp_remote_retrieve_body( $resp ), true );
    $art  = $data['results'][0]['artworkUrl100'] ?? '';
    if ( $art === '' ) {
        return rmbz_api_error( 'Capa em HD não encontrada.', 404, 'not_found' );
    }
    // iTunes serve 100x100 por defeito; pedimos 600x600 (mesma imagem, mais nítida).
    $hd = str_replace( '100x100bb', '600x600bb', $art );
    return rmbz_api_ok( [ 'url' => $hd ] );
}
