<?php
/**
 * 1MZ For Artists API — Ferramentas de IA geradoras (Fase 2b, só VIP)
 *
 *   POST /me/ai/caption  — gera legenda + hashtags para um release
 *   POST /me/ai/ask      — responde a uma pergunta de carreira usando os
 *                          números reais do artista como contexto
 *
 * Regras transversais:
 *   - Só VIP (insights são para todos; geração é benefício VIP)
 *   - Requer IA configurada (chave Anthropic)
 *   - Rate-limit por artista (controlo de custo/abuso)
 *   - Nunca inventa métricas — alimentamos os factos reais
 *
 * @package 1MZ_For_Artists
 * @since 1.5.283
 */

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

if ( ! function_exists( 'rmbz_ai_messages' ) ) {
    require_once dirname( __FILE__, 2 ) . '/ai/ai-client.php';
}
if ( ! function_exists( 'rmbz_insights_compute' ) ) {
    require_once dirname( __FILE__, 2 ) . '/ai/insights-engine.php';
}

add_action( 'rest_api_init', 'rmbz_api_register_ai_tools_routes' );

function rmbz_api_register_ai_tools_routes() {
    register_rest_route( RMBZ_API_NAMESPACE, '/me/ai/caption', [
        'methods'             => 'POST',
        'callback'            => 'rmbz_api_handle_ai_caption',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'release_id' => [ 'type' => 'integer', 'required' => true ],
            'tone'       => [ 'type' => 'string', 'default' => 'animado' ],
        ],
    ] );

    register_rest_route( RMBZ_API_NAMESPACE, '/me/ai/ask', [
        'methods'             => 'POST',
        'callback'            => 'rmbz_api_handle_ai_ask',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'question' => [ 'type' => 'string', 'required' => true ],
            'history'  => [ 'type' => 'array',  'default' => [] ],
        ],
    ] );

    // [1.5.285] Plano de Lançamento — plano de promoção para um release (VIP).
    register_rest_route( RMBZ_API_NAMESPACE, '/me/ai/playbook', [
        'methods'             => 'POST',
        'callback'            => 'rmbz_api_handle_ai_playbook',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'release_id' => [ 'type' => 'integer', 'required' => true ],
        ],
    ] );

    // [1.5.286] Kit de divulgação — bio + pitch + ideias de posts (VIP).
    register_rest_route( RMBZ_API_NAMESPACE, '/me/ai/promo-kit', [
        'methods'             => 'POST',
        'callback'            => 'rmbz_api_handle_ai_promo_kit',
        'permission_callback' => 'rmbz_api_require_auth',
    ] );

    // [1.5.296] Conceito de videoclipe — ajuda na candidatura "Ganhar Vídeo" (VIP).
    register_rest_route( RMBZ_API_NAMESPACE, '/me/ai/video-concept', [
        'methods'             => 'POST',
        'callback'            => 'rmbz_api_handle_ai_video_concept',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'song_title' => [ 'type' => 'string', 'default' => '' ],
            'about'      => [ 'type' => 'string', 'default' => '' ],
            'idea'       => [ 'type' => 'string', 'default' => '' ],
            'scenario'   => [ 'type' => 'string', 'default' => '' ],
        ],
    ] );

    // [1.6.302] Detalhes com IA — conversa de artista para artista sobre UM
    // relatório, podendo comparar com o histórico de toda a vida. Add-on de IA.
    register_rest_route( RMBZ_API_NAMESPACE, '/me/ai/report-chat', [
        'methods'             => 'POST',
        'callback'            => 'rmbz_api_handle_ai_report_chat',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'import_id' => [ 'type' => 'integer', 'default' => 0 ],
            'question'  => [ 'type' => 'string',  'default' => '' ],
            'history'   => [ 'type' => 'array',   'default' => [] ],
        ],
    ] );

    // [1.6.301] Recomendação genérica das listas (submissões site/redes,
    // Ganhar Vídeo, etc.). Para TODOS os artistas (não é VIP) — é
    // informação/engagement. A app envia os FACTOS reais (já formatados) + a
    // SITUAÇÃO a transmitir; o servidor só reescreve em bom pt-PT. Só responde
    // com IA quando o interruptor "Recomendações com IA" está ligado; caso
    // contrário { enabled:false } e a app usa o texto pré-definido dela.
    register_rest_route( RMBZ_API_NAMESPACE, '/me/ai/list-tip', [
        'methods'             => 'POST',
        'callback'            => 'rmbz_api_handle_ai_list_tip',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'domain'    => [ 'type' => 'string', 'default' => 'list' ],
            'kind'      => [ 'type' => 'string', 'default' => '' ],
            'noun'      => [ 'type' => 'string', 'default' => '' ],
            'title'     => [ 'type' => 'string', 'default' => '' ],
            'facts'     => [ 'type' => 'string', 'default' => '' ],
            'situation' => [ 'type' => 'string', 'default' => '' ],
        ],
    ] );

    // [1.6.333] Explicar uma notificação do banner inteligente da Home. O artista
    // abre a notificação em modal completa; se tiver o add-on de IA, a IA explica
    // PORQUÊ recebeu a notificação e, se for um problema, COMO resolver (passo a
    // passo), usando o contexto real do artista. Só para VIP com IA (guard).
    register_rest_route( RMBZ_API_NAMESPACE, '/me/ai/explain-notification', [
        'methods'             => 'POST',
        'callback'            => 'rmbz_api_handle_ai_explain_notification',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'type'  => [ 'type' => 'string', 'default' => '' ],
            'label' => [ 'type' => 'string', 'default' => '' ],
        ],
    ] );

    // [1.6.337] Explicar ao artista uma LIMITAÇÃO/banimento ativo. A IA lê o
    // motivo interno (confidencial) + o motivo mostrado e explica com tato,
    // sem revelar texto sensível, dizendo o que pode fazer. Só VIP com IA.
    register_rest_route( RMBZ_API_NAMESPACE, '/me/ai/explain-restriction', [
        'methods'             => 'POST',
        'callback'            => 'rmbz_api_handle_ai_explain_restriction',
        'permission_callback' => 'rmbz_api_require_auth',
        'args' => [
            'key' => [ 'type' => 'string', 'default' => '' ],
        ],
    ] );

    // [1.6.381] Alertar/explicar a MULTA por atraso do videoclipe (produção
    // parada sem VIP, ou dívida pendente depois de renovar). Só VIP com IA.
    register_rest_route( RMBZ_API_NAMESPACE, '/me/ai/explain-fine', [
        'methods'             => 'POST',
        'callback'            => 'rmbz_api_handle_ai_explain_fine',
        'permission_callback' => 'rmbz_api_require_auth',
    ] );
}

/**
 * [1.6.333] Descrição humana (pt-PT) de cada tipo de notificação do banner, para
 * dar contexto à IA sobre o que a notificação significa no fluxo da 1MZ.
 */
function rmbz_ai_notification_type_desc( $type ) {
    $map = [
        'SUBMISSION_CORRECTION'    => 'Uma submissão (Site/Redes) precisa de correção antes de avançar.',
        'SUBMISSION_REJECTED'      => 'Uma submissão (Site/Redes) foi rejeitada pela equipa.',
        'SUBMISSION_APPROVED'      => 'Uma submissão (Site/Redes) foi aprovada.',
        'SUBMISSION_PUBLISHED'     => 'Uma submissão (Site/Redes) já foi publicada.',
        'PROJECT_CONTRACT'         => 'Há um contrato/orçamento de videoclipe à espera de aceitação.',
        'PROJECT_KYC'              => 'Falta enviar a verificação de identidade (KYC) do projeto de vídeo.',
        'PROJECT_PAYMENT'          => 'Há um pagamento em falta num projeto de vídeo.',
        'PROJECT_CHAT_MESSAGE'     => 'O admin/equipa enviou uma mensagem nova no chat do projeto.',
        'PROJECT_PENDING_DECISIONS'=> 'Há itens adicionados pela equipa à espera de uma decisão do artista.',
        'PROJECT_PITCH_UPDATED'    => 'A equipa atualizou o pitch/plano de produção do videoclipe.',
        'PROJECT_VIDEO_READY'      => 'O videoclipe final está pronto para ver.',
        'PROJECT_APPROVED'         => 'A candidatura ao videoclipe foi aprovada.',
        'PROJECT_FILMING_DATES'    => 'A equipa definiu datas de gravação que o artista precisa de confirmar.',
        'PROJECT_CANCELLED'        => 'Um projeto de vídeo foi cancelado por falta de confirmação.',
        'RELEASE_RETURNED'         => 'Um lançamento (distribuição) foi devolvido para correção.',
        'RELEASE_REJECTED'         => 'Um lançamento (distribuição) foi rejeitado.',
        'WITHDRAWAL_APPROVED'      => 'Um pedido de levantamento de dinheiro foi aprovado.',
        'WITHDRAWAL_REJECTED'      => 'Um pedido de levantamento foi rejeitado (o saldo voltou à carteira).',
        'VIP_EXPIRING'             => 'A subscrição VIP está a expirar.',
        'VIDEO_NEEDS_SUBSCRIPTION' => 'Há um videoclipe em produção que pode parar porque a subscrição expirou/está a expirar.',
        'EARNINGS_UPDATED'         => 'Os ganhos/relatórios do artista foram atualizados.',
    ];
    return $map[ $type ] ?? '';
}

/* ----------------------------------------------------------------------- */
/* GUARDAS COMUNS                                                          */
/* ----------------------------------------------------------------------- */

/**
 * Valida VIP + IA disponível + rate-limit. Devolve WP_REST_Response de erro
 * (para o caller fazer `return`) ou null se está tudo OK.
 */
