<?php
/**
 * 1MZ For Artists — Motor de Insights (Fase 1: determinístico)
 *
 * O "Assistente do Artista" tem duas camadas:
 *
 *   1. CAMADA DETERMINÍSTICA (este ficheiro) — calcula FACTOS reais a partir
 *      das tabelas de analytics (streams, ganhos, variação %, top loja/país/
 *      faixa). Não custa nada e nunca inventa números. É a fonte da verdade.
 *
 *   2. CAMADA IA (futura) — recebe estes factos JÁ calculados e só os veste
 *      em linguagem natural (coaching + sugestões). O número "+32%" vem SEMPRE
 *      da camada 1; a IA apenas o redige. A função rmbz_insights_ai_enabled()
 *      é a costura: enquanto devolver false, usamos a narrativa por template
 *      (também em pt-PT) que está aqui.
 *
 * Reutiliza os helpers de analytics que já alimentam o ecrã de Stats:
 *   - rmbz_analytics_period_range( $period, $telemovel )
 *   - rmbz_analytics_delta( $current, $previous )
 *   - rmbz_country_flag( $code )
 *
 * @package 1MZ_For_Artists
 * @since 1.5.280
 */

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

if ( ! function_exists( 'rmbz_analytics_period_range' ) ) {
    require_once dirname( __FILE__, 2 ) . '/../distribution/distribution-analytics.php';
}
if ( ! function_exists( 'rmbz_ai_available' ) ) {
    require_once __DIR__ . '/ai-client.php';
}

/* ============================================================================
 * COSTURA PARA A IA
 * ============================================================================
 * [1.5.281] Ligada quando há chave da Anthropic configurada E a feature está
 * activa no admin. Caso contrário, cai na narrativa por template (também pt-PT).
 * O filtro permite forçar on/off em ambientes específicos.
 */
function rmbz_insights_ai_enabled() {
    $available = function_exists( 'rmbz_ai_available' ) && rmbz_ai_available();
    return (bool) apply_filters( 'rmbz_insights_ai_enabled', $available );
}

/* ============================================================================
 * NARRATIVA POR IA (Claude)
 * ============================================================================
 * Recebe os FACTOS já calculados e pede ao Claude apenas para os redigir em
 * coaching natural. Regras: pt-PT, nunca inventar números, devolver JSON.
 * Qualquer falha (sem chave, erro de rede, JSON inválido) cai no template.
 */
function rmbz_insights_ai_narrative( array $facts ) {
    $period_label = isset( $facts['period_label'] )
        ? $facts['period_label']
        : rmbz_insights_period_label( $facts['period'] );
    $fallback = rmbz_insights_template_narrative( $facts, $period_label );

    if ( ! function_exists( 'rmbz_ai_messages' ) ) {
        return $fallback;
    }

    // Compactar só os factos relevantes para o prompt (sem PII).
    $facts_for_ai = [
        'periodo'        => $period_label,
        'is_lifetime'    => (bool) $facts['is_all'],
        'streams'        => [
            'atual'         => (int) $facts['streams']['current'],
            'anterior'      => (int) $facts['streams']['previous'],
            'variacao_pct'  => $facts['streams']['delta']['pct'],
            'direcao'       => $facts['streams']['delta']['direction'],
        ],
        'ganhos_usd'     => [
            'atual'         => (float) $facts['earnings']['current'],
            'anterior'      => (float) $facts['earnings']['previous'],
            'variacao_pct'  => $facts['earnings']['delta']['pct'],
            'direcao'       => $facts['earnings']['delta']['direction'],
        ],
        '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,
    ];

    $system = "És o assistente de carreira da 1MZ Records, uma editora e "
        . "distribuidora de música de Moçambique. Falas português de Portugal "
        . "(pt-PT), como um manager HONESTO e REALISTA — torces pelo artista, mas "
        . "dizes-lhe a verdade, não és um animador. REGRAS ABSOLUTAS: (1) usa "
        . "apenas os números fornecidos — NUNCA inventes métricas, valores ou "
        . "plataformas; (2) não prometas resultados; (3) sê concreto e accionável; "
        . "(4) responde SEMPRE só com JSON válido, sem texto à volta. POSTURA: não "
        . "elogies por elogiar. Se os números mostram estagnação, queda, "
        . "dependência de uma só plataforma/país ou ganhos baixos face aos "
        . "streams, diz-o com clareza no summary e faz as sugestões CORRIGIREM "
        . "essa fraqueza. Quando há bom desempenho, reconhece-o em poucas palavras "
        . "e aponta o próximo passo.";

    $instruction = "Com base nestes factos do artista (em JSON), escreve uma "
        . "leitura curta e útil.\n\nFACTOS:\n" . wp_json_encode( $facts_for_ai, JSON_UNESCAPED_UNICODE )
        . "\n\nDevolve EXACTAMENTE este JSON:\n"
        . "{\n"
        . "  \"headline\": \"1 frase de impacto, máx 90 caracteres\",\n"
        . "  \"summary\": \"1 a 2 frases de contexto, máx 220 caracteres\",\n"
        . "  \"suggestions\": [ {\"icon\": \"emoji\", \"text\": \"ação concreta, máx 120 caracteres\"} ]\n"
        . "}\n"
        . "Entre 2 e 4 sugestões. Se não houver período anterior comparável, não fales de variação.";

    $res = rmbz_ai_messages( $system, $instruction, [
        'max_tokens'  => 600,
        'temperature' => 0.6,
    ] );

    if ( empty( $res['ok'] ) ) {
        return $fallback;
    }

    $data = rmbz_ai_extract_json( $res['text'] );
    if ( ! is_array( $data ) || empty( $data['headline'] ) ) {
        return $fallback;
    }

    // Guardar as sugestões da IA (se vierem bem formadas) para o build usar.
    if ( ! empty( $data['suggestions'] ) && is_array( $data['suggestions'] ) ) {
        $clean = [];
        foreach ( $data['suggestions'] as $s ) {
            if ( ! is_array( $s ) || empty( $s['text'] ) ) continue;
            $clean[] = [
                'icon' => isset( $s['icon'] ) ? (string) $s['icon'] : '💡',
                'text' => (string) $s['text'],
            ];
        }
        if ( $clean ) {
            $GLOBALS['rmbz_insights_ai_suggestions'] = array_slice( $clean, 0, 4 );
        }
    }

    return [
        'headline' => (string) $data['headline'],
        'summary'  => isset( $data['summary'] ) ? (string) $data['summary'] : $fallback['summary'],
    ];
}

/* ============================================================================
 * CÁLCULO DOS FACTOS
 * ============================================================================ */

/**
 * Calcula o pacote de factos do artista para um período.
 *
 * @param string $telemovel   Telemóvel do artista (login).
 * @param string $period      7d|30d|90d|12m|all.
 * @param array  $custom_range Intervalo opcional [from => Y-m-d, to => Y-m-d].
 * @return array  Estrutura de factos (sempre com 'has_data').
 */
