<?php
/**
 * 1MZ For Artists — Cliente da API de IA (Anthropic / Claude)
 *
 * Centraliza TODAS as chamadas ao Claude. A chave vive só no servidor:
 *   - Preferência: constante RMBZ_ANTHROPIC_API_KEY no wp-config.php
 *   - Alternativa: opção encriptada rmbz_ai_api_key (definida no admin)
 *
 * Nunca expõe a chave à app. As respostas são geradas no servidor e só o
 * texto final é devolvido pela REST API.
 *
 * @package 1MZ_For_Artists
 * @since 1.5.281
 */

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

if ( ! defined( 'RMBZ_AI_API_URL' ) ) {
    define( 'RMBZ_AI_API_URL', 'https://api.anthropic.com/v1/messages' );
}
if ( ! defined( 'RMBZ_AI_API_VERSION' ) ) {
    define( 'RMBZ_AI_API_VERSION', '2023-06-01' );
}
if ( ! defined( 'RMBZ_AI_DEFAULT_MODEL' ) ) {
    // Haiku 4.5 — rápido e barato, ideal para narrativa/insights.
    define( 'RMBZ_AI_DEFAULT_MODEL', 'claude-haiku-4-5-20251001' );
}

/* ============================================================================
 * CHAVE — leitura, escrita e encriptação
 * ============================================================================ */

/**
 * Devolve a chave da API em claro, ou '' se não estiver configurada.
 * Ordem: constante do wp-config → opção encriptada.
 */
function rmbz_ai_get_key() {
    if ( defined( 'RMBZ_ANTHROPIC_API_KEY' ) && RMBZ_ANTHROPIC_API_KEY ) {
        return (string) RMBZ_ANTHROPIC_API_KEY;
    }
    $stored = (string) get_option( 'rmbz_ai_api_key', '' );
    if ( $stored === '' ) return '';
    return rmbz_ai_decrypt( $stored );
}

/** Há chave + a feature está ligada? */
function rmbz_ai_available() {
    if ( ! (bool) get_option( 'rmbz_ai_enabled', 0 ) ) return false;
    return rmbz_ai_get_key() !== '';
}

/**
 * [1.6.301] As recomendações das listas (submissões: site/redes) devem ser
 * redigidas pela IA? Precisa da IA disponível E do interruptor próprio ligado.
 * Se desligado, a app usa textos pré-definidos (sem gastar tokens).
 */
function rmbz_ai_recommendations_enabled() {
    return rmbz_ai_available()
        && (bool) get_option( 'rmbz_ai_recommendations_enabled', 0 );
}

/* ============================================================================
 * [1.6.304] PERFIL DO ARTISTA PARA IA — objetivo declarado
 * ============================================================================
 * O objetivo principal do artista é a única coisa que pedimos (1 toque); o
 * resto do perfil é calculado dos dados da plataforma. Guardado em
 * wallet.extra_data['ai_goal'] (mesma fonte que vip_permissions).
 */

/** Objetivos válidos → frase para o prompt. */
function rmbz_ai_goal_label( $key ) {
    $map = [
        'money'     => 'ganhar dinheiro com a música',
        'listeners' => 'crescer o número de ouvintes',
        'viral'     => 'viralizar nas redes sociais',
        'video'     => 'ganhar um videoclipe profissional',
        'fans'      => 'construir uma base de fãs fiel',
    ];
    return isset( $map[ $key ] ) ? $map[ $key ] : '';
}

/** Lê o objetivo guardado (chave) ou '' se não definido. */
function rmbz_ai_get_goal( $telemovel ) {
    global $wpdb;
    $t = $wpdb->prefix . 'rmbz_wallets';
    $raw = $wpdb->get_var( $wpdb->prepare(
        "SELECT extra_data FROM {$t} WHERE user_telemovel = %s LIMIT 1", $telemovel
    ) );
    if ( ! $raw ) return '';
    $e = json_decode( $raw, true );
    return ( is_array( $e ) && ! empty( $e['ai_goal'] ) ) ? (string) $e['ai_goal'] : '';
}

/* ============================================================================
 * [1.6.310] USO DAS FERRAMENTAS DE IA (rate-limit diário) — para o admin ver
 * o que cada artista usou hoje e poder revogar o limite.
 * ============================================================================ */

/**
 * Mapa das ferramentas de IA: bucket → label + limite/dia.
 * NOTA: manter os limites em sincronia com rmbz_ai_tools_guard() (controller).
 */