function rmbz_ai_tools_guard( $telemovel, $bucket, $max_per_day ) {
    if ( ! rmbz_ai_user_is_vip( $telemovel ) ) {
        // [1.6.301] Distinguir os dois casos para a mensagem não enganar:
        //   • VIP ATIVO mas SEM o add-on de IA → pedir para renovar com a IA.
        //   • Sem subscrição → upsell normal.
        $has_sub = function_exists( 'rmbz_user_has_active_subscription' )
            && rmbz_user_has_active_subscription( $telemovel );
        if ( $has_sub ) {
            return rmbz_api_error(
                'A tua subscrição não inclui o Assistente de IA. Compra ou renova a '
                . 'subscrição e seleciona a opção de IA para usares esta ferramenta.',
                403, 'ai_addon_required'
            );
        }
        return rmbz_api_error(
            'Esta ferramenta precisa de uma subscrição com o Assistente de IA. '
            . 'Compra uma subscrição e seleciona a opção de IA.',
            403, 'vip_required'
        );
    }
    if ( ! rmbz_ai_available() ) {
        return rmbz_api_error(
            'O assistente de IA ainda não está disponível. Tenta mais tarde.',
            503, 'ai_unavailable'
        );
    }
    $rl = rmbz_api_rate_limit( $bucket . $telemovel, $max_per_day, DAY_IN_SECONDS );
    if ( empty( $rl['allowed'] ) ) {
        return rmbz_api_error(
            'Atingiste o limite diário desta ferramenta. Volta amanhã.',
            429, 'rate_limited'
        );
    }
    return null;
}

/**
 * [1.6.315] Mensagem AMIGÁVEL para o artista quando a IA falha. A causa técnica
 * real (modelo inválido, chave, saldo, sobrecarga) é REGISTADA no log do
 * servidor para o admin/dev diagnosticar — NÃO se mostra ao artista (não é
 * problema dele e só confunde).
 */
function rmbz_ai_err_hint( $res ) {
    $e = is_array( $res ) && ! empty( $res['error'] ) ? (string) $res['error'] : 'erro desconhecido';

    // Diagnóstico só para nós (vai para o log de erros do WordPress/servidor).
    error_log( '[1MZ AI] falha do assistente: ' . $e );

    $low = mb_strtolower( $e );
    // Transitório (sobrecarga/limite/rede) → vale a pena tentar já.
    if ( strpos( $low, 'overloaded' ) !== false || strpos( $low, '529' ) !== false
      || strpos( $low, 'rate' ) !== false || strpos( $low, '429' ) !== false
      || strpos( $low, 'timeout' ) !== false || strpos( $low, 'timed out' ) !== false ) {
        return 'Tenta novamente daqui a pouco.';
    }
    // Tudo o resto (config nossa) → mensagem neutra; nós tratamos pelo log.
    return 'Tenta novamente mais tarde.';
}

/**
 * [1.6.304] Prefixo "Perfil do Artista" para injetar em qualquer prompt de IA,
 * para o assistente CONHECER cada artista (objetivo, fase, mercado, números).
 * Vazio se o perfil não estiver disponível.
 */
function rmbz_ai_profile_prefix( $telemovel ) {
    if ( ! function_exists( 'rmbz_ai_artist_profile' ) ) {
        require_once dirname( __FILE__, 2 ) . '/ai/insights-engine.php';
    }
    if ( ! function_exists( 'rmbz_ai_artist_profile' ) ) return '';
    $p = rmbz_ai_artist_profile( $telemovel );
    return $p !== '' ? $p . "\n\n" : '';
}

/**
 * [1.6.317] Catálogo completo do artista (lançamentos + submissões + vídeos)
 * para a IA conhecer TUDO do utilizador, não só os relatórios de streams.
 */
function rmbz_ai_catalog_prefix( $telemovel ) {
    if ( ! function_exists( 'rmbz_ai_artist_catalog' ) ) {
        require_once dirname( __FILE__, 2 ) . '/ai/insights-engine.php';
    }
    if ( ! function_exists( 'rmbz_ai_artist_catalog' ) ) return '';
    return rmbz_ai_artist_catalog( $telemovel );
}

/**
 * [1.6.319] "Novidades / o que se passou" (notificações + avisos do admin +
 * estado dos serviços) para o assistente responder a "o que perdi offline?".
 */
function rmbz_ai_activity_prefix( $telemovel ) {
    if ( ! function_exists( 'rmbz_ai_artist_activity' ) ) {
        require_once dirname( __FILE__, 2 ) . '/ai/insights-engine.php';
    }
    if ( ! function_exists( 'rmbz_ai_artist_activity' ) ) return '';
    return rmbz_ai_artist_activity( $telemovel );
}

/**
 * [1.6.306] Memória da conversa — formata os turnos anteriores (vindos da app)
 * num bloco de contexto, para a IA manter continuidade. Limita aos últimos N.
 */
function rmbz_ai_format_history( $history, $max = 6 ) {
    if ( ! is_array( $history ) || empty( $history ) ) return '';
    $history = array_slice( $history, -$max );
    $out = [];
    foreach ( $history as $turn ) {
        if ( ! is_array( $turn ) ) continue;
        $q = isset( $turn['question'] ) ? trim( wp_strip_all_tags( (string) $turn['question'] ) ) : '';
        $a = isset( $turn['answer'] )   ? trim( wp_strip_all_tags( (string) $turn['answer'] ) )   : '';
        // Limita o tamanho de cada turno para o prompt não explodir.
        if ( function_exists( 'mb_substr' ) ) {
            if ( mb_strlen( $q ) > 300 )  $q = mb_substr( $q, 0, 300 );
            if ( mb_strlen( $a ) > 700 )  $a = mb_substr( $a, 0, 700 );
        }
        if ( $q !== '' ) $out[] = 'Artista: ' . $q;
        if ( $a !== '' ) $out[] = 'Tu (assistente): ' . $a;
    }
    if ( empty( $out ) ) return '';
    return "CONVERSA ATÉ AGORA (lembra-te disto e mantém a continuidade — não repitas o que já disseste):\n"
        . implode( "\n", $out ) . "\n\n";
}

/**
 * [1.6.311] Base de conhecimento do app — visão geral das secções + FAQ real,
 * para o assistente poder responder a perguntas sobre COMO USAR o app e as
 * suas configurações (não só sobre carreira/números).
 */