function rmbz_insights_compute( $telemovel, $period = '30d', $custom_range = [] ) {
    global $wpdb;

    $allowed = [ '7d', '30d', '90d', '12m', 'all' ];
    if ( ! in_array( $period, $allowed, true ) ) $period = '30d';

    $range = null;
    if ( is_array( $custom_range ) && ! empty( $custom_range['from'] ) && ! empty( $custom_range['to'] ) ) {
        $range = rmbz_insights_custom_range( $custom_range['from'], $custom_range['to'] );
        if ( is_array( $range ) ) $period = 'custom';
    }
    if ( ! is_array( $range ) ) {
        $range = rmbz_analytics_period_range( $period, $telemovel );
    }
    $is_all = ! empty( $range['is_all'] );
    $period_label = $period === 'custom'
        ? sprintf(
            'entre %s e %s',
            date_i18n( 'd/m/Y', strtotime( $range['from'] ) ),
            date_i18n( 'd/m/Y', strtotime( $range['to'] ) )
        )
        : rmbz_insights_period_label( $period );

    $table_daily    = $wpdb->prefix . 'rmbz_distro_daily_stats';
    $table_tracks   = $wpdb->prefix . 'rmbz_distro_tracks';
    $table_releases = $wpdb->prefix . 'rmbz_distro_releases';

    $empty = [
        'period'       => $period,
        'period_label' => $period_label,
        'is_all'       => $is_all,
        'date_from'    => $range['from'],
        'date_to'      => $range['to'],
        'has_data'     => false,
        'streams'      => [ 'current' => 0, 'previous' => 0, 'delta' => [ 'pct' => 0, 'direction' => 'flat' ] ],
        'earnings'     => [ 'current' => 0.0, 'previous' => 0.0, 'currency' => 'USD', 'delta' => [ 'pct' => 0, 'direction' => 'flat' ] ],
        'top_platform' => null,
        'top_country'  => null,
        'top_track'    => null,
    ];

    if ( ! function_exists( 'rmbz_analytics_table_exists' ) || ! rmbz_analytics_table_exists( $table_daily ) ) {
        return $empty;
    }

    $pa    = function_exists( 'rmbz_approval_clause' ) ? rmbz_approval_clause( $table_daily ) : '';

    // ── Streams + ganhos: período actual ────────────────────────────────
    $now = $wpdb->get_row( $wpdb->prepare(
        "SELECT COALESCE(SUM(streams),0) s, COALESCE(SUM(sales),0) e
         FROM $table_daily
         WHERE artist_telemovel = %s AND report_date BETWEEN %s AND %s $pa",
        $telemovel, $range['from'], $range['to']
    ) );

    // ── Streams + ganhos: período anterior (para variação) ──────────────
    $prev = $wpdb->get_row( $wpdb->prepare(
        "SELECT COALESCE(SUM(streams),0) s, COALESCE(SUM(sales),0) e
         FROM $table_daily
         WHERE artist_telemovel = %s AND report_date BETWEEN %s AND %s $pa",
        $telemovel, $range['prev_from'], $range['prev_to']
    ) );

    $streams_now  = $now  ? (int)   $now->s  : 0;
    $earn_now     = $now  ? (float) $now->e  : 0.0;
    $streams_prev = $prev ? (int)   $prev->s : 0;
    $earn_prev    = $prev ? (float) $prev->e : 0.0;

    // Sem período anterior comparável em 'all' → não mostrar variação.
    $flat = [ 'pct' => 0, 'direction' => 'flat' ];
    $streams_delta = $is_all ? $flat : rmbz_analytics_delta( $streams_now, $streams_prev );
    $earn_delta    = $is_all ? $flat : rmbz_analytics_delta( $earn_now, $earn_prev );

    // ── Top plataforma ──────────────────────────────────────────────────
    $top_platform = null;
    $plat_total = (int) $wpdb->get_var( $wpdb->prepare(
        "SELECT COALESCE(SUM(streams),0) FROM $table_daily
         WHERE artist_telemovel = %s AND report_date BETWEEN %s AND %s $pa",
        $telemovel, $range['from'], $range['to']
    ) );
    $plat_row = $wpdb->get_row( $wpdb->prepare(
        "SELECT store, SUM(streams) s FROM $table_daily
         WHERE artist_telemovel = %s AND report_date BETWEEN %s AND %s $pa
         GROUP BY store ORDER BY s DESC LIMIT 1",
        $telemovel, $range['from'], $range['to']
    ) );
    if ( $plat_row && (int) $plat_row->s > 0 ) {
        $top_platform = [
            'store'      => (string) $plat_row->store,
            'streams'    => (int) $plat_row->s,
            'percentage' => $plat_total > 0 ? round( ( (int) $plat_row->s / $plat_total ) * 100, 1 ) : 0.0,
        ];
    }

    // ── Plataformas por RECEITA (streams + USD) — para a IA perceber QUEM
    //    PAGA, não só quem tem mais visualizações.
    $platforms = [];
    $prows = $wpdb->get_results( $wpdb->prepare(
        "SELECT store, SUM(streams) s, SUM(sales) sales FROM $table_daily
         WHERE artist_telemovel = %s AND report_date BETWEEN %s AND %s $pa
         GROUP BY store ORDER BY sales DESC LIMIT 6",
        $telemovel, $range['from'], $range['to']
    ) );
    foreach ( (array) $prows as $pr ) {
        $platforms[] = [
            'store'        => (string) $pr->store,
            'streams'      => (int) $pr->s,
            'earnings_usd' => round( (float) $pr->sales, 2 ),
        ];
    }

    // ── Top país ────────────────────────────────────────────────────────
    $top_country = null;
    $country_row = $wpdb->get_row( $wpdb->prepare(
        "SELECT country_code, SUM(streams) s FROM $table_daily
         WHERE artist_telemovel = %s AND report_date BETWEEN %s AND %s $pa
         GROUP BY country_code ORDER BY s DESC LIMIT 1",
        $telemovel, $range['from'], $range['to']
    ) );
    if ( $country_row && (int) $country_row->s > 0 ) {
        $code = (string) $country_row->country_code;
        $top_country = [
            'code'       => $code,
            'name'       => rmbz_insights_country_name( $code ),
            'flag'       => function_exists( 'rmbz_country_flag' ) ? rmbz_country_flag( $code ) : '',
            'streams'    => (int) $country_row->s,
            'percentage' => $plat_total > 0 ? round( ( (int) $country_row->s / $plat_total ) * 100, 1 ) : 0.0,
        ];
    }

    // ── Top faixa ───────────────────────────────────────────────────────
    $top_track = null;
    if ( rmbz_analytics_table_exists( $table_tracks ) ) {
        $track_row = $wpdb->get_row( $wpdb->prepare(
            "SELECT t.track_title, r.title AS release_title, SUM(d.streams) AS s
             FROM $table_daily d
             JOIN $table_tracks   t ON d.track_id = t.id
             JOIN $table_releases r ON t.release_id = r.id
             WHERE d.artist_telemovel = %s
               AND d.report_date BETWEEN %s AND %s
               AND d.track_id IS NOT NULL
               AND ( d.pending_approval IS NULL OR d.pending_approval = 0 )
             GROUP BY t.id ORDER BY s DESC LIMIT 1",
            $telemovel, $range['from'], $range['to']
        ) );
        if ( $track_row && (int) $track_row->s > 0 ) {
            $top_track = [
                'title'         => (string) $track_row->track_title,
                'release_title' => (string) $track_row->release_title,
                'streams'       => (int) $track_row->s,
            ];
        }
    }

    $has_data = ( $streams_now > 0 || $earn_now > 0 || $top_track !== null );

    return [
        'period'       => $period,
        'period_label' => $period_label,
        'is_all'       => $is_all,
        'has_data'     => $has_data,
        'date_from'    => $range['from'],
        'date_to'      => $range['to'],
        'streams'  => [
            'current'  => $streams_now,
            'previous' => $streams_prev,
            'delta'    => $streams_delta,
        ],
        'earnings' => [
            'current'  => round( $earn_now, 2 ),
            'previous' => round( $earn_prev, 2 ),
            'currency' => 'USD',
            'delta'    => $earn_delta,
        ],
        'top_platform' => $top_platform,
        'platforms'    => $platforms,
        'top_country'  => $top_country,
        'top_track'    => $top_track,
    ];
}

/**
 * Normaliza um intervalo personalizado e calcula o período anterior com a
 * mesma duração. Devolve null para datas incompletas, inválidas ou futuras.
 */
function rmbz_insights_custom_range( $from_raw, $to_raw ) {
    $from_raw = trim( (string) $from_raw );
    $to_raw   = trim( (string) $to_raw );
    $timezone = function_exists( 'wp_timezone' ) ? wp_timezone() : null;
    $from = DateTimeImmutable::createFromFormat( '!Y-m-d', $from_raw, $timezone ?: null );
    $to   = DateTimeImmutable::createFromFormat( '!Y-m-d', $to_raw, $timezone ?: null );
    $today = new DateTimeImmutable( current_time( 'Y-m-d' ), $timezone ?: null );

    if ( ! $from || ! $to || $from->format( 'Y-m-d' ) !== $from_raw || $to->format( 'Y-m-d' ) !== $to_raw ) {
        return null;
    }
    if ( $from > $to || $from > $today || $to > $today ) {
        return null;
    }

    $days = (int) $from->diff( $to )->days + 1;
    $prev_to = $from->modify( '-1 day' );
    $prev_from = $prev_to->modify( '-' . ( $days - 1 ) . ' days' );
    $bucket = $days > 365 ? 'month' : ( $days > 90 ? 'week' : 'day' );

    return [
        'from'       => $from->format( 'Y-m-d' ),
        'to'         => $to->format( 'Y-m-d' ),
        'prev_from'  => $prev_from->format( 'Y-m-d' ),
        'prev_to'    => $prev_to->format( 'Y-m-d' ),
        'bucket'     => $bucket,
        'days'       => $days,
        'is_all'     => false,
        'is_custom'  => true,
    ];
}

/* ============================================================================
 * ANÁLISE POR RELATÓRIO/IMPORTAÇÃO
 * ============================================================================
 * As distribuidoras reportam em lotes (tipicamente trimestrais). Faz mais
 * sentido analisar POR RELATÓRIO (import) do que por janelas de dias — daí o
 * seletor do app ser a lista de relatórios e não 7/30/90 dias.
 */

/**
 * Lista os relatórios (imports) do artista que têm dados, mais recente
 * primeiro. Cada item já traz `label`/`subtitle` prontos para o app.
 */