function rmbz_ai_tool_limits() {
    return [
        'ai_report_chat_' => [ 'label' => 'Detalhes com IA',          'max' => 30 ],
        'ai_ask_'         => [ 'label' => 'Perguntar ao assistente',  'max' => 30 ],
        'ai_caption_'     => [ 'label' => 'Gerar legenda',            'max' => 15 ],
        'ai_playbook_'    => [ 'label' => 'Plano de lançamento',      'max' => 20 ],
        'ai_kit_'         => [ 'label' => 'Kit de divulgação',        'max' => 10 ],
        'ai_video_'       => [ 'label' => 'Conceito de vídeo',        'max' => 15 ],
    ];
}

/** Uso de hoje por ferramenta para um artista. */
function rmbz_ai_usage_for( $telemovel ) {
    global $wpdb;
    $table = $wpdb->prefix . 'rmbz_api_rate_limits';
    $out = [];
    foreach ( rmbz_ai_tool_limits() as $bucket => $info ) {
        $row = $wpdb->get_row( $wpdb->prepare(
            "SELECT hit_count, window_start FROM {$table} WHERE bucket_key = %s",
            $bucket . $telemovel
        ) );
        $used = 0; $reset_in = 0;
        if ( $row ) {
            $age = time() - strtotime( $row->window_start . ' UTC' );
            if ( $age < DAY_IN_SECONDS ) {
                $used     = (int) $row->hit_count;
                $reset_in = DAY_IN_SECONDS - $age;
            }
        }
        $out[] = [
            'label'     => $info['label'],
            'max'       => (int) $info['max'],
            'used'      => $used,
            'remaining' => max( 0, $info['max'] - $used ),
            'reached'   => $used >= $info['max'],
            'reset_in'  => $reset_in,
        ];
    }
    return $out;
}

/** Revoga (reinicia) os limites diários de IA de um artista. Devolve nº de buckets limpos. */
function rmbz_ai_reset_limits( $telemovel ) {
    global $wpdb;
    $table = $wpdb->prefix . 'rmbz_api_rate_limits';
    $n = 0;
    foreach ( rmbz_ai_tool_limits() as $bucket => $info ) {
        $n += (int) $wpdb->delete( $table, [ 'bucket_key' => $bucket . $telemovel ], [ '%s' ] );
    }
    return $n;
}

/** Guarda o objetivo (valida a chave). Devolve bool. */
function rmbz_ai_set_goal( $telemovel, $goal ) {
    if ( rmbz_ai_goal_label( $goal ) === '' ) return false; // chave inválida
    global $wpdb;
    $t = $wpdb->prefix . 'rmbz_wallets';
    $row = $wpdb->get_row( $wpdb->prepare(
        "SELECT extra_data FROM {$t} WHERE user_telemovel = %s LIMIT 1", $telemovel
    ) );
    if ( ! $row ) return false;
    $e = ! empty( $row->extra_data ) ? json_decode( $row->extra_data, true ) : [];
    if ( ! is_array( $e ) ) $e = [];
    $e['ai_goal'] = $goal;
    $wpdb->update( $t, [ 'extra_data' => wp_json_encode( $e ) ], [ 'user_telemovel' => $telemovel ], [ '%s' ], [ '%s' ] );
    delete_transient( 'rmbz_ai_profile_v2_' . md5( $telemovel ) ); // invalida cache do perfil
    return true;
}

/* ============================================================================
 * [1.6.313] MEMÓRIA DE CONVERSA PERSISTENTE (servidor, com LIMITE)
 * ============================================================================
 * O assistente "Perguntar" lembra-se do artista ao longo do tempo. Guardamos
 * os turnos em wallet.extra_data['ai_chat_memory'] (cross-device), CAPADO aos
 * últimos RMBZ_AI_MEMORY_MAX. A app guarda TUDO localmente; quando o servidor
 * enche, o excedente fica só no telemóvel (o servidor mantém os mais recentes).
 */
if ( ! defined( 'RMBZ_AI_MEMORY_MAX' ) ) {
    define( 'RMBZ_AI_MEMORY_MAX', 30 ); // limite de turnos guardados no servidor
}