function rmbz_ai_app_knowledge() {
    $overview =
        "BASE DE CONHECIMENTO DO APP 1MZ FOR ARTISTS (usa para responder a perguntas sobre como usar o app e as configurações):\n"
      . "- Início: resumo, dica do dia (Coach), atalhos e avisos.\n"
      . "- Carteira: saldo, DEPÓSITOS e LEVANTAMENTOS via M-Pesa/e-Mola, extrato; royalties em USD convertem para MT à taxa atual; precisa de PIN para levantar.\n"
      . "- Releases: lançar música, ver estado (rascunho/em análise/live), editar faixas enquanto em rascunho, pedir alteração de lojas, configurar splits de royalties por faixa.\n"
      . "- Estatísticas: relatórios da distribuidora por streams e ganhos (USD), top plataformas/países/faixas, gráficos, e 'Detalhes com IA'.\n"
      . "- Submissões: publicar no site 1MusicMoz e divulgar nas redes sociais oficiais da 1MZ.\n"
      . "- Ganhar Vídeo: candidatar uma música à produção de videoclipe (níveis Básico/Profissional/Cinema); se aprovado, recebe orçamento e a 1MZ produz. IMPORTANTE: enquanto o videoclipe está EM PRODUÇÃO, o artista TEM de manter a subscrição VIP ativa — a equipa não está autorizada a trabalhar sem ela; se a subscrição expirar com o projeto em curso, a produção pára e começa a contar uma MULTA por atraso (ver regra abaixo) até o artista renovar.\n"
      . "- Assistente IA: Insights, Coach diário, Detalhes com IA, Perguntar, Gerar legenda, Kit de divulgação, Plano de lançamento, Conceito de vídeo (exige o add-on de IA).\n"
      . "- Subscrição/VIP: planos e add-on de IA; comprar/renovar e escolher a opção de IA no checkout.\n"
      . "- Licenciamento SOMAS: registo de direitos de execução pública em Moçambique (a 1MZ trata por ti).\n"
      . "- Convites: indicar amigos e ganhar comissões.\n"
      . "- Perfil/Definições: dados pessoais, PIN de segurança, biometria, tema, notificações e contactos de suporte.\n";

    // [1.6.351] Regra da MULTA por atraso (videoclipe em produção sem subscrição),
    // com o valor/unidade REAIS configurados no admin — para a IA saber explicar.
    $overview .= "\nREGRA DA MULTA POR ATRASO (Ganhar Vídeo) — fonte de verdade:\n";
    if ( function_exists( 'rmbz_project_fine_value' ) && function_exists( 'rmbz_project_fine_unit' ) ) {
        $fval  = (float) rmbz_project_fine_value();
        $funit = rmbz_project_fine_unit() === 'hour' ? 'hora' : 'dia';
        if ( $fval > 0 ) {
            $overview .= sprintf(
                "- Se a subscrição VIP expirar enquanto há um videoclipe EM PRODUÇÃO, o projeto acumula automaticamente uma multa de %s MT por %s, contada desde o momento em que a subscrição expirou, até o artista renovar. A multa é informativa (não é debitada sozinha da carteira); renovar a subscrição PÁRA a contagem e a equipa retoma o trabalho. O valor/unidade são definidos pela administração (pode mudar).\n",
                number_format( $fval, 2, ',', '.' ), $funit
            );
        } else {
            $overview .= "- Regra: se a subscrição VIP expirar com um videoclipe EM PRODUÇÃO, a equipa pára e o sistema pode acumular uma multa por atraso (por dia ou por hora). De momento a administração tem a multa DESLIGADA (valor 0), mas a produção continua bloqueada até o artista renovar a subscrição.\n";
        }
    } else {
        $overview .= "- Se a subscrição VIP expirar com um videoclipe EM PRODUÇÃO, a equipa não trabalha e o projeto pode acumular uma multa por atraso (por dia ou hora, definida pela administração) até o artista renovar.\n";
    }
    $overview .= "- NUNCA inventes o valor da multa; usa o valor acima. Se o artista perguntar, lembra que renovar a subscrição resolve (pára a multa e retoma a produção).\n";

    // [1.6.352] Fluxo/regras do projeto de videoclipe (Ganhar Vídeo).
    $overview .=
        "\nREGRAS DO GANHAR VÍDEO (fluxo do projeto de videoclipe):\n"
      . "- Candidatura: o artista submete uma música a um nível (Básico/Profissional/Cinema). A comissão de curadoria da 1MZ avalia (pode aprovar, pedir correção ou rejeitar).\n"
      . "- Se aprovado: a 1MZ envia um ORÇAMENTO/contrato. O artista tem cerca de 24h para ACEITAR; se não aceitar a tempo, o projeto é cancelado automaticamente (pode candidatar-se de novo).\n"
      . "- Fases: Em Avaliação → Aprovado → KYC → Filmagem → Edição → Concluído. 'Em Avaliação' (antes da aprovação) NÃO gera multa.\n"
      . "- A partir de APROVADO (Aprovado, KYC, Filmagem, Edição) a equipa já está comprometida e a subscrição VIP TEM de estar ativa. Se expirar nestas fases, a produção fica bloqueada e acumula multa por atraso (ver acima); essa multa fica como dívida pendente mesmo que o artista renove — só sai quando for paga.\n"
      . "- Comunicação: a equipa fala com o artista pelo CHAT do projeto e por EMAIL — por isso é importante ter um email real configurado na conta.\n";

    // [1.6.352] Regras importantes da plataforma (com valores REAIS da config).
    $dep_min   = (int) get_option( 'rmbz_first_deposit_min', 500 );
    $sub_price = (string) get_option( 'rmbz_sub_price', '1500' );
    $sub_days  = (int) get_option( 'rmbz_sub_days', 30 );
    $ref_type  = get_option( 'rmbz_referral_type', 'mt' );
    $ref_rate  = (float) get_option( 'rmbz_referral_rate', 3.0 );
    $ref_days  = (int) get_option( 'rmbz_referral_days', 5 );
    $ref_rule  = ( $ref_type === 'days' )
        ? sprintf( 'quem convida ganha %d dias de VIP grátis por cada amigo que se regista e deposita', $ref_days )
        : sprintf( 'quem convida ganha %s%% do valor que o amigo depositar (creditado em MT na carteira)', number_format( $ref_rate, 2, ',', '.' ) );

    $overview .=
        "\nREGRAS IMPORTANTES DA PLATAFORMA (factos oficiais — usa-os e NÃO inventes):\n"
      . sprintf( "- Conta nova: para ATIVAR a conta, o artista faz um primeiro depósito de pelo menos %d MT. Se não confirmar o pagamento dentro de ~24h, a conta pendente é removida automaticamente. Esse valor fica na carteira e serve para comprar a subscrição.\n", $dep_min )
      . sprintf( "- Subscrição VIP: cerca de %s MT por %d dias (a administração pode ter vários pacotes/preços). Dá 100%% dos royalties e prioridade na revisão. O Assistente de IA é um ADD-ON da subscrição (escolhe-se no checkout).\n", $sub_price, $sub_days )
      . "- Carteira/Royalties: os royalties vêm em USD e convertem para MT à taxa atual definida pela 1MZ; LEVANTAR exige o PIN de segurança; depósitos e levantamentos são via M-Pesa/e-Mola.\n"
      . sprintf( "- Convites: %s. Um número pode ser banido de receber comissões (anti-fraude).\n", $ref_rule )
      . "- Distribuição digital: lançar música nas lojas exige a permissão de DISTRIBUIÇÃO no plano e a subscrição ativa.\n"
      . "- Ganhar Vídeo: candidata-se uma música SEM videoclipe (a 1MZ é que vai produzir um). A regra 'a música não pode já ter videoclipe' aplica-se SÓ ao Ganhar Vídeo, NÃO ao Publicar no Site.\n"
      . "- Castigos/limitações: a administração pode bloquear serviços (levantamentos, depósitos, distribuição, vídeo, promoção) ou a conta inteira, de forma TEMPORÁRIA (com data de desbloqueio e cronómetro) ou PERMANENTE, sempre com um motivo mostrado ao artista. Enquanto bloqueado, o serviço fica indisponível até desbloquear/expirar.\n"
      . "- Honestidade: nunca prometas resultados nem inventes números, prazos ou regras; se não souberes, encaminha para o suporte.\n";

    if ( ! function_exists( 'rmbz_support_faq_items' ) ) {
        $sc = dirname( __FILE__ ) . '/support-controller.php';
        if ( file_exists( $sc ) ) require_once $sc;
    }
    $faq = '';
    if ( function_exists( 'rmbz_support_faq_items' ) ) {
        $faq = "\nPERGUNTAS FREQUENTES (FAQ) — respostas OFICIAIS, usa-as como fonte de verdade:\n";
        foreach ( rmbz_support_faq_items() as $f ) {
            $faq .= '• P: ' . $f['question'] . "\n  R: " . wp_strip_all_tags( $f['answer'] ) . "\n";
        }
    }
    return $overview . $faq;
}

/* ----------------------------------------------------------------------- */
/* POST /me/ai/caption                                                     */
/* ----------------------------------------------------------------------- */

function rmbz_api_handle_ai_caption( WP_REST_Request $request ) {
    $telemovel  = (string) $request->get_param( 'rmbz_telemovel' );
    $release_id = (int)    $request->get_param( 'release_id' );
    $tone       = sanitize_text_field( (string) $request->get_param( 'tone' ) );

    if ( $guard = rmbz_ai_tools_guard( $telemovel, 'ai_caption_', 15 ) ) {
        return $guard;
    }

    global $wpdb;
    $t_rel    = $wpdb->prefix . 'rmbz_distro_releases';
    $t_tracks = $wpdb->prefix . 'rmbz_distro_tracks';

    $release = $wpdb->get_row( $wpdb->prepare(
        "SELECT id, title, status FROM $t_rel
         WHERE id = %d AND artist_telemovel = %s",
        $release_id, $telemovel
    ) );
    if ( ! $release ) {
        return rmbz_api_error( 'Lançamento não encontrado.', 404, 'not_found' );
    }

    $n_tracks = (int) $wpdb->get_var( $wpdb->prepare(
        "SELECT COUNT(*) FROM $t_tracks WHERE release_id = %d", $release_id
    ) );

    $artist_name = '';
    if ( function_exists( 'rmbz_get_artist_nome_by_telemovel' ) ) {
        $artist_name = (string) rmbz_get_artist_nome_by_telemovel( $telemovel );
    }
    if ( $artist_name === '' ) {
        $artist_name = (string) $wpdb->get_var( $wpdb->prepare(
            "SELECT user_nome FROM {$wpdb->prefix}rmbz_wallets WHERE user_telemovel = %s",
            $telemovel
        ) );
    }

    $tones = [
        'animado'       => 'energético e entusiasmado',
        'profissional'  => 'profissional e elegante',
        'curto'         => 'muito curto e directo',
        'poetico'       => 'poético e emotivo',
        'agradecimento' => 'de gratidão aos fãs',
    ];
    $tone_desc = $tones[ $tone ] ?? 'autêntico e cativante';

    $is_live = in_array( $release->status, [ 'live', 'distribuido', 'delivered' ], true );

    $facts = [
        'titulo'     => (string) $release->title,
        'artista'    => $artist_name ?: 'o artista',
        'tipo'       => $n_tracks > 1 ? 'álbum/EP' : 'single',
        'n_faixas'   => $n_tracks,
        'ja_nas_lojas' => $is_live,
    ];

    $system = "És gestor de redes sociais de artistas da 1MZ Records, uma editora "
        . "moçambicana. Escreves legendas autênticas e cativantes em português de "
        . "Portugal (pt-PT) para Instagram/Facebook de músicos. Evita clichés e "
        . "exageros. Responde SEMPRE só com JSON válido, sem texto à volta.";

    $instruction = rmbz_ai_profile_prefix( $telemovel )
        . "Cria uma legenda para divulgar este lançamento.\n\n"
        . "DADOS:\n" . wp_json_encode( $facts, JSON_UNESCAPED_UNICODE ) . "\n\n"
        . "TOM: " . $tone_desc . "\n\n"
        . "Regras: 1 a 3 frases curtas + 1 chamada à ação; no máximo 2 emojis; "
        . "se ainda não está nas lojas, fala em 'a chegar' em vez de 'já disponível'. "
        . "Inclui 6 a 10 hashtags relevantes (sem repetir, mistura português e o nicho musical).\n\n"
        . "Devolve EXACTAMENTE este JSON:\n"
        . "{ \"caption\": \"texto da legenda\", \"hashtags\": [\"semHash1\", \"semHash2\"] }\n"
        . "As hashtags vão SEM o símbolo #.";

    $res = rmbz_ai_messages( $system, $instruction, [ 'max_tokens' => 500, 'temperature' => 0.8 ] );
    if ( empty( $res['ok'] ) ) {
        return rmbz_api_error( 'Não foi possível gerar a legenda agora. ' . rmbz_ai_err_hint( $res ), 502, 'ai_error' );
    }

    $data = rmbz_ai_extract_json( $res['text'] );
    if ( ! is_array( $data ) || empty( $data['caption'] ) ) {
        return rmbz_api_error( 'A IA devolveu um formato inesperado. Tenta de novo.', 502, 'ai_format' );
    }

    $hashtags = [];
    if ( ! empty( $data['hashtags'] ) && is_array( $data['hashtags'] ) ) {
        foreach ( $data['hashtags'] as $h ) {
            $h = ltrim( trim( (string) $h ), '#' );
            if ( $h !== '' ) $hashtags[] = $h;
        }
    }

    return rmbz_api_ok( [
        'caption'  => (string) $data['caption'],
        'hashtags' => array_slice( $hashtags, 0, 12 ),
    ] );
}

