<?php
declare(strict_types=1);

error_reporting(E_ALL);
ini_set('display_errors','1');
ini_set('log_errors','1');
ini_set('error_log', __DIR__ . '/../logs/php_errors.log');

/**
 * inc/spin_lib.php
 * Lógica de sorteio, padrão L/W, logging e ACK.
 * REVISADO E CORRIGIDO - Versão 2.0
 */

$__db_bootstrap = __DIR__ . '/../db/config.php';
if (is_file($__db_bootstrap)) {
  require_once $__db_bootstrap;
}

/** @var PDO|null $pdo */
if (!isset($pdo) || !($pdo instanceof PDO)) {
  if (function_exists('db')) $pdo = db();
}
if (!isset($pdo) || !($pdo instanceof PDO)) {
  if (class_exists('DB') && method_exists('DB','pdo')) $pdo = DB::pdo();
}
if (!isset($pdo) || !($pdo instanceof PDO)) {
  throw new RuntimeException('PDO indisponível em inc/spin_lib.php');
}

/* ---------- Helpers de cliente ---------- */
function client_ip(): string {
  foreach (['HTTP_CF_CONNECTING_IP','HTTP_X_FORWARDED_FOR','HTTP_X_REAL_IP','REMOTE_ADDR'] as $h) {
    if (!empty($_SERVER[$h])) { $v = trim(explode(',', $_SERVER[$h])[0]); if ($v) return $v; }
  }
  return '';
}

function ip_hash(string $ip): string {
  if (!$ip) return '';
  return substr(hash('sha256', $ip.'|'.($_SERVER['HTTP_USER_AGENT']??'')),0,32);
}

/* ---------- Carregar roleta/segmentos ---------- */
function getRoulette(PDO $pdo, string $idOrSlug) {
  if (ctype_digit($idOrSlug)) {
    $st=$pdo->prepare("SELECT * FROM roulettes WHERE id=? AND is_active=1");
    $st->execute([$idOrSlug]);
  } else {
    $st=$pdo->prepare("SELECT * FROM roulettes WHERE slug=? AND is_active=1");
    $st->execute([$idOrSlug]);
  }
  return $st->fetch(PDO::FETCH_ASSOC);
}

function getSegments(PDO $pdo, int $rid): array {
  $st=$pdo->prepare("SELECT * FROM roulette_segments WHERE roulette_id=? ORDER BY segment_order ASC, id ASC");
  $st->execute([$rid]);
  $rows=$st->fetchAll(PDO::FETCH_ASSOC);
  foreach ($rows as &$r) {
    $r['type'] = strtoupper((string)($r['type'] ?? $r['kind'] ?? 'LOSE'));
    $r['weight'] = (float)($r['weight'] ?? 0);
  }
  unset($r);
  return $rows;
}