/** Lê a memória de conversa (array de [q,a,t]) do artista. */
function rmbz_ai_get_chat_memory( $telemovel ) {
    global $wpdb;
    $t = $wpdb->prefix . 'rmbz_wallets';
    $raw = $wpdb->get_var( $wpdb->prepare(
        "SELECT extra_data FROM {$t} WHERE user_telemovel = %s LIMIT 1", $telemovel
    ) );
    if ( ! $raw ) return [];
    $e = json_decode( $raw, true );
    if ( ! is_array( $e ) || empty( $e['ai_chat_memory'] ) || ! is_array( $e['ai_chat_memory'] ) ) {
        return [];
    }
    return $e['ai_chat_memory'];
}

/** Acrescenta um turno (pergunta + resposta) e mantém só os últimos N. */
function rmbz_ai_append_chat_memory( $telemovel, $q, $a ) {
    $q = trim( (string) $q );
    $a = trim( (string) $a );
    if ( $q === '' || $a === '' ) return;
    global $wpdb;
    $t = $wpdb->prefix . 'rmbz_wallets';
    $row = $wpdb->get_row( $wpdb->prepare(
        "SELECT extra_data FROM {$t} WHERE user_telemovel = %s LIMIT 1", $telemovel
    ) );
    if ( ! $row ) return;
    $e = ! empty( $row->extra_data ) ? json_decode( $row->extra_data, true ) : [];
    if ( ! is_array( $e ) ) $e = [];
    $mem = ( isset( $e['ai_chat_memory'] ) && is_array( $e['ai_chat_memory'] ) ) ? $e['ai_chat_memory'] : [];
    // Limita o tamanho de cada turno guardado (controla o tamanho da BD).
    if ( function_exists( 'mb_substr' ) ) {
        if ( mb_strlen( $q ) > 300 ) $q = mb_substr( $q, 0, 300 );
        if ( mb_strlen( $a ) > 900 ) $a = mb_substr( $a, 0, 900 );
    }
    $mem[] = [ 'q' => $q, 'a' => $a, 't' => time() ];
    if ( count( $mem ) > RMBZ_AI_MEMORY_MAX ) {
        $mem = array_slice( $mem, -RMBZ_AI_MEMORY_MAX ); // mantém os mais recentes
    }
    $e['ai_chat_memory'] = $mem;
    $wpdb->update( $t, [ 'extra_data' => wp_json_encode( $e ) ], [ 'user_telemovel' => $telemovel ], [ '%s' ], [ '%s' ] );
}

/** Apaga a memória de conversa do artista. */
function rmbz_ai_clear_chat_memory( $telemovel ) {
    global $wpdb;
    $t = $wpdb->prefix . 'rmbz_wallets';
    $row = $wpdb->get_row( $wpdb->prepare(
        "SELECT extra_data FROM {$t} WHERE user_telemovel = %s LIMIT 1", $telemovel
    ) );
    if ( ! $row ) return;
    $e = ! empty( $row->extra_data ) ? json_decode( $row->extra_data, true ) : [];
    if ( ! is_array( $e ) ) $e = [];
    unset( $e['ai_chat_memory'] );
    $wpdb->update( $t, [ 'extra_data' => wp_json_encode( $e ) ], [ 'user_telemovel' => $telemovel ], [ '%s' ], [ '%s' ] );
}

/** Modelo configurado (com fallback) — usado na narrativa/insights/coach. */
function rmbz_ai_model() {
    $m = trim( (string) get_option( 'rmbz_ai_model', '' ) );
    return $m !== '' ? $m : RMBZ_AI_DEFAULT_MODEL;
}

/**
 * [1.6.309] Modelo para os CHATS de conversa (Perguntar, Detalhes com IA).
 * Aqui a qualidade do RACIOCÍNIO importa muito mais do que o custo, por isso o
 * default é o Sonnet (raciocina melhor, adapta-se à conversa). Editável no
 * admin via `rmbz_ai_chat_model`. Os recursos baratos (insights/coach) ficam
 * no modelo normal (Haiku).
 */
function rmbz_ai_chat_model() {
    $m = trim( (string) get_option( 'rmbz_ai_chat_model', '' ) );
    // [1.6.313] Default Opus 4.8 — o mais inteligente, para os chats raciocinarem
    // e recomendarem ações. Editável no admin.
    return $m !== '' ? $m : 'claude-opus-4-8';
}