/* ----------------------------------------------------------------------- */
/* POST /me/ai/ask                                                         */
/* ----------------------------------------------------------------------- */

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

    if ( $question === '' ) {
        return rmbz_api_error( 'Escreve uma pergunta.', 400, 'empty_question' );
    }
    if ( mb_strlen( $question ) > 400 ) {
        $question = mb_substr( $question, 0, 400 );
    }

    if ( $guard = rmbz_ai_tools_guard( $telemovel, 'ai_ask_', 30 ) ) {
        return $guard;
    }

    // Contexto: factos reais (lifetime) — para a IA não inventar números.
    $facts = rmbz_insights_compute( $telemovel, 'all' );
    $ctx = [
        'streams_total'  => (int) $facts['streams']['current'],
        'ganhos_usd'     => (float) $facts['earnings']['current'],
        'maior_plataforma' => $facts['top_platform'] ? [
            'nome' => $facts['top_platform']['store'],
            'pct'  => $facts['top_platform']['percentage'],
        ] : null,
        'pais_top' => $facts['top_country'] ? [
            'nome' => $facts['top_country']['name'],
            'pct'  => $facts['top_country']['percentage'],
        ] : null,
        'faixa_top' => $facts['top_track'] ? [
            'titulo'  => $facts['top_track']['title'],
            'streams' => (int) $facts['top_track']['streams'],
        ] : null,
        // Receita por plataforma — para raciocinar sobre QUEM PAGA, não só views.
        'plataformas_por_receita' => array_map( function ( $p ) {
            return [
                'nome'       => $p['store'],
                'streams'    => (int) $p['streams'],
                'ganhos_usd' => (float) $p['earnings_usd'],
            ];
        }, isset( $facts['platforms'] ) ? $facts['platforms'] : [] ),
    ];

    $system = "És o assistente da 1MZ Records, uma editora moçambicana. Ajudas o artista em DUAS coisas: "
        . "(A) a carreira musical (números, estratégia) e (B) a USAR O APP 1MZ — configurações, como fazer "
        . "depósitos/levantamentos, lançar música, splits, SOMAS, VIP, etc. Falas português de Portugal "
        . "(pt-PT), directo e prático. CONHECES TUDO do artista: o CATÁLOGO E ATIVIDADE (lançamentos, "
        . "submissões, vídeos) e as NOVIDADES (notificações, avisos/mensagens gerais do admin, estado dos "
        . "serviços) fornecidos. Usa-os para responder a 'quais os meus lançamentos?', 'em que pé está a "
        . "submissão?', e em especial a 'o que perdi enquanto estive offline?' — nesse caso resume o que é "
        . "relevante das NOVIDADES (mensagens do admin, serviços ativados/desativados, ações pendentes). "
        . "PARA PERGUNTAS SOBRE O APP: usa a BASE DE CONHECIMENTO e o FAQ fornecidos como fonte de verdade; "
        . "responde com passos claros e simples (onde tocar). Se a resposta não estiver no conhecimento dado, "
        . "diz que não tens a certeza e sugere o suporte — NÃO inventes funcionalidades nem passos. "
        . "PARA PERGUNTAS DE CARREIRA: para ganhar DINHEIRO conta a RECEITA (USD) por plataforma, NÃO o volume "
        . "de views — uma plataforma com menos views mas mais USD é melhor aposta (vê 'plataformas_por_receita'); "
        . "ADAPTA-TE se o artista trouxer um facto novo e MUDA a recomendação (não repitas posições padronizadas). "
        . "REGRAS: nunca inventes métricas nem passos; só redireciona se a pergunta não tiver NADA a ver com "
        . "música nem com o app; responde curto (máx ~150 palavras).";

    // Memória PERSISTENTE do servidor (lembra-se do artista ao longo do tempo).
    $server_mem = function_exists( 'rmbz_ai_get_chat_memory' )
        ? rmbz_ai_get_chat_memory( $telemovel ) : [];
    $mem_turns = array_map( function ( $m ) {
        return [ 'question' => isset( $m['q'] ) ? $m['q'] : '', 'answer' => isset( $m['a'] ) ? $m['a'] : '' ];
    }, $server_mem );

    $instruction = rmbz_ai_profile_prefix( $telemovel )
        . rmbz_ai_catalog_prefix( $telemovel )
        . rmbz_ai_activity_prefix( $telemovel )
        . rmbz_ai_app_knowledge() . "\n"
        . rmbz_ai_format_history( $mem_turns, 10 )
        . "NÚMEROS REAIS DO ARTISTA (para perguntas de carreira/estatísticas):\n"
        . wp_json_encode( $ctx, JSON_UNESCAPED_UNICODE ) . "\n\n"
        . "NOVA PERGUNTA DO ARTISTA:\n" . $question . "\n\n"
        . "Responde dando continuidade à conversa. Se for sobre o APP/configurações, usa a base de "
        . "conhecimento e o FAQ e dá passos claros; se for sobre a carreira, usa os números reais. "
        . "Quando fizer sentido, recomenda PROACTIVAMENTE uma ação concreta a fazer no app (ex.: criar "
        . "um lançamento, configurar splits, candidatar ao Ganhar Vídeo). Nunca inventes.";

    $res = rmbz_ai_messages( $system, $instruction, [
        'model'       => rmbz_ai_chat_model(),
        'max_tokens'  => 600,
        'temperature' => 0.7,
    ] );
    if ( empty( $res['ok'] ) ) {
        return rmbz_api_error( 'Não foi possível responder agora. ' . rmbz_ai_err_hint( $res ), 502, 'ai_error' );
    }

    $answer = trim( $res['text'] );

    // Persiste o turno na memória do servidor (capada a RMBZ_AI_MEMORY_MAX).
    if ( function_exists( 'rmbz_ai_append_chat_memory' ) ) {
        rmbz_ai_append_chat_memory( $telemovel, $question, $answer );
    }

    return rmbz_api_ok( [
        'question' => $question,
        'answer'   => $answer,
    ] );
}

/* ----------------------------------------------------------------------- */
/* POST /me/ai/playbook — Plano de Lançamento                              */
/* ----------------------------------------------------------------------- */

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

    if ( $guard = rmbz_ai_tools_guard( $telemovel, 'ai_playbook_', 20 ) ) {
        return $guard;
    }

    global $wpdb;
    $t_rel = $wpdb->prefix . 'rmbz_distro_releases';
    $release = $wpdb->get_row( $wpdb->prepare(
        "SELECT id, title, status FROM $t_rel WHERE id = %d AND artist_telemovel = %s",
        $release_id, $telemovel
    ) );
    if ( ! $release ) {
        return rmbz_api_error( 'Lançamento não encontrado.', 404, 'not_found' );
    }

    $artist_name = function_exists( 'rmbz_get_artist_nome_by_telemovel' )
        ? (string) rmbz_get_artist_nome_by_telemovel( $telemovel ) : '';

    // Factos reais (lifetime) — para o plano focar onde o artista já cresce.
    $facts = rmbz_insights_compute( $telemovel, 'all' );
    $ctx = [
        'titulo'           => (string) $release->title,
        'artista'          => $artist_name ?: 'o artista',
        'ja_nas_lojas'     => in_array( $release->status, [ 'live', 'distribuido', 'delivered' ], true ),
        'maior_plataforma' => $facts['top_platform']['store'] ?? null,
        'pais_top'         => $facts['top_country']['name'] ?? null,
        'faixa_top'        => $facts['top_track']['title'] ?? null,
    ];

    $system = "És estratega de promoção musical da 1MZ Records (Moçambique). "
        . "Crias planos de divulgação realistas para artistas independentes em "
        . "Moçambique — recursos limitados, foco em redes sociais, WhatsApp, "
        . "playlists e rádio local. Falas pt-PT, directo e accionável. Usa apenas "
        . "os dados fornecidos, nunca inventes números. Responde só com JSON válido.";

    $instruction = rmbz_ai_profile_prefix( $telemovel )
        . "Cria um plano de lançamento para promover esta música.\n\n"
        . "DADOS:\n" . wp_json_encode( $ctx, JSON_UNESCAPED_UNICODE ) . "\n\n"
        . "Se houver 'maior_plataforma' ou 'pais_top', usa-os para focar o plano. "
        . "Devolve EXACTAMENTE este JSON:\n"
        . "{ \"intro\": \"1 frase de enquadramento (máx 140 caracteres)\", "
        . "\"steps\": [ { \"icon\": \"emoji\", \"title\": \"passo curto\", "
        . "\"detail\": \"o que fazer, máx 140 caracteres\" } ] }\n"
        . "Entre 4 e 6 passos, ordenados do mais importante para o menos.";

    $res = rmbz_ai_messages( $system, $instruction, [ 'max_tokens' => 900, 'temperature' => 0.7 ] );
    if ( empty( $res['ok'] ) ) {
        return rmbz_api_error( 'Não foi possível gerar o plano agora. ' . rmbz_ai_err_hint( $res ), 502, 'ai_error' );
    }

    $data = rmbz_ai_extract_json( $res['text'] );
    if ( ! is_array( $data ) || empty( $data['steps'] ) || ! is_array( $data['steps'] ) ) {
        return rmbz_api_error( 'A IA devolveu um formato inesperado. Tenta de novo.', 502, 'ai_format' );
    }

    $steps = [];
    foreach ( $data['steps'] as $s ) {
        if ( ! is_array( $s ) || empty( $s['title'] ) ) continue;
        $steps[] = [
            'icon'   => isset( $s['icon'] ) ? (string) $s['icon'] : '✅',
            'title'  => (string) $s['title'],
            'detail' => isset( $s['detail'] ) ? (string) $s['detail'] : '',
        ];
    }

    return rmbz_api_ok( [
        'release_title' => (string) $release->title,
        'intro'         => isset( $data['intro'] ) ? (string) $data['intro'] : '',
        'steps'         => array_slice( $steps, 0, 8 ),
    ] );
}