function rmbz_insights_imports( $telemovel, $limit = 12 ) {
    global $wpdb;
    $t_logs  = $wpdb->prefix . 'rmbz_distro_import_logs';
    $t_daily = $wpdb->prefix . 'rmbz_distro_daily_stats';

    if ( ! rmbz_analytics_table_exists( $t_logs ) || ! rmbz_analytics_table_exists( $t_daily ) ) {
        return [];
    }

    $cols        = (array) $wpdb->get_col( "SHOW COLUMNS FROM $t_daily" );
    $has_log_id  = in_array( 'import_log_id', $cols, true );
    $has_hash    = in_array( 'import_hash', $cols, true );
    $has_pending = in_array( 'pending_approval', $cols, true );
    $approval    = $has_pending ? 'AND ds.pending_approval = 0' : '';

    if ( $has_log_id && $has_hash ) {
        $join = "ds.import_log_id = il.id OR ( ds.import_log_id IS NULL AND ds.import_hash = il.file_hash )";
    } elseif ( $has_hash ) {
        $join = "ds.import_hash = il.file_hash";
    } else {
        $join = "ds.import_log_id = il.id";
    }

    $rows = $wpdb->get_results( $wpdb->prepare(
        "SELECT il.id, il.source_name, il.reporting_period, il.imported_at, il.file_hash,
                COALESCE(SUM(ds.sales),0)   AS earnings_usd,
                COALESCE(SUM(ds.streams),0) AS streams
         FROM $t_logs il
         INNER JOIN $t_daily ds ON ( $join )
         WHERE il.status = 'active' AND ds.artist_telemovel = %s $approval
         GROUP BY il.id
         HAVING earnings_usd > 0 OR streams > 0
         ORDER BY il.imported_at DESC
         LIMIT %d",
        $telemovel, $limit
    ) );

    $out = [];
    foreach ( (array) $rows as $r ) {
        $period = trim( (string) ( $r->reporting_period ?? '' ) );
        // O nome do parceiro de distribuição fica restrito à administração.
        $source = '1MZ For Artists';
        $date   = $r->imported_at ? date_i18n( 'd M Y', strtotime( $r->imported_at ) ) : '';
        $label  = $period !== '' ? $period : ( 'Relatório de ' . $date );
        $out[] = [
            'id'              => (int) $r->id,
            'file_hash'       => (string) ( $r->file_hash ?? '' ),
            'source_name'     => $source,
            'reporting_period'=> $period,
            'imported_at'     => (string) $r->imported_at,
            'streams'         => (int) $r->streams,
            'earnings_usd'    => round( (float) $r->earnings_usd, 2 ),
            'label'           => $label,
            'subtitle'        => $source . ( $date ? ' · ' . $date : '' ),
            // flags de schema para a query de top-* reutilizar a ligação certa
            '_has_log_id'     => $has_log_id,
            '_has_hash'       => $has_hash,
            '_has_pending'    => $has_pending,
        ];
    }
    return $out;
}

/**
 * [1.6.304] Constrói o "Perfil do Artista" — bloco de contexto reutilizado por
 * TODA a IA (coach, insights, report-chat, kit, legenda, conceito de vídeo).
 * Junta o objetivo DECLARADO (1 toque) com factos CALCULADOS da plataforma.
 * Todos os números são reais. Cache 6h (invalidada ao mudar o objetivo).
 *
 * @return string Bloco pronto a injetar nos prompts (vazio se sem dados úteis).
 */
function rmbz_ai_artist_profile( $telemovel ) {
    // v2: removido o saldo da carteira do contexto — esse dinheiro é para PAGAR
    // serviços (vídeo/subscrição), não é receita da música. A IA fala de
    // dinheiro só pelos ganhos das plataformas (USD dos relatórios).
    $cache_key = 'rmbz_ai_profile_v2_' . md5( $telemovel );
    $cached = get_transient( $cache_key );
    if ( is_string( $cached ) ) return $cached;

    global $wpdb;
    $tw = $wpdb->prefix . 'rmbz_wallets';
    $w  = $wpdb->get_row( $wpdb->prepare(
        "SELECT user_nome, user_provincia FROM {$tw} WHERE user_telemovel = %s LIMIT 1",
        $telemovel
    ) );

    $lines = [];

    $nome = $w && ! empty( $w->user_nome ) ? trim( (string) $w->user_nome ) : '';
    if ( $nome !== '' ) $lines[] = "Nome: {$nome}";
    if ( $w && ! empty( $w->user_provincia ) ) {
        $lines[] = 'Localização: ' . trim( (string) $w->user_provincia ) . ', Moçambique';
    }

    // Objetivo declarado (o que mais personaliza).
    if ( function_exists( 'rmbz_ai_get_goal' ) ) {
        $goal = rmbz_ai_goal_label( rmbz_ai_get_goal( $telemovel ) );
        if ( $goal !== '' ) $lines[] = "Objetivo principal: {$goal}";
    }

    // Estatuto (sem o saldo da carteira — ver nota no topo da função).
    $active = function_exists( 'rmbz_user_has_active_subscription' ) && rmbz_user_has_active_subscription( $telemovel );
    $has_ai = function_exists( 'rmbz_ai_user_is_vip' ) && rmbz_ai_user_is_vip( $telemovel );
    $lines[] = 'Subscrição: ' . ( $active ? ( $has_ai ? 'VIP com add-on de IA' : 'VIP' ) : 'sem subscrição ativa' );

    // Catálogo / trajetória (a partir dos relatórios reais).
    $imports = rmbz_insights_imports( $telemovel, 12 );
    $rc = count( $imports );
    $lines[] = "Relatórios recebidos: {$rc}";

    if ( $rc > 0 ) {
        $sel  = $imports[0];
        $prev = isset( $imports[1] ) ? $imports[1] : null;
        $f    = rmbz_insights_compute_by_import( $telemovel, $sel, $prev );
        $cur  = (int) $f['streams']['current'];
        $lines[] = sprintf( 'Último relatório (%s): %d streams, %.2f USD',
            (string) $sel['label'], $cur, (float) $f['earnings']['current'] );
        if ( $prev ) {
            $d = $f['streams']['delta'];
            $dir = isset( $d['direction'] ) && $d['direction'] === 'up' ? 'a subir'
                 : ( isset( $d['direction'] ) && $d['direction'] === 'down' ? 'a descer' : 'estável' );
            $lines[] = "Tendência de streams: {$dir}";
        }
        if ( ! empty( $f['top_platform'] ) ) $lines[] = 'Plataforma principal: ' . $f['top_platform']['store'];
        if ( ! empty( $f['top_country'] ) )  $lines[] = 'País principal: ' . $f['top_country']['name'];

        // Fase de carreira estimada (heurística simples a partir dos dados).
        $stage = $rc < 2 ? 'a começar'
               : ( $cur < 2000 ? 'em crescimento inicial'
               : ( $cur < 20000 ? 'em crescimento' : 'estabelecido' ) );
        $lines[] = "Fase estimada: {$stage}";
    } else {
        $lines[] = 'Fase estimada: a começar (ainda sem relatórios)';
    }

    $block = "PERFIL DO ARTISTA (factos reais — usa para personalizar a resposta; "
        . "NÃO os listes de volta nem inventes nada além disto):\n"
        . implode( "\n", array_map( function ( $l ) { return "- {$l}"; }, $lines ) );

    set_transient( $cache_key, $block, 6 * HOUR_IN_SECONDS );
    return $block;
}

/**
 * [1.6.319] "Novidades / o que se passou" — para o assistente responder a
 * "o que perdi offline?", "o admin mandou alguma coisa?", "algum serviço meu
 * foi desativado?". Reúne notificações recentes, avisos/mensagens gerais do
 * admin (anúncios) e o estado atual dos serviços (permissões da subscrição).
 * Factos reais. Cache 5 min.
 */