/**
 * O artista tem direito às ferramentas geradoras de IA (legendas, kit, plano,
 * perguntas)? Os insights e o coach são para todos com dados.
 *
 * [1.5.291] REGRA: precisa de subscrição ACTIVA **e** do add-on de IA
 * (`vip_permissions.ai`), que é escolhido na compra do plano. SEM
 * grandfathering — quem não marcou a IA na compra não tem acesso até renovar
 * e voltar a marcar.
 *
 * Usa a MESMA fonte que o endpoint /subscription/status (e o cliente):
 *   - rmbz_user_has_active_subscription()  → is_active
 *   - rmbz_api_get_user_vip_permissions()  → permissions.ai
 */
function rmbz_ai_user_is_vip( $telemovel ) {
    $active = function_exists( 'rmbz_user_has_active_subscription' )
        && rmbz_user_has_active_subscription( $telemovel );
    if ( ! $active ) return false;

    if ( function_exists( 'rmbz_api_get_user_vip_permissions' ) ) {
        $perms = rmbz_api_get_user_vip_permissions( $telemovel );
        return ! empty( $perms['ai'] );
    }
    return false;
}

/** Chave derivada das salts do WordPress para encriptar a API key. */
function rmbz_ai_secret() {
    $base = ( defined( 'AUTH_KEY' ) ? AUTH_KEY : '' )
          . ( defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : '' )
          . ( defined( 'LOGGED_IN_KEY' ) ? LOGGED_IN_KEY : '' );
    if ( $base === '' ) $base = 'rmbz-fallback-' . DB_NAME; // melhor que nada
    return hash( 'sha256', 'rmbz-ai|' . $base, true ); // 32 bytes
}

/**
 * Encripta a chave para guardar na BD. Prefixo "enc:" identifica o formato.
 * Fallback (sem openssl) marca "b64:" — pelo menos não fica em claro óbvio.
 */
function rmbz_ai_encrypt( $plain ) {
    $plain = (string) $plain;
    if ( $plain === '' ) return '';
    if ( function_exists( 'openssl_encrypt' ) ) {
        $iv  = random_bytes( 16 );
        $ct  = openssl_encrypt( $plain, 'aes-256-cbc', rmbz_ai_secret(), OPENSSL_RAW_DATA, $iv );
        if ( $ct !== false ) {
            return 'enc:' . base64_encode( $iv . $ct );
        }
    }
    return 'b64:' . base64_encode( $plain );
}

/** Desencripta a chave guardada. */
function rmbz_ai_decrypt( $stored ) {
    $stored = (string) $stored;
    if ( strpos( $stored, 'enc:' ) === 0 && function_exists( 'openssl_decrypt' ) ) {
        $raw = base64_decode( substr( $stored, 4 ), true );
        if ( $raw === false || strlen( $raw ) <= 16 ) return '';
        $iv = substr( $raw, 0, 16 );
        $ct = substr( $raw, 16 );
        $pt = openssl_decrypt( $ct, 'aes-256-cbc', rmbz_ai_secret(), OPENSSL_RAW_DATA, $iv );
        return $pt === false ? '' : $pt;
    }
    if ( strpos( $stored, 'b64:' ) === 0 ) {
        return (string) base64_decode( substr( $stored, 4 ), true );
    }
    // Compat: valor antigo guardado em claro.
    return $stored;
}

/**
 * Sanitize callback para o campo da chave no admin.
 *
 * Truque importante: o campo mostra um placeholder mascarado (••••) em vez da
 * chave real. Se o admin guardar sem tocar no campo, recebemos o mascarado —
 * nesse caso mantemos a chave já guardada (não a apagamos por engano). Só
 * substituímos quando vem uma chave nova de verdade.
 */
function rmbz_ai_sanitize_key( $value ) {
    $value = trim( (string) wp_unslash( $value ) );

    // [1.6.334] Campo vazio → MANTER a chave atual. O campo é um `password`
    // com value="" (os pontos são placeholder), por isso guardar QUALQUER outra
    // definição submetia a chave vazia e apagava-a. A UI diz "deixa em branco
    // para a manter" — não a apagamos nunca por um save acidental.
    if ( $value === '' ) return (string) get_option( 'rmbz_ai_api_key', '' );

    // Mantém o valor actual se o admin não mexeu (placeholder mascarado).
    if ( strpos( $value, '•' ) !== false || $value === rmbz_ai_masked_key() ) {
        return (string) get_option( 'rmbz_ai_api_key', '' );
    }

    return rmbz_ai_encrypt( $value );
}

/** Versão mascarada da chave, para mostrar no admin sem revelar o segredo. */
function rmbz_ai_masked_key() {
    $key = rmbz_ai_get_key();
    if ( $key === '' ) return '';
    $tail = substr( $key, -4 );
    return '••••••••••••' . $tail;
}