/* ----------------------------------------------------------------------- */
/* POST /me/ai/promo-kit — Kit de divulgação                              */
/* ----------------------------------------------------------------------- */

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

    if ( $guard = rmbz_ai_tools_guard( $telemovel, 'ai_kit_', 10 ) ) {
        return $guard;
    }

    $artist_name = function_exists( 'rmbz_get_artist_nome_by_telemovel' )
        ? (string) rmbz_get_artist_nome_by_telemovel( $telemovel ) : '';

    $facts = rmbz_insights_compute( $telemovel, 'all' );
    $ctx = [
        'artista'          => $artist_name ?: 'o artista',
        'maior_plataforma' => $facts['top_platform']['store'] ?? null,
        'pais_top'         => $facts['top_country']['name'] ?? null,
        'faixa_top'        => $facts['top_track']['title'] ?? null,
    ];

    $system = "És publicista musical da 1MZ Records (Moçambique). Escreves "
        . "materiais de divulgação em pt-PT para artistas independentes. Tom "
        . "autêntico, sem exageros. Usa apenas os dados fornecidos. Responde só "
        . "com JSON válido.";

    $instruction = rmbz_ai_profile_prefix( $telemovel )
        . "Cria um kit de divulgação para este artista.\n\n"
        . "DADOS:\n" . wp_json_encode( $ctx, JSON_UNESCAPED_UNICODE ) . "\n\n"
        . "Devolve EXACTAMENTE este JSON:\n"
        . "{ \"bio\": \"bio curta de artista, 2-3 frases (máx 320 caracteres)\", "
        . "\"pitch\": \"texto curto para enviar a curadores de playlists/rádio (máx 320 caracteres)\", "
        . "\"post_ideas\": [ \"ideia de publicação/reel para esta semana\" ] }\n"
        . "Inclui 3 ideias de posts, concretas e fáceis de gravar com um telemóvel.";

    $res = rmbz_ai_messages( $system, $instruction, [ 'max_tokens' => 800, 'temperature' => 0.8 ] );
    if ( empty( $res['ok'] ) ) {
        return rmbz_api_error( 'Não foi possível gerar o kit agora. ' . rmbz_ai_err_hint( $res ), 502, 'ai_error' );
    }

    $data = rmbz_ai_extract_json( $res['text'] );
    if ( ! is_array( $data ) || empty( $data['bio'] ) ) {
        return rmbz_api_error( 'A IA devolveu um formato inesperado. Tenta de novo.', 502, 'ai_format' );
    }

    $ideas = [];
    if ( ! empty( $data['post_ideas'] ) && is_array( $data['post_ideas'] ) ) {
        foreach ( $data['post_ideas'] as $i ) {
            $i = trim( (string) $i );
            if ( $i !== '' ) $ideas[] = $i;
        }
    }

    return rmbz_api_ok( [
        'bio'        => (string) $data['bio'],
        'pitch'      => isset( $data['pitch'] ) ? (string) $data['pitch'] : '',
        'post_ideas' => array_slice( $ideas, 0, 5 ),
    ] );
}

/* ----------------------------------------------------------------------- */
/* POST /me/ai/video-concept — conceito de videoclipe                      */
/* ----------------------------------------------------------------------- */

function rmbz_api_handle_ai_video_concept( WP_REST_Request $request ) {
    $telemovel  = (string) $request->get_param( 'rmbz_telemovel' );
    $song_title = sanitize_text_field( (string) $request->get_param( 'song_title' ) );
    $about      = trim( (string) $request->get_param( 'about' ) );
    $idea       = trim( (string) $request->get_param( 'idea' ) );
    $scenario   = sanitize_text_field( (string) $request->get_param( 'scenario' ) );

    if ( $about === '' && $idea === '' ) {
        return rmbz_api_error( 'Diz-nos sobre o que é a música ou a tua ideia.', 400, 'empty_input' );
    }
    if ( mb_strlen( $about ) > 600 ) $about = mb_substr( $about, 0, 600 );
    if ( mb_strlen( $idea ) > 600 )  $idea  = mb_substr( $idea, 0, 600 );

    if ( $guard = rmbz_ai_tools_guard( $telemovel, 'ai_video_', 15 ) ) {
        return $guard;
    }

    $artist_name = function_exists( 'rmbz_get_artist_nome_by_telemovel' )
        ? (string) rmbz_get_artist_nome_by_telemovel( $telemovel ) : '';

    // [1.5.299] Os 3 níveis de cenário — a IA adapta-se ao escolhido e pode
    // recomendar mudar se a ideia pedir outro nível.
    $levels = [
        'basic' => [
            'nome' => 'Básico',
            'desc' => 'produção simples e económica: 1-2 locais acessíveis, equipa mínima, edição direta. Bom para histórias intimistas e diretas.',
        ],
        'professional' => [
            'nome' => 'Profissional',
            'desc' => 'produção de qualidade: vários locais, melhor câmara e iluminação, mais figuração, edição cuidada. Para conceitos com mais ambição visual.',
        ],
        'cinema' => [
            'nome' => 'Cinema',
            'desc' => 'produção cinematográfica de alto nível: narrativa visual rica, cenários elaborados, planos complexos. Para grandes histórias e produções épicas.',
        ],
    ];
    $scenario_key = strtolower( trim( $scenario ) );
    $chosen = $levels[ $scenario_key ] ?? null;

    $ctx = [
        'artista'        => $artist_name ?: 'o artista',
        'musica'         => $song_title ?: null,
        'sobre'          => $about ?: null,
        'ideia'          => $idea ?: null,
        'nivel_escolhido'=> $chosen ? ( $chosen['nome'] . ' — ' . $chosen['desc'] ) : ( $scenario ?: null ),
        'niveis_disponiveis' => 'Básico (simples/económico), Profissional (qualidade média), Cinema (alto nível/épico)',
    ];

    $system = "És realizador e diretor criativo de videoclipes da 1MZ Records "
        . "(Moçambique). Escreves ROTEIROS de videoclipe ORGANIZADOS POR PARTES, que "
        . "contam uma HISTÓRIA verdadeira e emocionam os fãs. Falas português de "
        . "Portugal (pt-PT). Existem 3 NÍVEIS de produção (Básico, Profissional, "
        . "Cinema) com orçamentos crescentes — o roteiro tem de ser VIÁVEL no nível "
        . "que o artista escolheu. Usa apenas o que o artista deu, não inventes "
        . "factos sobre ele. Respeita a ideia dele em CADA parte. Responde só com JSON válido.";

    $instruction = rmbz_ai_profile_prefix( $telemovel )
        . "Transforma a ideia do artista num ROTEIRO de videoclipe ORGANIZADO, "
        . "ADEQUADO AO NÍVEL ESCOLHIDO, que aumente a probabilidade de ser aprovado.\n\n"
        . "DADOS:\n" . wp_json_encode( $ctx, JSON_UNESCAPED_UNICODE ) . "\n\n"
        . "Devolve um array \"sections\" com 5 a 6 PARTES, cada uma { \"title\", \"content\" }, "
        . "NESTA ordem e com estes títulos:\n"
        . "1. \"Conceito\" — a história/mensagem central e o que o fã vai sentir (2-4 frases).\n"
        . "2. \"Cenário e locais\" — onde se passa, locais concretos VIÁVEIS neste nível.\n"
        . "3. \"Estética e visual\" — cores, luz, mood, estilo de imagem.\n"
        . "4. \"Cenas principais\" — 3 a 5 momentos-chave, em sequência, ligados à energia/letra (1 linha cada).\n"
        . "5. \"Guarda-roupa e styling\" — visual do artista e de figurantes.\n"
        . "6. \"Produção e extras\" — adereços, figuração, efeitos/transições adequados ao nível.\n"
        . "Adapta a profundidade ao nível (no Básico sê enxuto e económico; no Cinema podes ser ambicioso). "
        . "Cada \"content\" é texto corrido curto e claro, sem markdown.\n\n"
        . "AVALIA O ENCAIXE: se a ideia for grande/épica demais para o nível escolhido, ou tão simples que o "
        . "nível é exagerado, escreve em \"recommendation\" uma sugestão curta de mudar de nível (e qual). "
        . "Se encaixa bem, devolve \"recommendation\" vazio.\n\n"
        . "Devolve EXACTAMENTE: { \"sections\": [ { \"title\": \"...\", \"content\": \"...\" } ], \"recommendation\": \"texto ou vazio\" }";

    $res = rmbz_ai_messages( $system, $instruction, [ 'max_tokens' => 1200, 'temperature' => 0.85 ] );
    if ( empty( $res['ok'] ) ) {
        return rmbz_api_error( 'Não foi possível gerar o conceito agora. ' . rmbz_ai_err_hint( $res ), 502, 'ai_error' );
    }

    $data = rmbz_ai_extract_json( $res['text'] );

    // Normaliza as secções estruturadas.
    $sections = [];
    if ( is_array( $data ) && ! empty( $data['sections'] ) && is_array( $data['sections'] ) ) {
        foreach ( $data['sections'] as $s ) {
            if ( ! is_array( $s ) ) continue;
            $title   = isset( $s['title'] ) ? trim( wp_strip_all_tags( (string) $s['title'] ) ) : '';
            $content = isset( $s['content'] ) ? trim( wp_strip_all_tags( (string) $s['content'] ) ) : '';
            if ( $content === '' ) continue;
            $sections[] = [ 'title' => $title !== '' ? $title : 'Parte', 'content' => $content ];
        }
    }

    $recommendation = ( is_array( $data ) && isset( $data['recommendation'] ) )
        ? trim( (string) $data['recommendation'] ) : '';

    // Concept combinado (texto único) — para compatibilidade e para "usar".
    if ( ! empty( $sections ) ) {
        $parts = [];
        foreach ( $sections as $s ) {
            $parts[] = strtoupper( $s['title'] ) . "\n" . $s['content'];
        }
        $concept = implode( "\n\n", $parts );
    } else {
        // Fallback: sem secções — usa o texto cru e mete numa secção única.
        $concept = trim( ( is_array( $data ) && ! empty( $data['concept'] ) ) ? (string) $data['concept'] : $res['text'] );
        if ( $concept === '' ) {
            return rmbz_api_error( 'A IA devolveu um formato inesperado. Tenta de novo.', 502, 'ai_format' );
        }
        $sections[] = [ 'title' => 'Conceito', 'content' => $concept ];
    }

    return rmbz_api_ok( [
        'concept'        => $concept,
        'sections'       => $sections,
        'recommendation' => $recommendation,
    ] );
}