/* ---------- Rate limit mais tolerante ---------- */
function rateLimit(PDO $pdo, int $rid, string $ipHash): array {
  $win = 12; // 12 segundos (mais tolerante)
  $max = 3;  // Permite 3 spins em 12 segundos
  
  $st = $pdo->prepare("SELECT COUNT(*) FROM spin_logs 
                      WHERE roulette_id=? 
                      AND created_at >= DATE_SUB(NOW(), INTERVAL ? SECOND) 
                      AND ip_hash = ?");
  $st->execute([$rid, $win, $ipHash]);
  $count = (int)$st->fetchColumn();
  
  if ($count >= $max) {
    return [
      'ok' => false, 
      'code' => 'rate_limited', 
      'retry_after' => $win,
      'message' => 'Muitas tentativas seguidas. Aguarde ' . $win . ' segundos.'
    ];
  }
  
  return ['ok' => true];
}

/* ---------- ✅ CORREÇÃO: Conta TODOS os spins, não apenas ACKs ---------- */
function ackCount(PDO $pdo, int $rid): int {
  $st = $pdo->prepare("SELECT COUNT(*) FROM spin_logs WHERE roulette_id = ?");
  $st->execute([$rid]);
  return (int)$st->fetchColumn();
}

/* ---------- Núcleo: sorteio ponderado com peso<=1=0 ---------- */
function weightedPick(array $segments, array &$dbg, string $reason): array {
  // NOVA LÓGICA: pesos <= 1 são ignorados (efetivo = 0)
  $norm = []; 
  $total = 0;
  $maxW = -INF;
  $maxList = [];
  
  foreach ($segments as $s) {
    $w = (float)($s['weight'] ?? 0);
    $eff = ($w <= 1) ? 0.0 : $w; // PESOS <= 1 SÃO IGNORADOS
    
    $norm[] = ['ref' => $s, 'w' => $w, 'eff' => $eff];
    $total += $eff;
    
    // Registra peso máximo para fallback
    if ($w > $maxW) {
      $maxW = $w;
      $maxList = [$s];
    } elseif ($w == $maxW) {
      $maxList[] = $s;
    }
  }

  $dbg['reason'] = $reason;
  $dbg['total_eff_weight'] = $total;
  $dbg['max_absolute_weight'] = $maxW;

  // Se há pesos efetivos > 0, usa sorteio ponderado
  if ($total > 0) {
    $r = random_int(1, (int)round($total * 1000));
    foreach ($norm as $n) {
      $r -= (int)round($n['eff'] * 1000);
      if ($r <= 0) {
        $dbg['fallback'] = null;
        $dbg['chosen_weight'] = $n['w'];
        $dbg['chosen_eff_weight'] = $n['eff'];
        return $n['ref'];
      }
    }
    $dbg['fallback'] = 'loop_guard';
    return end($norm)['ref'];
  }

  // FALLBACK: todos os pesos efetivos são 0 → escolhe aleatoriamente entre os de maior peso absoluto
  $dbg['fallback'] = 'total_eff_zero_pick_max_abs';
  $dbg['max_candidates'] = count($maxList);
  return $maxList[random_int(0, count($maxList) - 1)];
}

/* ---------- Aux: WIN de maior peso ---------- */
function pickWinMax(array $wins, array &$dbg, string $reason): array {
  if (!$wins) { 
    $dbg['reason'] = $reason; 
    $dbg['fallback'] = 'no_win'; 
    return []; 
  }
  
  $max = -INF; 
  $cand = [];
  foreach ($wins as $w) {
    $ww = (float)($w['weight'] ?? 0);
    if ($ww > $max) { 
      $max = $ww; 
      $cand = [$w]; 
    } elseif ($ww == $max) { 
      $cand[] = $w; 
    }
  }
  
  $dbg['reason'] = $reason;
  $dbg['max_win_weight'] = $max;
  $dbg['max_win_candidates'] = count($cand);
  
  return $cand[random_int(0, count($cand) - 1)];
}

/* ---------- Padrão L/W + sorteio ---------- */
function pickWithPattern(PDO $pdo, int $rid, string $pattern, array $segments, array &$debug): array {
  $debug['pattern_raw'] = $pattern;
  $pat = strtoupper(preg_replace('/[^LW]/', '', (string)$pattern));
  
  // ✅✅✅ Se pattern for 'none' ou vazio, sorteio normal
  if ($pat === '' || $pat === 'NONE') {
    $sub = [];
    $seg = weightedPick($segments, $sub, 'normal_weighted');
    $seg['__forced'] = '';
    return $seg;
  }

  $debug['pattern'] = $pat;

  // ✅✅✅ CORREÇÃO: Usar ACK COUNT (total de spins) para sequência infinita
  $acks = ackCount($pdo, $rid);
  $debug['ack_count'] = $acks;

  // ✅✅✅ CALCULO CORRETO: Considera ACK COUNT 0-based para sequência infinita
  $pattern_length = strlen($pat);
  $step_index = $acks % $pattern_length;
  $step = $pat[$step_index];
  
  $debug['step'] = $step;
  $debug['step_index'] = $step_index;
  $debug['pattern_length'] = $pattern_length;
  $debug['calculation'] = "$acks % $pattern_length = $step_index → '$step'";

  // prepara subsets
  $wins  = array_values(array_filter($segments, fn($s) => ($s['type'] === 'WIN')));
  $loses = array_values(array_filter($segments, fn($s) => ($s['type'] !== 'WIN')));

  // info p/ debug
  $debug['segments'] = array_map(function($s){
    return [
      'id'     => (int)($s['id'] ?? 0),
      'type'   => $s['type'],
      'weight' => (float)($s['weight'] ?? 0),
      'peso_efetivo' => ((float)($s['weight'] ?? 0) <= 1) ? 0 : (float)$s['weight'],
    ];
  }, $segments);

  if ($step === 'W') {
    $sub = [];
    $seg = pickWinMax($wins, $sub, 'forced_W_max_win_weight');
    if (!$seg) { 
      $sub2 = [];
      $seg = weightedPick($segments, $sub2, 'forced_W_fallback_weighted');
      $debug['win_fallback_debug'] = $sub2;
    }
    $debug['win_pick_debug'] = $sub;
    $seg['__forced'] = 'W';
    return $seg;
  }

  // L
  if ($loses) {
    $sub = [];
    $seg = weightedPick($loses, $sub, 'forced_L_weighted_loses');
    $debug['lose_pick_debug'] = $sub;
    $seg['__forced'] = 'L';
    return $seg;
  }

  // sem LOSE → fallback
  $sub = [];
  $seg = weightedPick($segments, $sub, 'forced_L_fallback_weighted');
  $debug['lose_fallback_debug'] = $sub;
  $seg['__forced'] = 'L';
  return $seg;
}

/* ---------- HMAC / log / ACK ---------- */
function hmac_for(int $rid, int $sid, string $nonce): string {
  return hash_hmac('sha256', "$rid:$sid:$nonce", 'roulette-secret-key');
}

function logIssue(PDO $pdo, int $rid, int $sid, string $ipHash, string $ua, string $nonce, string $hmac): int {
  $st = $pdo->prepare("INSERT INTO spin_logs(roulette_id,segment_id,ip_hash,user_agent,nonce,hmac,created_at,status)
                     VALUES (?,?,?,?,?,?,NOW(),'pending')");
  $st->execute([$rid, $sid, $ipHash, $ua, $nonce, $hmac]);
  return (int)$pdo->lastInsertId();
}

function ackSpin(PDO $pdo, int $spinId, string $nonce, string $hmac): bool {
  $st = $pdo->prepare("SELECT nonce,hmac,created_at FROM spin_logs WHERE id=?");
  $st->execute([$spinId]); 
  $r = $st->fetch(PDO::FETCH_ASSOC);
  
  if (!$r) return false;
  if (!hash_equals((string)$r['nonce'], (string)$nonce)) return false;
  if (!hash_equals((string)$r['hmac'],  (string)$hmac )) return false;
  
  $dur = 0;
  if (!empty($r['created_at'])) {
    $ts = strtotime((string)$r['created_at']); 
    if ($ts) $dur = max(0, (int)round((microtime(true) - $ts) * 1000));
  }
  
  $up = $pdo->prepare("UPDATE spin_logs SET status='ack', ack_at=NOW(), duration_ms=? WHERE id=?");
  return $up->execute([$dur, $spinId]);
}