function rmbz_ai_artist_activity( $telemovel ) {
    $cache_key = 'rmbz_ai_activity_' . md5( $telemovel );
    $cached = get_transient( $cache_key );
    if ( is_string( $cached ) ) return $cached;

    global $wpdb;
    $lines = [];

    // [1.6.320] Quando ficou offline (para marcar o que surgiu DESDE então).
    $away_since = 0;
    $wallet_extra = $wpdb->get_var( $wpdb->prepare(
        "SELECT extra_data FROM {$wpdb->prefix}rmbz_wallets WHERE user_telemovel = %s LIMIT 1",
        $telemovel
    ) );
    if ( $wallet_extra ) {
        $we = json_decode( $wallet_extra, true );
        if ( is_array( $we ) && ! empty( $we['away_since'] ) ) {
            $away_since = (int) $we['away_since'];
        }
    }
    if ( $away_since > 0 ) {
        $secs = max( 0, time() - $away_since );
        $ago  = $secs < 3600 ? floor( $secs / 60 ) . ' min'
              : ( $secs < 86400 ? floor( $secs / 3600 ) . 'h' : floor( $secs / 86400 ) . ' dia(s)' );
        $lines[] = 'OFFLINE DESDE: ' . date_i18n( 'd M Y H:i', $away_since ) . " (há {$ago}). "
                 . 'Os itens marcados "(novo)" surgiram desde então — são o que o artista PERDEU offline.';
    }

    $mark_new = function ( $ts_string ) use ( $away_since ) {
        if ( $away_since <= 0 || empty( $ts_string ) ) return '';
        return ( strtotime( $ts_string ) >= $away_since ) ? ' (novo)' : '';
    };

    // --- Notificações recentes (sistema/admin) ---
    if ( function_exists( 'rmbz_notify_get_for_user' ) ) {
        $notifs = rmbz_notify_get_for_user( $telemovel, [ 'limit' => 10 ] );
        if ( ! empty( $notifs ) ) {
            $items = [];
            foreach ( $notifs as $n ) {
                $created = isset( $n->created_at ) ? (string) $n->created_at : '';
                $when  = $created !== '' ? date_i18n( 'd M', strtotime( $created ) ) : '';
                $title = trim( (string) ( isset( $n->title ) ? $n->title : '' ) );
                $body  = trim( wp_strip_all_tags( (string) ( isset( $n->body ) ? $n->body : '' ) ) );
                if ( function_exists( 'mb_substr' ) && mb_strlen( $body ) > 160 ) {
                    $body = mb_substr( $body, 0, 160 ) . '…';
                }
                $items[] = trim( ( $when ? "[$when] " : '' ) . $title
                    . ( $body !== '' ? ' — ' . $body : '' ) . $mark_new( $created ) );
            }
            $lines[] = "NOTIFICAÇÕES RECENTES:\n  - " . implode( "\n  - ", $items );
        }
    }

    // --- Avisos / mensagens gerais do admin (anúncios ativos) ---
    $t_ann = $wpdb->prefix . 'rmbz_app_announcements';
    if ( $wpdb->get_var( "SHOW TABLES LIKE '$t_ann'" ) === $t_ann ) {
        $is_vip = function_exists( 'rmbz_announcements_user_is_vip' )
            ? rmbz_announcements_user_is_vip( $telemovel ) : false;
        $aud = $is_vip ? 'vip' : 'non_vip';
        $now_utc = gmdate( 'Y-m-d H:i:s' );
        $arows = $wpdb->get_results( $wpdb->prepare(
            "SELECT title, message, level, starts_at FROM {$t_ann}
              WHERE is_active = 1
                AND ( starts_at IS NULL OR starts_at <= %s )
                AND ( ends_at   IS NULL OR ends_at   >= %s )
                AND ( audience = 'all' OR audience = %s )
              ORDER BY sort_order ASC, id DESC LIMIT 10",
            $now_utc, $now_utc, $aud
        ) );
        if ( ! empty( $arows ) ) {
            $items = [];
            foreach ( $arows as $a ) {
                $msg = trim( wp_strip_all_tags( (string) $a->message ) );
                if ( function_exists( 'mb_substr' ) && mb_strlen( $msg ) > 200 ) {
                    $msg = mb_substr( $msg, 0, 200 ) . '…';
                }
                $items[] = trim( (string) $a->title . ( $msg !== '' ? ' — ' . $msg : '' )
                    . $mark_new( isset( $a->starts_at ) ? $a->starts_at : '' ) );
            }
            $lines[] = "AVISOS/MENSAGENS GERAIS DO ADMIN (ativos agora):\n  - " . implode( "\n  - ", $items );
        }
    }

    // --- Estado dos serviços (permissões) — p/ "um serviço foi desativado" ---
    if ( function_exists( 'rmbz_user_has_active_subscription' ) ) {
        if ( rmbz_user_has_active_subscription( $telemovel ) && function_exists( 'rmbz_api_get_user_vip_permissions' ) ) {
            $perms = rmbz_api_get_user_vip_permissions( $telemovel );
            $map = [
                'distribute' => 'distribuição', 'site' => 'publicar no site',
                'social' => 'redes sociais', 'video' => 'ganhar vídeo', 'ai' => 'assistente de IA',
            ];
            $on = []; $off = [];
            foreach ( $map as $k => $lbl ) {
                if ( ! empty( $perms[ $k ] ) ) $on[] = $lbl; else $off[] = $lbl;
            }
            $lines[] = 'SERVIÇOS ATIVOS na subscrição: ' . ( $on ? implode( ', ', $on ) : 'nenhum' )
                     . ( $off ? '. NÃO incluídos: ' . implode( ', ', $off ) : '' );
        } else {
            $lines[] = 'SUBSCRIÇÃO: sem subscrição ativa — os serviços VIP estão indisponíveis até renovar.';
        }
    }

    // [1.6.351] Multa por atraso ATIVA agora (videoclipe em produção + subscrição
    // expirada) — para a IA poder avisar/explicar com o valor real.
    if ( function_exists( 'rmbz_project_overdue_info' ) ) {
        $ov = rmbz_project_overdue_info( $telemovel );
        if ( ! empty( $ov['overdue'] ) ) {
            $unit_lbl  = $ov['unit'] === 'hour' ? 'horas' : 'dias';
            $unit_each = $ov['unit'] === 'hour' ? 'hora' : 'dia';
            $title     = $ov['project_title'] !== '' ? $ov['project_title'] : 'em curso';
            if ( ! empty( $ov['frozen'] ) ) {
                // [1.6.384] VIP já renovado, mas ficou dívida por pagar (congelada).
                $lines[] = ! empty( $ov['enabled'] )
                    ? sprintf(
                        'MULTA PENDENTE (NÃO está expirada): o artista JÁ RENOVOU o VIP, mas ficou uma multa por atraso de %s MT por pagar no videoclipe "%s" (CONGELADA — não está a aumentar). Para a equipa continuar a produção, tem de PAGAR a multa (na app, botão "Pagar multa") ou combinar com a equipa pelo chat. NÃO digas que a subscrição está expirada.',
                        number_format( (float) $ov['amount'], 2, ',', '.' ), $title )
                    : sprintf(
                        'PENDÊNCIA NO VIDEOCLIPE "%s": o artista já renovou o VIP, mas ficou uma pendência por resolver com a equipa antes de continuar a produção.',
                        $title );
            } elseif ( ! empty( $ov['enabled'] ) ) {
                $lines[] = sprintf(
                    'MULTA POR ATRASO ATIVA AGORA: o videoclipe "%s" está em produção mas a subscrição EXPIROU há %d %s; multa já acumulada ≈ %s MT (a %s MT/%s e a subir). Renovar a subscrição PÁRA a multa; pagar a multa (na app) limpa a dívida.',
                    $title, (int) $ov['units'], $unit_lbl,
                    number_format( (float) $ov['amount'], 2, ',', '.' ),
                    number_format( (float) $ov['rate'], 2, ',', '.' ), $unit_each
                );
            } else {
                $lines[] = sprintf(
                    'VÍDEO BLOQUEADO POR SUBSCRIÇÃO: o videoclipe "%s" está em produção mas a subscrição EXPIROU há %d %s; a equipa não pode trabalhar até renovar (multa desligada de momento).',
                    $title, (int) $ov['units'], $unit_lbl
                );
            }
        }
    }

    $block = empty( $lines ) ? '' :
        "NOVIDADES / O QUE SE PASSOU (factos reais — usa para responder a 'o que perdi?', "
        . "'o admin mandou algo?', 'algum serviço foi desativado?'):\n" . implode( "\n", $lines ) . "\n\n";

    set_transient( $cache_key, $block, 5 * MINUTE_IN_SECONDS );
    return $block;
}

/**
 * [1.6.317] Rótulo legível (pt-PT) do estado de um lançamento de distribuição.
 */
function rmbz_ai_release_status_label( $status ) {
    $map = [
        'draft'       => 'rascunho',
        'incomplete'  => 'incompleto',
        'submitted'   => 'em análise',
        'pending'     => 'em análise',
        'live'        => 'publicado',
        'distributed' => 'publicado',
        'returned'    => 'devolvido p/ correção',
        'rejected'    => 'rejeitado',
        'takedown'    => 'removido',
    ];
    $s = strtolower( trim( (string) $status ) );
    return isset( $map[ $s ] ) ? $map[ $s ] : ( $s !== '' ? $s : 'desconhecido' );
}

/**
 * [1.6.317] Catálogo e atividade COMPLETOS do artista (cross-serviço), para a
 * IA conhecer tudo: lançamentos de distribuição, submissões (site/redes) e
 * projetos de vídeo (Ganhar Vídeo). Factos reais. Cache 10 min.
 *
 * @return string Bloco pronto a injetar nos prompts (vazio se nada).
 */
function rmbz_ai_artist_catalog( $telemovel ) {
    $cache_key = 'rmbz_ai_catalog_' . md5( $telemovel );
    $cached = get_transient( $cache_key );
    if ( is_string( $cached ) ) return $cached;

    global $wpdb;
    $lines = [];

    // --- Lançamentos de distribuição ---
    $rel = $wpdb->prefix . 'rmbz_distro_releases';
    $rows = $wpdb->get_results( $wpdb->prepare(
        "SELECT title, release_type, status, release_date, live_at
         FROM {$rel} WHERE artist_telemovel = %s ORDER BY created_at DESC LIMIT 20",
        $telemovel
    ) );
    if ( $rows ) {
        $items = [];
        foreach ( $rows as $r ) {
            $st   = rmbz_ai_release_status_label( (string) $r->status );
            $date = '';
            if ( ! empty( $r->live_at ) )        $date = date_i18n( 'M Y', strtotime( $r->live_at ) );
            elseif ( ! empty( $r->release_date ) ) $date = date_i18n( 'M Y', strtotime( $r->release_date ) );
            $items[] = sprintf( '"%s" (%s, %s%s)',
                $r->title, $r->release_type ?: 'single', $st, $date ? ', ' . $date : '' );
        }
        $lines[] = 'LANÇAMENTOS DE DISTRIBUIÇÃO (' . count( $rows ) . '): ' . implode( '; ', $items );
    } else {
        $lines[] = 'LANÇAMENTOS DE DISTRIBUIÇÃO: nenhum ainda.';
    }

    // --- Submissões (site/redes) ---
    $sub = $wpdb->prefix . 'rmbz_artist_submissions';
    $srows = $wpdb->get_results( $wpdb->prepare(
        "SELECT music_title, scenario_type, review_status
         FROM {$sub} WHERE artist_telemovel = %s ORDER BY id DESC LIMIT 12",
        $telemovel
    ) );
    if ( $srows ) {
        $items = [];
        foreach ( $srows as $s ) {
            $items[] = sprintf( '"%s" (%s — %s)',
                $s->music_title, $s->scenario_type, $s->review_status );
        }
        $lines[] = 'SUBMISSÕES (site/redes): ' . implode( '; ', $items );
    }

    // --- Projetos de vídeo (Ganhar Vídeo) ---
    $proj = $wpdb->prefix . 'rmbz_artist_projects';
    $prows = $wpdb->get_results( $wpdb->prepare(
        "SELECT project_title, scenario_type, production_status
         FROM {$proj} WHERE artist_telemovel = %s ORDER BY id DESC LIMIT 12",
        $telemovel
    ) );
    if ( $prows ) {
        $items = [];
        foreach ( $prows as $p ) {
            $items[] = sprintf( '"%s" (cenário %s — %s)',
                $p->project_title ?: 'sem título', $p->scenario_type, $p->production_status );
        }
        $lines[] = 'PROJETOS DE VÍDEO (Ganhar Vídeo): ' . implode( '; ', $items );
    }

    $block = empty( $lines ) ? '' :
        "CATÁLOGO E ATIVIDADE DO ARTISTA (factos reais — usa para responder sobre lançamentos, "
        . "submissões e vídeos; não inventes):\n- " . implode( "\n- ", $lines ) . "\n\n";

    set_transient( $cache_key, $block, 10 * MINUTE_IN_SECONDS );
    return $block;
}