/* ----------------------------------------------------------------------- */
/* POST /me/ai/list-tip — recomendação genérica das listas                 */
/* ----------------------------------------------------------------------- */

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

    // Desligado (ou sem chave) → a app usa o texto pré-definido dela.
    if ( ! rmbz_ai_recommendations_enabled() ) {
        return rmbz_api_ok( [ 'enabled' => false ] );
    }

    $domain    = sanitize_key( (string) $request->get_param( 'domain' ) );
    $kind      = sanitize_text_field( (string) $request->get_param( 'kind' ) );
    $noun      = sanitize_text_field( (string) $request->get_param( 'noun' ) );
    $title     = trim( wp_strip_all_tags( (string) $request->get_param( 'title' ) ) );
    $facts     = trim( wp_strip_all_tags( (string) $request->get_param( 'facts' ) ) );
    $situation = trim( wp_strip_all_tags( (string) $request->get_param( 'situation' ) ) );

    if ( $kind === '' || $situation === '' ) {
        return rmbz_api_ok( [ 'enabled' => false ] );
    }

    // Cache por estado — a mesma combinação reutiliza o texto (não gasta tokens
    // a cada abertura da lista). 7 dias.
    $cache_key = 'rmbz_listtip_' . md5( implode( '|', [
        $domain, $kind, $noun, $title, $facts, $situation,
    ] ) );
    $cached = get_transient( $cache_key );
    if ( is_array( $cached ) && ! empty( $cached['headline'] ) && ! empty( $cached['body'] ) ) {
        return rmbz_api_ok( [
            'enabled'  => true,
            'headline' => $cached['headline'],
            'body'     => $cached['body'],
        ] );
    }

    // Rede de segurança de custo se a cache falhar muito (ex.: títulos únicos).
    $rl = rmbz_api_rate_limit( 'ai_listtip_' . $telemovel, 120, DAY_IN_SECONDS );
    if ( empty( $rl['allowed'] ) ) {
        return rmbz_api_ok( [ 'enabled' => false ] );
    }

    $system = 'És o assistente do artista da 1MZ Records (Moçambique). Escreves em português de Portugal (pt-PT), '
            . 'caloroso, claro e curto. REGRAS OBRIGATÓRIAS: usa apenas os factos dados; NUNCA inventes números nem dados; '
            . 'respeita SEMPRE o género das palavras (a música/canção → a tua, publicada; o vídeo/videoclipe → o teu, publicado); '
            . 'trata o artista por tu. Responde SÓ com JSON válido, sem texto à volta: '
            . '{"headline":"2 a 4 palavras","body":"1 a 2 frases"}.';

    $block = '';
    if ( $noun  !== '' ) $block .= "Assunto: {$noun}\n";
    if ( $title !== '' ) $block .= "Título: \"{$title}\"\n";
    if ( $facts !== '' ) $block .= "Factos reais: {$facts}\n";

    $instruction = $block . "Situação a transmitir: {$situation}\n\n"
                 . 'Escreve a recomendação. Quando fizer sentido, inclui o título entre aspas no body.';

    $res = rmbz_ai_messages( $system, $instruction, [ 'max_tokens' => 220, 'temperature' => 0.7 ] );
    if ( empty( $res['ok'] ) ) {
        return rmbz_api_ok( [ 'enabled' => false ] );
    }

    $data = rmbz_ai_extract_json( $res['text'] );
    $headline = ( is_array( $data ) && ! empty( $data['headline'] ) )
        ? sanitize_text_field( (string) $data['headline'] ) : '';
    $body = ( is_array( $data ) && ! empty( $data['body'] ) )
        ? trim( wp_strip_all_tags( (string) $data['body'] ) ) : '';

    if ( $headline === '' || $body === '' ) {
        return rmbz_api_ok( [ 'enabled' => false ] );
    }

    set_transient( $cache_key, [ 'headline' => $headline, 'body' => $body ], 7 * DAY_IN_SECONDS );

    return rmbz_api_ok( [
        'enabled'  => true,
        'headline' => $headline,
        'body'     => $body,
    ] );
}

/* ----------------------------------------------------------------------- */
/* POST /me/ai/report-chat — "Detalhes com IA" sobre um relatório           */
/* ----------------------------------------------------------------------- */