/**
 * [1.6.314] O modelo aceita o parâmetro `temperature`? O Opus 4.8 deprecou-o
 * (a API devolve erro se for enviado). Mantemos uma lista de prefixos que NÃO
 * o suportam — fácil de alargar a futuros modelos.
 */
function rmbz_ai_model_supports_temperature( $model ) {
    $model = strtolower( (string) $model );
    $no_temperature = [ 'claude-opus-4-8' ];
    foreach ( $no_temperature as $prefix ) {
        if ( strpos( $model, $prefix ) !== false ) return false;
    }
    return true;
}

/* ============================================================================
 * CHAMADA AO MODELO
 * ============================================================================ */

/**
 * Envia uma mensagem ao Claude e devolve o texto da resposta.
 *
 * @param string $system       Instrução de sistema (papel/regras).
 * @param string $user_content Conteúdo do utilizador (o pedido + dados).
 * @param array  $opts         max_tokens, temperature, model (opcionais).
 * @return array { ok: bool, text: string, error: string|null }
 */
function rmbz_ai_messages( $system, $user_content, array $opts = [] ) {
    $key = rmbz_ai_get_key();
    if ( $key === '' ) {
        return [ 'ok' => false, 'text' => '', 'error' => 'no_key' ];
    }

    $model = isset( $opts['model'] ) ? (string) $opts['model'] : rmbz_ai_model();

    $body = [
        'model'      => $model,
        'max_tokens' => isset( $opts['max_tokens'] ) ? (int) $opts['max_tokens'] : 600,
        'system'     => (string) $system,
        'messages'   => [
            [ 'role' => 'user', 'content' => (string) $user_content ],
        ],
    ];
    // [1.6.314] O Opus 4.8 DEPRECOU o parâmetro `temperature` (a API rejeita o
    // pedido). Só enviamos temperature em modelos que o suportam.
    if ( isset( $opts['temperature'] ) && rmbz_ai_model_supports_temperature( $model ) ) {
        $body['temperature'] = (float) $opts['temperature'];
    }

    $resp = wp_remote_post( RMBZ_AI_API_URL, [
        'timeout' => isset( $opts['timeout'] ) ? (int) $opts['timeout'] : 25,
        'headers' => [
            'content-type'      => 'application/json',
            'x-api-key'         => $key,
            'anthropic-version' => RMBZ_AI_API_VERSION,
        ],
        'body' => wp_json_encode( $body ),
    ] );

    if ( is_wp_error( $resp ) ) {
        return [ 'ok' => false, 'text' => '', 'error' => $resp->get_error_message() ];
    }

    $code = (int) wp_remote_retrieve_response_code( $resp );
    $raw  = wp_remote_retrieve_body( $resp );
    $json = json_decode( $raw, true );

    if ( $code !== 200 ) {
        $msg = is_array( $json ) && isset( $json['error']['message'] )
            ? $json['error']['message']
            : ( 'HTTP ' . $code );
        return [ 'ok' => false, 'text' => '', 'error' => $msg ];
    }

    // Resposta: { content: [ { type:"text", text:"..." }, ... ] }
    $text = '';
    if ( is_array( $json ) && ! empty( $json['content'] ) && is_array( $json['content'] ) ) {
        foreach ( $json['content'] as $block ) {
            if ( isset( $block['type'], $block['text'] ) && $block['type'] === 'text' ) {
                $text .= $block['text'];
            }
        }
    }

    if ( trim( $text ) === '' ) {
        return [ 'ok' => false, 'text' => '', 'error' => 'empty_response' ];
    }

    return [ 'ok' => true, 'text' => $text, 'error' => null ];
}

/**
 * Extrai um objecto JSON da resposta do modelo, tolerando ```json fences```
 * ou texto à volta. Devolve array associativo ou null.
 */
function rmbz_ai_extract_json( $text ) {
    $text = (string) $text;

    // Remove cercas de código.
    $text = preg_replace( '/```(?:json)?/i', '', $text );

    // Apanha do primeiro { ao último }.
    $start = strpos( $text, '{' );
    $end   = strrpos( $text, '}' );
    if ( $start === false || $end === false || $end <= $start ) return null;

    $candidate = substr( $text, $start, $end - $start + 1 );
    $data = json_decode( $candidate, true );
    return is_array( $data ) ? $data : null;
}