/**
 * Calcula os factos de UM relatório (import), comparando com o relatório
 * anterior (se existir) para a variação.
 *
 * @param array      $sel  Linha do relatório seleccionado (de rmbz_insights_imports).
 * @param array|null $prev Linha do relatório anterior (para delta), ou null.
 */
function rmbz_insights_compute_by_import( $telemovel, array $sel, $prev = null ) {
    global $wpdb;
    $t_daily    = $wpdb->prefix . 'rmbz_distro_daily_stats';
    $t_tracks   = $wpdb->prefix . 'rmbz_distro_tracks';
    $t_releases = $wpdb->prefix . 'rmbz_distro_releases';

    // Predicado que isola as linhas DESTE relatório para o artista.
    $approval = ! empty( $sel['_has_pending'] ) ? 'AND ds.pending_approval = 0' : '';
    if ( ! empty( $sel['_has_log_id'] ) && ! empty( $sel['_has_hash'] ) ) {
        $link = $wpdb->prepare(
            "( ds.import_log_id = %d OR ( ds.import_log_id IS NULL AND ds.import_hash = %s ) )",
            $sel['id'], $sel['file_hash']
        );
    } elseif ( ! empty( $sel['_has_hash'] ) ) {
        $link = $wpdb->prepare( "ds.import_hash = %s", $sel['file_hash'] );
    } else {
        $link = $wpdb->prepare( "ds.import_log_id = %d", $sel['id'] );
    }
    $where = $wpdb->prepare( "ds.artist_telemovel = %s", $telemovel ) . " AND $link $approval";

    $streams_now  = (int)   $sel['streams'];
    $earn_now     = (float) $sel['earnings_usd'];
    $streams_prev = $prev ? (int)   $prev['streams']      : 0;
    $earn_prev    = $prev ? (float) $prev['earnings_usd'] : 0.0;
    $has_prev     = ( $prev !== null );

    $flat = [ 'pct' => 0, 'direction' => 'flat' ];
    $streams_delta = $has_prev ? rmbz_analytics_delta( $streams_now, $streams_prev ) : $flat;
    $earn_delta    = $has_prev ? rmbz_analytics_delta( $earn_now, $earn_prev ) : $flat;

    // Top plataforma
    $top_platform = null;
    $plat_row = $wpdb->get_row(
        "SELECT ds.store AS store, SUM(ds.streams) s FROM $t_daily ds
         WHERE $where GROUP BY ds.store ORDER BY s DESC LIMIT 1"
    );
    if ( $plat_row && (int) $plat_row->s > 0 ) {
        $top_platform = [
            'store'      => (string) $plat_row->store,
            'streams'    => (int) $plat_row->s,
            'percentage' => $streams_now > 0 ? round( ( (int) $plat_row->s / $streams_now ) * 100, 1 ) : 0.0,
        ];
    }

    // Plataformas por RECEITA (streams + USD) — quem PAGA, não só quem tem views.
    $platforms = [];
    $prows = $wpdb->get_results(
        "SELECT ds.store store, SUM(ds.streams) s, SUM(ds.sales) sales FROM $t_daily ds
         WHERE $where GROUP BY ds.store ORDER BY sales DESC LIMIT 6"
    );
    foreach ( (array) $prows as $pr ) {
        $platforms[] = [
            'store'        => (string) $pr->store,
            'streams'      => (int) $pr->s,
            'earnings_usd' => round( (float) $pr->sales, 2 ),
        ];
    }

    // Top país
    $top_country = null;
    $country_row = $wpdb->get_row(
        "SELECT ds.country_code AS country_code, SUM(ds.streams) s FROM $t_daily ds
         WHERE $where GROUP BY ds.country_code ORDER BY s DESC LIMIT 1"
    );
    if ( $country_row && (int) $country_row->s > 0 ) {
        $code = (string) $country_row->country_code;
        $top_country = [
            'code'       => $code,
            'name'       => rmbz_insights_country_name( $code ),
            'flag'       => function_exists( 'rmbz_country_flag' ) ? rmbz_country_flag( $code ) : '',
            'streams'    => (int) $country_row->s,
            'percentage' => $streams_now > 0 ? round( ( (int) $country_row->s / $streams_now ) * 100, 1 ) : 0.0,
        ];
    }

    // Top faixa
    $top_track = null;
    if ( rmbz_analytics_table_exists( $t_tracks ) ) {
        $track_row = $wpdb->get_row(
            "SELECT t.track_title, r.title AS release_title, SUM(ds.streams) AS s
             FROM $t_daily ds
             JOIN $t_tracks   t ON ds.track_id = t.id
             JOIN $t_releases r ON t.release_id = r.id
             WHERE $where AND ds.track_id IS NOT NULL
             GROUP BY t.id ORDER BY s DESC LIMIT 1"
        );
        if ( $track_row && (int) $track_row->s > 0 ) {
            $top_track = [
                'title'         => (string) $track_row->track_title,
                'release_title' => (string) $track_row->release_title,
                'streams'       => (int) $track_row->s,
            ];
        }
    }

    $has_data = ( $streams_now > 0 || $earn_now > 0 || $top_track !== null );

    return [
        'period'       => 'import',
        'period_label' => 'neste relatório',
        'is_all'       => false,
        'has_data'     => $has_data,
        'streams'  => [
            'current'  => $streams_now,
            'previous' => $streams_prev,
            'delta'    => $streams_delta,
        ],
        'earnings' => [
            'current'  => round( $earn_now, 2 ),
            'previous' => round( $earn_prev, 2 ),
            'currency' => 'USD',
            'delta'    => $earn_delta,
        ],
        'top_platform' => $top_platform,
        'platforms'    => $platforms,
        'top_country'  => $top_country,
        'top_track'    => $top_track,
    ];
}

/* ============================================================================
 * NARRATIVA + CARDS + SUGESTÕES (template determinístico — pt-PT)
 * ============================================================================ */

/**
 * Constrói a "voz do assistente" a partir dos factos.
 * Devolve headline, summary, cards[] e suggestions[].
 *
 * Quando a IA estiver ligada, esta função delega a redacção da narrativa ao
 * Claude (alimentando os factos), mas os CARDS/números continuam a vir daqui.
 */
function rmbz_insights_build( array $facts ) {
    $period_label = isset( $facts['period_label'] )
        ? $facts['period_label']
        : rmbz_insights_period_label( $facts['period'] );

    if ( empty( $facts['has_data'] ) ) {
        return [
            'headline'    => 'Ainda sem dados para analisar',
            'summary'     => 'Quando os teus relatórios de streams chegarem, o assistente '
                           . 'mostra aqui o que está a crescer e o que podes fazer a seguir.',
            'cards'       => [],
            'suggestions' => [
                [ 'icon' => '🚀', 'text' => 'Lança ou divulga uma faixa para começares a ver insights.' ],
            ],
        ];
    }

    $cards = rmbz_insights_cards( $facts, $period_label );

    // Narrativa: IA (Claude) ou template determinístico.
    // A narrativa por IA pode também produzir sugestões — ficam num global
    // temporário que lemos a seguir (evita reestruturar a assinatura).
    unset( $GLOBALS['rmbz_insights_ai_suggestions'] );
    if ( rmbz_insights_ai_enabled() && function_exists( 'rmbz_insights_ai_narrative' ) ) {
        $narrative = rmbz_insights_ai_narrative( $facts );
    } else {
        $narrative = rmbz_insights_template_narrative( $facts, $period_label );
    }

    // Sugestões: preferir as da IA (se vieram bem formadas), senão template.
    $suggestions = ! empty( $GLOBALS['rmbz_insights_ai_suggestions'] )
        ? $GLOBALS['rmbz_insights_ai_suggestions']
        : rmbz_insights_suggestions( $facts );
    unset( $GLOBALS['rmbz_insights_ai_suggestions'] );

    return [
        'headline'    => $narrative['headline'],
        'summary'     => $narrative['summary'],
        'cards'       => $cards,
        'suggestions' => $suggestions,
    ];
}