/** Texto curto da variação a partir do delta {pct, direction}. */
function rmbz_report_delta_text( $delta ) {
    $dir = isset( $delta['direction'] ) ? $delta['direction'] : 'flat';
    $pct = isset( $delta['pct'] ) ? round( (float) $delta['pct'] ) : 0;
    if ( $dir === 'up' )   return "subiu {$pct}%";
    if ( $dir === 'down' ) return "desceu {$pct}%";
    return 'estável';
}

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

    // Add-on de IA + rate-limit (mensagem certa para VIP sem IA via guard).
    $guard = rmbz_ai_tools_guard( $telemovel, 'ai_report_chat_', 30 );
    if ( $guard ) return $guard;

    if ( ! function_exists( 'rmbz_insights_imports' ) ) {
        require_once dirname( __FILE__, 2 ) . '/ai/insights-engine.php';
    }

    $import_id = (int) $request->get_param( 'import_id' );
    $question  = trim( wp_strip_all_tags( (string) $request->get_param( 'question' ) ) );
    if ( $question === '' ) {
        $question = 'Explica-me este relatório em palavras simples.';
    }
    if ( function_exists( 'mb_substr' ) && mb_strlen( $question ) > 300 ) {
        $question = mb_substr( $question, 0, 300 );
    }

    $imports = rmbz_insights_imports( $telemovel, 12 );
    if ( empty( $imports ) ) {
        return rmbz_api_error(
            'Ainda não tens relatórios para analisar. Quando a distribuidora reportar, volta aqui.',
            404, 'no_reports'
        );
    }

    // Selecionar o relatório em foco (0/ inválido = mais recente).
    $sel_index = 0;
    if ( $import_id > 0 ) {
        foreach ( $imports as $i => $imp ) {
            if ( (int) $imp['id'] === $import_id ) { $sel_index = $i; break; }
        }
    }
    $sel  = $imports[ $sel_index ];
    $prev = isset( $imports[ $sel_index + 1 ] ) ? $imports[ $sel_index + 1 ] : null;
    $facts = rmbz_insights_compute_by_import( $telemovel, $sel, $prev );

    // ---- Bloco de factos do relatório em foco (números REAIS) ----
    $f = [];
    $f[] = sprintf( 'Streams: %d', (int) $facts['streams']['current'] );
    $f[] = sprintf( 'Ganhos: %.2f USD', (float) $facts['earnings']['current'] );
    if ( $prev ) {
        $f[] = sprintf(
            'Relatório anterior (%s): %d streams, %.2f USD',
            (string) $prev['label'], (int) $facts['streams']['previous'], (float) $facts['earnings']['previous']
        );
        $f[] = 'Variação de streams: ' . rmbz_report_delta_text( $facts['streams']['delta'] );
        $f[] = 'Variação de ganhos: '  . rmbz_report_delta_text( $facts['earnings']['delta'] );
    }
    if ( ! empty( $facts['top_platform'] ) ) {
        $f[] = sprintf( 'Plataforma top: %s (%d streams, %s%%)',
            $facts['top_platform']['store'], (int) $facts['top_platform']['streams'], $facts['top_platform']['percentage'] );
    }
    if ( ! empty( $facts['top_country'] ) ) {
        $f[] = sprintf( 'País top: %s (%d streams)',
            $facts['top_country']['name'], (int) $facts['top_country']['streams'] );
    }
    if ( ! empty( $facts['top_track'] ) ) {
        $f[] = sprintf( 'Faixa top: %s (%d streams)',
            $facts['top_track']['title'], (int) $facts['top_track']['streams'] );
    }
    // Receita POR PLATAFORMA — o ponto-chave para raciocinar sobre QUEM PAGA.
    if ( ! empty( $facts['platforms'] ) ) {
        $pl = [];
        foreach ( $facts['platforms'] as $p ) {
            $pl[] = sprintf( '%s: %d streams = %.2f USD',
                $p['store'], (int) $p['streams'], (float) $p['earnings_usd'] );
        }
        $f[] = 'Receita por plataforma neste relatório (compara o DINHEIRO, não só os streams): '
            . implode( ' | ', $pl );
    }

    // ---- Histórico de toda a vida (compacto, para comparar por data) ----
    $hist = [];
    foreach ( $imports as $imp ) {
        $hist[] = sprintf( '- %s: %d streams, %.2f USD',
            (string) $imp['label'], (int) $imp['streams'], (float) $imp['earnings_usd'] );
    }

    // Memória da conversa (turnos anteriores enviados pela app).
    $history_block = rmbz_ai_format_history( $request->get_param( 'history' ) );
    $has_history   = ( $history_block !== '' );

    // Cache por (artista + relatório + pergunta) — só quando NÃO há memória
    // (com conversa, cada turno é único e não deve ser reutilizado).
    $cache_key = 'rmbz_reptchat_v4_' . md5( $telemovel . '|' . $sel['id'] . '|' . $question );
    if ( ! $has_history ) {
        $cached = get_transient( $cache_key );
        if ( is_string( $cached ) && $cached !== '' ) {
            return rmbz_api_ok( [ 'answer' => $cached, 'report_label' => (string) $sel['label'] ] );
        }
    }

    $system = 'És um mentor musical experiente e RESPEITADO da 1MZ Records (Moçambique). Falas de '
        . 'ARTISTA PARA ARTISTA, em português de Portugal (pt-PT), tratando por tu — como alguém que '
        . 'se senta ao lado do artista e lhe diz a verdade. '
        . "POSTURA: honesto e REALISTA, não um animador. NÃO elogies por elogiar. Aponta com clareza o "
        . 'que está mal ou fraco (estagnação, queda, dependência de uma só plataforma/país, ganhos '
        . 'baixos face aos streams, falta de consistência entre relatórios) e diz porquê importa. '
        . 'Quando algo for bom, reconhece em 1 frase e segue para o que é preciso melhorar. '
        . "DETALHE: usa os NÚMEROS REAIS (streams, USD, %, datas, nomes de plataforma/país/faixa) para "
        . 'fundamentar cada afirmação; compara relatórios pelas datas do histórico e mostra a tendência. '
        . 'RACIOCÍNIO (importante): pensa antes de aconselhar. Para "ganhar dinheiro", o que conta é a '
        . 'RECEITA (USD), NÃO o volume de streams/visualizações — uma plataforma com menos views mas '
        . 'mais USD é melhor aposta; repara na "receita por plataforma". NUNCA recomendes focar numa '
        . 'plataforma só porque tem mais streams se ela paga pouco. ADAPTA-TE: se o artista te corrigir '
        . 'ou trouxer um facto novo (ex.: "o YouTube pagou-me mais"), aceita, reavalia com os números e '
        . 'MUDA a recomendação — não repitas a posição anterior nem dês conselhos padronizados. '
        . 'REGRAS: usa APENAS os dados fornecidos; NUNCA inventes números nem factos; não prometas '
        . 'resultados. Fecha com 2 a 3 ações concretas, realistas e priorizadas (a mais importante '
        . 'primeiro), ligadas ao objetivo do artista (se indicado no perfil). '
        . 'FORMATO: texto corrido, tom de conversa, claro e específico (~150 a 220 palavras). Sem '
        . 'markdown, sem títulos, sem listas com bullets — fala como uma pessoa.';

    $instruction = rmbz_ai_profile_prefix( $telemovel )
        . rmbz_ai_catalog_prefix( $telemovel )
        . 'RELATÓRIO EM FOCO: ' . $sel['label'] . "\n"
        . implode( "\n", $f ) . "\n\n"
        . "HISTÓRICO DE TODOS OS RELATÓRIOS (do mais recente ao mais antigo):\n"
        . implode( "\n", $hist ) . "\n\n"
        . $history_block
        . 'NOVA PERGUNTA DO ARTISTA: "' . $question . "\"\n\n"
        . 'Responde à pergunta de forma detalhada e honesta, dando continuidade à conversa acima. '
        . 'Fundamenta com os números reais, compara com outras datas do histórico, diz com franqueza '
        . 'o que está a correr mal e o que está bem, e termina com as ações concretas que tu farias '
        . 'no lugar dele.';

    $res = rmbz_ai_messages( $system, $instruction, [
        'model'       => rmbz_ai_chat_model(),
        'max_tokens'  => 750,
        'temperature' => 0.8,
    ] );
    if ( empty( $res['ok'] ) ) {
        return rmbz_api_error( 'Não consegui analisar o relatório agora. ' . rmbz_ai_err_hint( $res ), 502, 'ai_error' );
    }

    $answer = trim( $res['text'] );
    if ( $answer === '' ) {
        return rmbz_api_error( 'A IA devolveu uma resposta vazia. Tenta de novo.', 502, 'ai_empty' );
    }

    // Só cacheia a 1ª resposta (sem memória) — as de conversa são únicas.
    if ( ! $has_history ) {
        set_transient( $cache_key, $answer, DAY_IN_SECONDS );
    }

    return rmbz_api_ok( [
        'answer'       => $answer,
        'report_label' => (string) $sel['label'],
    ] );
}

/**
 * [1.6.333] Explica uma notificação do banner inteligente da Home. A IA diz
 * PORQUÊ o artista recebeu esta notificação e, se for um problema, COMO resolver
 * (passo a passo), usando o contexto real do artista (perfil + catálogo +
 * atividade + conhecimento da app). Só para VIP com add-on de IA.
 */
function rmbz_api_handle_ai_explain_notification( WP_REST_Request $request ) {
    $telemovel = (string) $request->get_param( 'rmbz_telemovel' );

    $guard = rmbz_ai_tools_guard( $telemovel, 'ai_explain_notif_', 60 );
    if ( $guard ) return $guard;

    $type  = trim( wp_strip_all_tags( (string) $request->get_param( 'type' ) ) );
    $label = trim( wp_strip_all_tags( (string) $request->get_param( 'label' ) ) );
    if ( function_exists( 'mb_substr' ) && mb_strlen( $label ) > 500 ) {
        $label = mb_substr( $label, 0, 500 );
    }
    if ( $label === '' && $type === '' ) {
        return rmbz_api_error( 'Notificação sem conteúdo para explicar.', 400, 'empty_notification' );
    }

    $type_desc = rmbz_ai_notification_type_desc( $type );

    // Cache por (artista + tipo + label) — a explicação é estável durante o dia.
    $cache_key = 'rmbz_xnotif_v1_' . md5( $telemovel . '|' . $type . '|' . $label );
    $cached = get_transient( $cache_key );
    if ( is_string( $cached ) && $cached !== '' ) {
        return rmbz_api_ok( [ 'enabled' => true, 'explanation' => $cached ] );
    }

    $system = 'És o Assistente pessoal da 1MZ Records (Moçambique) dentro da app do artista. '
        . 'Falas em português de Portugal (pt-PT), tratando por tu, de forma calorosa, clara e direta. '
        . 'O artista abriu uma NOTIFICAÇÃO que recebeu na app e quer perceber o que ela significa. '
        . 'A tua missão: (1) explicar em linguagem simples PORQUÊ recebeu esta notificação e o que ela '
        . 'quer dizer no contexto dele; (2) se for um PROBLEMA ou exigir uma ação, dizer EXATAMENTE o '
        . 'que fazer a seguir, passo a passo, e onde na app (Ganhar Vídeo, Distribuição, Site/Redes, '
        . 'Carteira, Subscrição, etc.); (3) se for boa notícia, celebrar em 1 frase e sugerir o próximo '
        . 'passo para aproveitar. Sê específico ao artista (usa o que sabes do perfil/catálogo/atividade). '
        . 'REGRAS: usa APENAS factos fornecidos; NUNCA inventes números, prazos ou nomes; não prometas '
        . 'resultados. Se faltar informação, diz o que ele deve verificar ou a quem falar (equipa/admin '
        . 'pelo chat do projeto). FORMATO: texto corrido, tom de conversa, curto e útil (~90 a 150 '
        . 'palavras). Sem markdown, sem títulos, sem bullets — fala como uma pessoa ao lado dele.';

    $instruction = rmbz_ai_profile_prefix( $telemovel )
        . rmbz_ai_catalog_prefix( $telemovel )
        . rmbz_ai_activity_prefix( $telemovel )
        . rmbz_ai_app_knowledge() . "\n"
        . 'NOTIFICAÇÃO RECEBIDA PELO ARTISTA:' . "\n"
        . ( $type_desc !== '' ? '- Significado do tipo: ' . $type_desc . "\n" : '' )
        . '- Texto mostrado: "' . $label . "\"\n\n"
        . 'Explica-lhe esta notificação e, se for um problema ou pedir ação, diz-lhe como resolver, '
        . 'passo a passo, no contexto real dele.';

    $res = rmbz_ai_messages( $system, $instruction, [
        'model'      => rmbz_ai_chat_model(),
        'max_tokens' => 600,
    ] );
    if ( empty( $res['ok'] ) ) {
        return rmbz_api_error( 'Não consegui explicar esta notificação agora. ' . rmbz_ai_err_hint( $res ), 502, 'ai_error' );
    }

    $explanation = trim( $res['text'] );
    if ( $explanation === '' ) {
        return rmbz_api_error( 'A IA devolveu uma resposta vazia. Tenta de novo.', 502, 'ai_empty' );
    }

    set_transient( $cache_key, $explanation, DAY_IN_SECONDS );

    return rmbz_api_ok( [ 'enabled' => true, 'explanation' => $explanation ] );
}

/**
 * [1.6.337] Explica ao artista uma LIMITAÇÃO/banimento ATIVO dele. A IA recebe o
 * motivo INTERNO (confidencial — não o reproduz textualmente se for sensível) +
 * o motivo mostrado + a data de desbloqueio, e responde com tato: porquê, o que
 * pode fazer e quando volta. Só VIP com IA.
 */