/**
 * Narrativa por template — frase principal + resumo, em pt-PT, derivada
 * exclusivamente dos factos (sem inventar nada).
 */
function rmbz_insights_template_narrative( array $facts, $period_label ) {
    $sd = $facts['streams']['delta'];
    $streams_now = (int) $facts['streams']['current'];

    // Headline: variação de streams é a métrica mais "viva".
    if ( $facts['is_all'] ) {
        $headline = sprintf( 'Já somas %s reproduções no total.', rmbz_insights_short_num( $streams_now ) );
    } elseif ( $sd['direction'] === 'up' ) {
        $headline = sprintf( 'As tuas reproduções subiram %s%% %s.', rmbz_insights_pct( $sd['pct'] ), $period_label );
    } elseif ( $sd['direction'] === 'down' ) {
        $headline = sprintf( 'As reproduções desceram %s%% %s.', rmbz_insights_pct( $sd['pct'] ), $period_label );
    } else {
        $headline = sprintf( 'Reproduções estáveis %s.', $period_label );
    }

    // Summary: junta top plataforma + top país, se existirem.
    $bits = [];
    if ( ! empty( $facts['top_platform'] ) ) {
        $bits[] = sprintf(
            '%s é a tua maior plataforma (%s%% dos streams)',
            $facts['top_platform']['store'],
            rmbz_insights_pct( $facts['top_platform']['percentage'] )
        );
    }
    if ( ! empty( $facts['top_country'] ) ) {
        $bits[] = sprintf(
            '%s lidera por país %s',
            $facts['top_country']['name'],
            $facts['top_country']['flag']
        );
    }
    if ( ! empty( $facts['top_track'] ) ) {
        $bits[] = sprintf( '"%s" é a tua faixa mais ouvida', $facts['top_track']['title'] );
    }

    $summary = $bits ? ( implode( '. ', $bits ) . '.' )
                     : 'Continua a divulgar — cada partilha conta.';

    return [ 'headline' => $headline, 'summary' => $summary ];
}

/**
 * Cards numéricos prontos para o app. Cada card:
 *   icon, label, value, sub (pode ser ''), tone (positive|neutral|negative)
 */
function rmbz_insights_cards( array $facts, $period_label ) {
    $cards = [];

    // Streams
    $sd = $facts['streams']['delta'];
    $cards[] = [
        'key'   => 'streams',
        'icon'  => '🎧',
        'label' => 'Reproduções ' . $period_label,
        'value' => rmbz_insights_short_num( (int) $facts['streams']['current'] ),
        'sub'   => rmbz_insights_delta_label( $sd, $facts['is_all'] ),
        'tone'  => rmbz_insights_tone( $sd, $facts['is_all'] ),
    ];

    // Ganhos
    $ed = $facts['earnings']['delta'];
    $cards[] = [
        'key'   => 'earnings',
        'icon'  => '💵',
        'label' => 'Ganhos ' . $period_label,
        'value' => '$' . number_format( (float) $facts['earnings']['current'], 2 ),
        'sub'   => rmbz_insights_delta_label( $ed, $facts['is_all'] ),
        'tone'  => rmbz_insights_tone( $ed, $facts['is_all'] ),
    ];

    // Top plataforma
    if ( ! empty( $facts['top_platform'] ) ) {
        $cards[] = [
            'key'   => 'platform',
            'icon'  => '📡',
            'label' => 'Maior plataforma',
            'value' => $facts['top_platform']['store'],
            'sub'   => rmbz_insights_pct( $facts['top_platform']['percentage'] ) . '% dos streams',
            'tone'  => 'neutral',
        ];
    }

    // Top país
    if ( ! empty( $facts['top_country'] ) ) {
        $cards[] = [
            'key'   => 'country',
            'icon'  => $facts['top_country']['flag'] ?: '🌍',
            'label' => 'País nº 1',
            'value' => $facts['top_country']['name'],
            'sub'   => rmbz_insights_pct( $facts['top_country']['percentage'] ) . '% dos streams',
            'tone'  => 'neutral',
        ];
    }

    // Top faixa
    if ( ! empty( $facts['top_track'] ) ) {
        $cards[] = [
            'key'   => 'track',
            'icon'  => '⭐',
            'label' => 'Faixa mais ouvida',
            'value' => $facts['top_track']['title'],
            'sub'   => rmbz_insights_short_num( (int) $facts['top_track']['streams'] ) . ' reproduções',
            'tone'  => 'positive',
        ];
    }

    return $cards;
}

/**
 * Sugestões acionáveis derivadas dos factos (regras simples, determinísticas).
 */
function rmbz_insights_suggestions( array $facts ) {
    $out = [];

    $sd = $facts['streams']['delta'];
    if ( ! $facts['is_all'] && $sd['direction'] === 'up' && $sd['pct'] >= 10 ) {
        $out[] = [ 'icon' => '🔥', 'text' => 'Estás em alta — aproveita o momento e publica algo novo esta semana.' ];
    }
    if ( ! $facts['is_all'] && $sd['direction'] === 'down' && $sd['pct'] >= 10 ) {
        $out[] = [ 'icon' => '📣', 'text' => 'As reproduções abrandaram. Partilha as tuas faixas nas redes para reativar.' ];
    }

    if ( ! empty( $facts['top_platform'] ) ) {
        $out[] = [
            'icon' => '📡',
            'text' => sprintf(
                'A maioria ouve-te no %s. Garante que o teu perfil nessa plataforma está completo e partilhado.',
                $facts['top_platform']['store']
            ),
        ];
    }

    if ( ! empty( $facts['top_country'] ) ) {
        $out[] = [
            'icon' => $facts['top_country']['flag'] ?: '🌍',
            'text' => sprintf(
                'O teu maior público está em %s. Pensa em conteúdo ou divulgação dirigida a esse mercado.',
                $facts['top_country']['name']
            ),
        ];
    }

    if ( ! empty( $facts['top_track'] ) ) {
        $out[] = [
            'icon' => '⭐',
            'text' => sprintf(
                '"%s" é o que mais agarra ouvintes. Usa-a como porta de entrada nas tuas divulgações.',
                $facts['top_track']['title']
            ),
        ];
    }

    if ( empty( $out ) ) {
        $out[] = [ 'icon' => '💡', 'text' => 'Mantém um ritmo de lançamentos — consistência cria audiência.' ];
    }

    // Limite saudável para a UI.
    return array_slice( $out, 0, 4 );
}

/* ============================================================================
 * COACH DIÁRIO — saudação + dica do dia (para todos)
 * ============================================================================
 * Mensagem curta e motivadora, com 1 acção concreta baseada nos números
 * reais. Faz o artista sentir-se acompanhado no dia-a-dia. IA quando ligada,
 * template caso contrário. Pensado para ser cacheado por dia (barato).
 */
function rmbz_coach_build( $telemovel ) {
    $facts    = rmbz_insights_compute( $telemovel, 'all' );
    $presence = rmbz_coach_presence( $telemovel );   // [1.6.353] todos os serviços
    $segment  = rmbz_coach_segment();                // [1.6.353] manhã/tarde/noite

    $nome = '';
    if ( function_exists( 'rmbz_get_artist_nome_by_telemovel' ) ) {
        $nome = (string) rmbz_get_artist_nome_by_telemovel( $telemovel );
    }
    $primeiro = $nome !== '' ? trim( explode( ' ', $nome )[0] ) : '';
    $greeting = rmbz_coach_greeting( $primeiro );

    // [1.6.353] Com IA: gera e GUARDA localmente (a dica fica em cache no
    // controller); regista a dica para NUNCA repetir.
    if ( rmbz_insights_ai_enabled() && function_exists( 'rmbz_ai_messages' ) ) {
        $goal = function_exists( 'rmbz_ai_get_goal' )
            ? rmbz_ai_goal_label( rmbz_ai_get_goal( $telemovel ) ) : '';
        $ai = rmbz_coach_ai( $facts, $presence, $primeiro, $goal, $segment, $telemovel );
        if ( $ai ) {
            rmbz_coach_seen_add( $telemovel, $ai['tip']['text'] );
            return [ 'greeting' => $greeting, 'message' => $ai['message'], 'tip' => $ai['tip'] ];
        }
    }

    // Sem IA (ou falha): textos LOCAIS, por período, sem repetir.
    $built = rmbz_coach_template( $facts, $presence, $greeting, $segment, $telemovel );
    rmbz_coach_seen_add( $telemovel, $built['tip']['text'] );
    return $built;
}

function rmbz_coach_greeting( $primeiro ) {
    $h = (int) current_time( 'G' );
    $saudacao = $h < 12 ? 'Bom dia' : ( $h < 19 ? 'Boa tarde' : 'Boa noite' );
    return $primeiro !== '' ? "$saudacao, $primeiro!" : "$saudacao!";
}

/**
 * [1.5.295] Saudação FRESCA para um artista — recalculada a cada pedido (não
 * cacheada), para a hora do dia estar sempre certa. A mensagem/dica continuam
 * em cache diária; só a saudação é dinâmica.
 */
function rmbz_coach_greeting_for( $telemovel ) {
    $nome = function_exists( 'rmbz_get_artist_nome_by_telemovel' )
        ? (string) rmbz_get_artist_nome_by_telemovel( $telemovel ) : '';
    $primeiro = $nome !== '' ? trim( explode( ' ', $nome )[0] ) : '';
    return rmbz_coach_greeting( $primeiro );
}

/** [1.6.353] Período do dia: 'manha' (<12), 'tarde' (<19) ou 'noite'. */
function rmbz_coach_segment() {
    $h = (int) current_time( 'G' );
    return $h < 12 ? 'manha' : ( $h < 19 ? 'tarde' : 'noite' );
}

/** Rótulo humano do período. */
function rmbz_coach_segment_label( $seg = null ) {
    $seg = $seg ?: rmbz_coach_segment();
    return $seg === 'manha' ? 'manhã' : ( $seg === 'tarde' ? 'tarde' : 'noite' );
}

/**
 * [1.6.353] Presença do artista em TODOS os serviços (não só imports/streams):
 * distribuição, site, redes e videoclipes. Conta simples, para o coach falar de
 * toda a atividade e não só de relatórios.
 */
function rmbz_coach_presence( $telemovel ) {
    global $wpdb;
    $rel       = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}rmbz_distro_releases WHERE artist_telemovel = %s", $telemovel ) );
    $rel_live  = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}rmbz_distro_releases WHERE artist_telemovel = %s AND (status = 'live' OR status = 'publicado')", $telemovel ) );
    $sub_site  = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}rmbz_artist_submissions WHERE artist_telemovel = %s AND scenario_type = 'Publicação Site'", $telemovel ) );
    $sub_soc   = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}rmbz_artist_submissions WHERE artist_telemovel = %s AND scenario_type = 'Redes Sociais'", $telemovel ) );
    $proj      = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}rmbz_artist_projects WHERE artist_telemovel = %s", $telemovel ) );
    return [
        'releases'      => $rel,
        'releases_live' => $rel_live,
        'sub_site'      => $sub_site,
        'sub_social'    => $sub_soc,
        'projects'      => $proj,
        'any'           => ( $rel + $sub_site + $sub_soc + $proj ) > 0,
    ];
}

/** [1.6.353] Lista persistente de dicas já mostradas (TEXTOS), para NUNCA repetir. */
function rmbz_coach_seen_get( $telemovel ) {
    $v = get_option( 'rmbz_coach_seen_' . md5( $telemovel ), [] );
    return is_array( $v ) ? $v : [];
}
function rmbz_coach_seen_norm( $text ) {
    $t = function_exists( 'mb_strtolower' ) ? mb_strtolower( trim( (string) $text ) ) : strtolower( trim( (string) $text ) );
    return preg_replace( '/\s+/', ' ', $t );
}
function rmbz_coach_seen_add( $telemovel, $text ) {
    if ( trim( (string) $text ) === '' ) return;
    $list = rmbz_coach_seen_get( $telemovel );
    array_unshift( $list, trim( (string) $text ) );
    // Únicos por forma normalizada, cap 60.
    $seen = []; $out = [];
    foreach ( $list as $t ) {
        $n = rmbz_coach_seen_norm( $t );
        if ( $n === '' || isset( $seen[ $n ] ) ) continue;
        $seen[ $n ] = 1; $out[] = $t;
    }
    update_option( 'rmbz_coach_seen_' . md5( $telemovel ), array_slice( $out, 0, 60 ), false );
}
function rmbz_coach_was_seen( $telemovel, $text ) {
    $n = rmbz_coach_seen_norm( $text );
    foreach ( rmbz_coach_seen_get( $telemovel ) as $t ) {
        if ( rmbz_coach_seen_norm( $t ) === $n ) return true;
    }
    return false;
}
/** Últimas N dicas (texto) para a IA evitar — juntas por "\n- ". */
function rmbz_coach_recent_texts( $telemovel, $n = 10 ) {
    $list = array_slice( rmbz_coach_seen_get( $telemovel ), 0, $n );
    return implode( "\n- ", $list );
}

/** Ângulos de foco rotativos para o coach (usados pela IA para variar o tema). */
function rmbz_coach_focus_angles() {
    return [
        'reforçar a presença na maior plataforma do artista',
        'falar diretamente com o público do país/região onde mais é ouvido',
        'uma ideia simples de reel/vídeo curto para gravar esta semana',
        'usar a faixa mais ouvida como porta de entrada para novos ouvintes',
        'pedir aos fãs para guardarem e partilharem a música',
        'manter consistência e preparar o próximo lançamento',
        'interagir com a comunidade — responder, agradecer, criar ligação',
        'procurar uma colaboração ou feature com outro artista',
        'otimizar o perfil (foto, bio, links) numa das plataformas',
        'aproveitar uma data ou tendência local de Moçambique',
        'reaproveitar conteúdo antigo que funcionou bem',
        'criar um momento de bastidores autêntico para mostrar o processo',
    ];
}

/**
 * [1.6.353] Coach por template (LOCAL, sem IA). Junta dicas de TODOS os serviços
 * (streams, distribuição, site, redes, vídeo) + dicas gerais, escolhe uma que
 * AINDA não foi mostrada (nunca repetir) e roda a frase de motivação conforme o
 * PERÍODO do dia (manhã/tarde/noite).
 */
function rmbz_coach_template( array $facts, array $presence, $greeting, $segment, $telemovel ) {
    // Pool de dicas, das mais relevantes (dados) às gerais.
    $tips = [];
    if ( ! empty( $facts['top_platform'] ) ) {
        $tips[] = [ 'icon' => '📡', 'text' => sprintf( 'O %s é a tua maior plataforma — garante que o perfil lá está completo e partilhado.', $facts['top_platform']['store'] ) ];
    }
    if ( ! empty( $facts['top_country'] ) ) {
        $tips[] = [ 'icon' => $facts['top_country']['flag'] ?: '🌍', 'text' => sprintf( 'O teu maior público está em %s. Fala com eles na próxima publicação.', $facts['top_country']['name'] ) ];
    }
    if ( ! empty( $facts['top_track'] ) ) {
        $tips[] = [ 'icon' => '⭐', 'text' => sprintf( 'Usa a "%s" — a tua faixa mais ouvida — para atrair novos ouvintes.', $facts['top_track']['title'] ) ];
    }
    // Dicas por SERVIÇO (presença real do artista).
    if ( $presence['releases'] === 0 ) {
        $tips[] = [ 'icon' => '🚀', 'text' => 'Ainda não lançaste nas lojas digitais — distribui a tua primeira música e chega ao Spotify, Apple e mais.' ];
    } else {
        $tips[] = [ 'icon' => '🎵', 'text' => sprintf( 'Já tens %d lançamento(s) nas lojas — partilha o link de um deles hoje com quem ainda não ouviu.', $presence['releases'] ) ];
    }
    if ( $presence['sub_site'] === 0 ) {
        $tips[] = [ 'icon' => '🌐', 'text' => 'Publica uma música no site 1MusicMoz — é mais uma porta para novos ouvintes te descobrirem.' ];
    }
    if ( $presence['sub_social'] === 0 ) {
        $tips[] = [ 'icon' => '📣', 'text' => 'Envia um vídeo para divulgação nas redes da 1MZ — alcance grátis para a tua música.' ];
    }
    if ( $presence['projects'] === 0 ) {
        $tips[] = [ 'icon' => '🎬', 'text' => 'Candidata uma música ao Ganhar Vídeo — a 1MZ pode produzir-te um videoclipe.' ];
    }
    // Dicas gerais (sempre disponíveis).
    $tips = array_merge( $tips, [
        [ 'icon' => '💛', 'text' => 'Pede aos teus fãs para guardarem a tua música nas playlists deles.' ],
        [ 'icon' => '🎥', 'text' => 'Um vídeo curto nos bastidores costuma trazer novos ouvintes.' ],
        [ 'icon' => '📲', 'text' => 'Partilha a tua música nos teus estados/stories — é grátis e funciona.' ],
        [ 'icon' => '🤝', 'text' => 'Procura um feature com um artista da tua área. Públicos somam-se.' ],
        [ 'icon' => '🎙️', 'text' => 'Grava um vídeo a explicar a história por trás de uma das tuas músicas.' ],
        [ 'icon' => '🔁', 'text' => 'Reaproveita um post antigo que funcionou — nem todos viram da 1ª vez.' ],
        [ 'icon' => '🗓️', 'text' => 'Define um dia fixo da semana para lançar conteúdo. Constância cria hábito.' ],
        [ 'icon' => '💬', 'text' => 'Responde aos comentários — proximidade transforma ouvintes em fãs.' ],
        [ 'icon' => '🔗', 'text' => 'Confirma que o link da tua música está fácil de encontrar na tua bio.' ],
        [ 'icon' => '🌟', 'text' => 'Convida 3 amigos para guardarem a tua música agora. Pequenos passos contam.' ],
        [ 'icon' => '🎧', 'text' => 'Ouve uma faixa tua como se fosses fã — o que destacarias numa publicação?' ],
        [ 'icon' => '📈', 'text' => 'Abre as tuas estatísticas e celebra um número que cresceu este mês.' ],
    ] );

    // Escolhe a 1ª dica AINDA NÃO mostrada; se todas já foram, escolhe por período/dia.
    $tip = null;
    foreach ( $tips as $t ) {
        if ( ! rmbz_coach_was_seen( $telemovel, $t['text'] ) ) { $tip = $t; break; }
    }
    if ( $tip === null ) {
        $idx = ( (int) current_time( 'z' ) + array_search( $segment, [ 'manha', 'tarde', 'noite' ], true ) ) % count( $tips );
        $tip = $tips[ $idx ];
    }

    // Mensagem de MOTIVAÇÃO conforme o período do dia.
    $rising = isset( $facts['streams']['delta'] ) && $facts['streams']['delta']['direction'] === 'up';
    $pool = [
        'manha' => [
            'Bom dia de criação! Um pequeno passo agora dá ritmo ao dia inteiro. ☀️',
            'A manhã é tua — começa com uma ação pela tua música. 🌅',
            'Hoje é mais um dia para te aproximares do teu objetivo. Vamos? 💪',
        ],
        'tarde' => [
            'Mantém o embalo da tarde — a tua arte merece consistência. 🔥',
            'Meio do dia, meio do caminho: continua a mover a tua música. 🚀',
            'Uma ação à tarde vale por muitas intenções. Avança. 💛',
        ],
        'noite' => [
            'Antes de descansar, deixa a tua música a trabalhar por ti. 🌙',
            'A noite é boa para refletir e preparar o próximo passo. ✨',
            'Fecha o dia com orgulho — amanhã constróis mais. 🌟',
        ],
    ];
    $msgs = $pool[ $segment ] ?? $pool['manha'];
    if ( $rising ) array_unshift( $msgs, 'As tuas reproduções estão a crescer — aproveita o embalo! 🔥' );
    $message = $msgs[ (int) current_time( 'z' ) % count( $msgs ) ];

    return [ 'greeting' => $greeting, 'message' => $message, 'tip' => $tip ];
}