function rmbz_api_handle_ai_explain_restriction( WP_REST_Request $request ) {
    $telemovel = (string) $request->get_param( 'rmbz_telemovel' );

    $guard = rmbz_ai_tools_guard( $telemovel, 'ai_explain_restr_', 60 );
    if ( $guard ) return $guard;

    $key = trim( wp_strip_all_tags( (string) $request->get_param( 'key' ) ) );
    if ( $key === '' ) {
        return rmbz_api_error( 'Limitação não indicada.', 400, 'no_key' );
    }

    // Lê a meta REAL do artista (inclui o motivo interno — fica no servidor).
    global $wpdb;
    $row = $wpdb->get_row( $wpdb->prepare(
        "SELECT is_banned, extra_data FROM {$wpdb->prefix}rmbz_wallets WHERE user_telemovel = %s LIMIT 1",
        $telemovel
    ) );
    $extra = ( $row && ! empty( $row->extra_data ) ) ? json_decode( $row->extra_data, true ) : [];
    if ( ! is_array( $extra ) ) $extra = [];
    $meta = function_exists( 'rmbz_blocks_meta' ) ? rmbz_blocks_meta( $extra ) : ( $extra['blocks_meta'] ?? [] );
    $m = isset( $meta[ $key ] ) && is_array( $meta[ $key ] ) ? $meta[ $key ] : [];

    $label    = function_exists( 'rmbz_block_label' ) ? rmbz_block_label( $key ) : $key;
    $internal = isset( $m['internal'] ) ? trim( (string) $m['internal'] ) : '';
    $user_r   = isset( $m['user'] ) ? trim( (string) $m['user'] ) : '';
    $until    = ! empty( $m['until'] ) ? (int) $m['until'] : 0;
    $when     = $until > 0 ? wp_date( 'd/m/Y \à\s H:i', $until ) : '';

    $cache_key = 'rmbz_xrestr_v1_' . md5( $telemovel . '|' . $key . '|' . $internal . '|' . $until );
    $cached = get_transient( $cache_key );
    if ( is_string( $cached ) && $cached !== '' ) {
        return rmbz_api_ok( [ 'enabled' => true, 'explanation' => $cached ] );
    }

    $system = 'És o Assistente pessoal da 1MZ Records (Moçambique) dentro da app do artista. '
        . 'Falas em português de Portugal (pt-PT), tratando por tu, com calma, respeito e empatia. '
        . 'O artista tem uma LIMITAÇÃO/castigo ativo na conta e quer perceber porquê. '
        . 'Tu sabes o motivo interno da equipa (CONFIDENCIAL). REGRAS IMPORTANTES: '
        . '(1) NÃO reproduzas o texto interno tal e qual nem reveles detalhes sensíveis, nomes, '
        . 'provas ou valores específicos; explica a RAZÃO de forma geral e construtiva. '
        . '(2) Não acuses nem humilhes; mantém um tom de ajuda. '
        . '(3) Diz com clareza o que o artista pode fazer para resolver ou evitar repetir, e, se '
        . 'houver data de desbloqueio, lembra-a. (4) Se o motivo interno for muito sensível, dá uma '
        . 'explicação prudente e encaminha para o suporte. NUNCA inventes factos. '
        . 'FORMATO: texto corrido, curto e humano (~80 a 140 palavras), sem markdown nem listas.';

    $instruction = rmbz_ai_profile_prefix( $telemovel )
        . 'LIMITAÇÃO ATIVA: ' . $label . "\n"
        . ( $internal !== '' ? 'Motivo interno da equipa (CONFIDENCIAL — não citar literalmente): "' . $internal . "\"\n" : '' )
        . ( $user_r !== '' ? 'Mensagem já mostrada ao artista: "' . $user_r . "\"\n" : '' )
        . ( $when !== '' ? 'Desbloqueia em: ' . $when . "\n" : 'Sem data de desbloqueio definida (pode ser permanente).' . "\n" )
        . "\nExplica ao artista, com tato, porque tem esta limitação e o que pode fazer.";

    $res = rmbz_ai_messages( $system, $instruction, [
        'model'      => rmbz_ai_chat_model(),
        'max_tokens' => 600,
    ] );
    if ( empty( $res['ok'] ) ) {
        return rmbz_api_error( 'Não consegui explicar isto agora. ' . rmbz_ai_err_hint( $res ), 502, 'ai_error' );
    }

    $explanation = trim( $res['text'] );
    if ( $explanation === '' ) {
        return rmbz_api_error( 'A IA devolveu uma resposta vazia. Tenta de novo.', 502, 'ai_empty' );
    }

    set_transient( $cache_key, $explanation, DAY_IN_SECONDS );
    return rmbz_api_ok( [ 'enabled' => true, 'explanation' => $explanation ] );
}

/**
 * [1.6.381] A IA alerta/explica a MULTA por atraso do videoclipe. Lê a multa
 * real (rmbz_project_overdue_info) e explica com calma porque o projeto está
 * trancado e o que o artista deve fazer. Só VIP com IA.
 */
function rmbz_api_handle_ai_explain_fine( WP_REST_Request $request ) {
    $telemovel = (string) $request->get_param( 'rmbz_telemovel' );

    $guard = rmbz_ai_tools_guard( $telemovel, 'ai_explain_fine_', 60 );
    if ( $guard ) return $guard;

    $info = function_exists( 'rmbz_project_overdue_info' )
        ? rmbz_project_overdue_info( $telemovel ) : [ 'overdue' => false ];

    if ( empty( $info['overdue'] ) ) {
        return rmbz_api_ok( [
            'enabled'     => true,
            'explanation' => 'Boas notícias: neste momento não há nenhuma multa por atraso no teu videoclipe. Continua com a subscrição ativa durante a filmagem e a edição para a equipa não parar.',
        ] );
    }

    $unit_lbl = ( ( $info['unit'] ?? 'day' ) === 'hour' ) ? 'hora' : 'dia';
    $amount   = (float) ( $info['amount'] ?? 0 );
    $units    = (int) ( $info['units'] ?? 0 );
    $rate     = (float) ( $info['rate'] ?? 0 );
    $frozen   = ! empty( $info['frozen'] );
    $enabled  = ! empty( $info['enabled'] );
    $title    = (string) ( $info['project_title'] ?? 'o teu videoclipe' );

    $cache_key = 'rmbz_xfine_v1_' . md5( $telemovel . '|' . ( $frozen ? 'F' : 'L' ) . '|' . $amount . '|' . $units );
    $cached = get_transient( $cache_key );
    if ( is_string( $cached ) && $cached !== '' ) {
        return rmbz_api_ok( [ 'enabled' => true, 'explanation' => $cached ] );
    }

    $system = 'És o Assistente pessoal da 1MZ Records (Moçambique) dentro da app do artista. '
        . 'Falas em português de Portugal (pt-PT), tratas por tu, com calma, respeito e empatia. '
        . 'O videoclipe do artista está TRANCADO por uma multa por atraso. REGRAS: '
        . '(1) Explica de forma simples porque é que isto aconteceu (a produção — filmagem/edição — exige '
        . 'a subscrição VIP ativa; quando expira, a equipa pára e acumula multa). '
        . '(2) Se a multa está CONGELADA (o artista já renovou mas ficou dívida), explica que renovar parou a '
        . 'contagem mas a dívida pendente tem de ser resolvida para a equipa continuar. '
        . '(3) Diz o que deve fazer AGORA: pagar a multa na app (botão "Pagar multa") ou combinar com a equipa pelo chat. '
        . '(4) REFORÇA estas regras: a multa NÃO desaparece ao renovar — mesmo renovando o VIP, a dívida acumulada TEM de ser paga; '
        . 'e para a produção continuar precisa do plano VIP que INCLUI o serviço "Ganhar Vídeo" (vídeo), não basta um VIP qualquer. '
        . 'Aconselha, com simpatia, a NUNCA deixar esse VIP expirar enquanto tiver um projeto de vídeo ativo, para evitar multas e paragens. '
        . '(5) Tom de ajuda, sem culpar nem humilhar. NUNCA inventes valores — usa só os indicados. '
        . 'FORMATO: texto corrido, curto e humano (~90 a 150 palavras), sem markdown nem listas.';

    $estado = $frozen
        ? 'A multa está CONGELADA: o artista já renovou o VIP mas ficou esta dívida por pagar.'
        : 'A subscrição está expirada e a multa continua a acumular enquanto não renovar.';

    $valor_txt = ( $enabled && $rate > 0 )
        ? sprintf( 'Multa: %s MT (%d %s(s) × %s MT/%s).',
            number_format( $amount, 2, ',', '.' ), $units, $unit_lbl,
            number_format( $rate, 2, ',', '.' ), $unit_lbl )
        : 'A multa está sem valor definido (informativa), mas a produção está bloqueada.';

    $instruction = rmbz_ai_profile_prefix( $telemovel )
        . 'VIDEOCLIPE: "' . $title . "\"\n"
        . $estado . "\n"
        . $valor_txt . "\n"
        . "\nExplica ao artista, com tato, porque o projeto está trancado e o que fazer para resolver.";

    $res = rmbz_ai_messages( $system, $instruction, [
        'model'      => rmbz_ai_chat_model(),
        'max_tokens' => 600,
    ] );
    if ( empty( $res['ok'] ) ) {
        return rmbz_api_error( 'Não consegui explicar isto agora. ' . rmbz_ai_err_hint( $res ), 502, 'ai_error' );
    }

    $explanation = trim( $res['text'] );
    if ( $explanation === '' ) {
        return rmbz_api_error( 'A IA devolveu uma resposta vazia. Tenta de novo.', 502, 'ai_empty' );
    }

    set_transient( $cache_key, $explanation, 6 * HOUR_IN_SECONDS );
    return rmbz_api_ok( [ 'enabled' => true, 'explanation' => $explanation ] );
}