/** [1.6.353] Coach por IA — usa TODOS os serviços, é MOTIVACIONAL, muda com o
 * período do dia e NUNCA repete dicas. Devolve ['message','tip'] ou null. */
function rmbz_coach_ai( array $facts, array $presence, $primeiro, $goal, $segment, $telemovel ) {
    $periodo = rmbz_coach_segment_label( $segment );
    $ctx = [
        'artista'          => $primeiro ?: null,
        'objetivo'         => $goal !== '' ? $goal : null,
        'periodo_do_dia'   => $periodo,
        'streams_total'    => (int) $facts['streams']['current'],
        'tendencia'        => $facts['streams']['delta']['direction'] ?? 'flat',
        'maior_plataforma' => $facts['top_platform']['store'] ?? null,
        'pais_top'         => $facts['top_country']['name'] ?? null,
        'faixa_top'        => $facts['top_track']['title'] ?? null,
        // [1.6.353] Presença em TODOS os serviços (não só streams):
        'lancamentos_lojas'    => $presence['releases'],
        'lancamentos_ao_vivo'  => $presence['releases_live'],
        'publicacoes_site'     => $presence['sub_site'],
        'envios_redes'         => $presence['sub_social'],
        'projetos_videoclipe'  => $presence['projects'],
    ];

    $angles = rmbz_coach_focus_angles();
    $angle = $angles[ ( (int) current_time( 'z' ) + array_search( $segment, [ 'manha', 'tarde', 'noite' ], true ) ) % count( $angles ) ];

    // Dicas recentes para a IA NÃO repetir (texto, não hash).
    $avoid = function_exists( 'rmbz_coach_recent_texts' ) ? rmbz_coach_recent_texts( $telemovel ) : '';

    $tom = $segment === 'manha'
        ? 'Dá energia para começar bem o dia.'
        : ( $segment === 'tarde' ? 'Mantém o embalo e o foco da tarde.' : 'Tom calmo: reflexão e preparar o próximo passo.' );

    $system = "És o COACH e motivador de carreira da 1MZ Records (Moçambique). Falas pt-PT, "
        . "breve e caloroso, como um manager honesto que ACREDITA no artista. A tua missão "
        . "principal é MOTIVAR — dá energia e uma ação concreta. Olha para TODA a presença do "
        . "artista (lançamentos nas lojas, publicações no site, envios para redes, projetos de "
        . "videoclipe), não só os streams. Adapta o tom ao PERÍODO DO DIA. Sê específico e "
        . "pessoal; usa só os dados do contexto, nunca inventes números. Responde só com JSON válido.";

    $instruction = "Escreve a mensagem de motivação de AGORA (período: {$periodo}) para este artista. "
        . "{$tom} Tem de ser fresca e DIFERENTE das anteriores.\n\n"
        . "CONTEXTO (toda a atividade):\n" . wp_json_encode( $ctx, JSON_UNESCAPED_UNICODE ) . "\n\n"
        . "FOCO SUGERIDO: " . $angle . "\n"
        . ( $avoid !== '' ? "NÃO repitas nem reformules nenhuma destas dicas já dadas:\n- " . $avoid . "\n\n" : "\n" )
        . "Dá uma dica/ação concreta e NOVA, ligada a um dos serviços do artista (ou ao foco) e ao "
        . "objetivo (se houver).\n\n"
        . "Devolve EXACTAMENTE: { \"message\": \"1 frase curta de motivação (máx 110 caracteres)\", "
        . "\"tip\": { \"icon\": \"emoji\", \"text\": \"1 ação concreta para agora (máx 110 caracteres)\" } }";

    $res = rmbz_ai_messages( $system, $instruction, [ 'max_tokens' => 320, 'temperature' => 0.9 ] );
    if ( empty( $res['ok'] ) ) return null;

    $data = rmbz_ai_extract_json( $res['text'] );
    if ( ! is_array( $data ) || empty( $data['message'] ) || empty( $data['tip']['text'] ) ) {
        return null;
    }
    return [
        'message' => (string) $data['message'],
        'tip'     => [
            'icon' => isset( $data['tip']['icon'] ) ? (string) $data['tip']['icon'] : '💡',
            'text' => (string) $data['tip']['text'],
        ],
    ];
}

/* ============================================================================
 * UTILITÁRIOS
 * ============================================================================ */

function rmbz_insights_period_label( $period ) {
    switch ( $period ) {
        case '7d':  return 'nos últimos 7 dias';
        case '30d': return 'nos últimos 30 dias';
        case '90d': return 'nos últimos 90 dias';
        case '12m': return 'nos últimos 12 meses';
        case 'all': return 'de sempre';
        default:    return 'no período';
    }
}

/** Variação como rótulo legível, escondendo-a em 'all'. */
function rmbz_insights_delta_label( array $delta, $is_all ) {
    if ( $is_all || $delta['direction'] === 'flat' ) return 'sem variação';
    $arrow = $delta['direction'] === 'up' ? '▲' : '▼';
    $sign  = $delta['direction'] === 'up' ? '+' : '−';
    return sprintf( '%s %s%s%% vs. anterior', $arrow, $sign, rmbz_insights_pct( $delta['pct'] ) );
}

function rmbz_insights_tone( array $delta, $is_all ) {
    if ( $is_all || $delta['direction'] === 'flat' ) return 'neutral';
    return $delta['direction'] === 'up' ? 'positive' : 'negative';
}

/** Formata percentagem sem casas decimais desnecessárias (32.0 → "32"). */
function rmbz_insights_pct( $pct ) {
    $pct = (float) $pct;
    return rtrim( rtrim( number_format( $pct, 1, '.', '' ), '0' ), '.' );
}

/** Número curto: 1234 → "1,2K", 1500000 → "1,5M". Vírgula decimal pt-PT. */
function rmbz_insights_short_num( $n ) {
    $n = (int) $n;
    if ( $n >= 1000000 ) return str_replace( '.', ',', (string) round( $n / 1000000, 1 ) ) . 'M';
    if ( $n >= 1000 )    return str_replace( '.', ',', (string) round( $n / 1000, 1 ) ) . 'K';
    return (string) $n;
}

/** Nome do país em pt-PT para os mercados mais comuns; fallback ao código. */
function rmbz_insights_country_name( $code ) {
    $code = strtoupper( trim( (string) $code ) );
    $map = [
        'MZ' => 'Moçambique', 'PT' => 'Portugal', 'BR' => 'Brasil',
        'AO' => 'Angola', 'ZA' => 'África do Sul', 'US' => 'Estados Unidos',
        'GB' => 'Reino Unido', 'FR' => 'França', 'DE' => 'Alemanha',
        'ES' => 'Espanha', 'NL' => 'Países Baixos', 'CV' => 'Cabo Verde',
        'GW' => 'Guiné-Bissau', 'ST' => 'São Tomé e Príncipe', 'TL' => 'Timor-Leste',
        'NG' => 'Nigéria', 'KE' => 'Quénia', 'GH' => 'Gana', 'TZ' => 'Tanzânia',
        'ZW' => 'Zimbabué', 'ZM' => 'Zâmbia', 'MW' => 'Malávi', 'SZ' => 'Essuatíni',
        'CA' => 'Canadá', 'IT' => 'Itália', 'BE' => 'Bélgica', 'CH' => 'Suíça',
        'SE' => 'Suécia', 'AU' => 'Austrália', 'IN' => 'Índia', 'JP' => 'Japão',
    ];
    if ( isset( $map[ $code ] ) ) return $map[ $code ];
    if ( $code === '' || $code === 'UNK' ) return 'Desconhecido';
    return $code;
}
