<?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, ACK e sistema de segurança completo.
 * REVISADO E CORRIGIDO - Versão 3.0
 * ATUALIZADO: Sistema de limite total de giros por usuário
 */

$__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;
}

/* ===== SISTEMA DE SEGURANÇA COMPLETO ===== */

/**
 * Verifica se IP está bloqueado
 */
function isIpBlocked(PDO $pdo, string $ipHash, int $rouletteId = null): bool {
  $sql = "SELECT * FROM blocked_ips 
          WHERE ip_hash = ? 
          AND (roulette_id IS NULL OR roulette_id = ?)
          AND (permanent_block = 1 OR blocked_until > NOW())";
  
  $st = $pdo->prepare($sql);
  $st->execute([$ipHash, $rouletteId]);
  
  return (bool)$st->fetch();
}

/**
 * Obtém limites de segurança da roleta
 */
function getRouletteLimits(PDO $pdo, int $rouletteId): array {
  $st = $pdo->prepare("SELECT * FROM roulette_limits WHERE roulette_id = ? AND enabled = 1");
  $st->execute([$rouletteId]);
  $limits = $st->fetch(PDO::FETCH_ASSOC);
  
  // Se não há configuração específica, retorna padrão desabilitado
  if (!$limits) {
    return [
      'enabled' => false,
      'max_spins_per_hour' => 100,
      'max_spins_per_day' => 500,
      'max_spins_per_session' => 50,
      'max_total_spins' => null,
      'total_spins_period' => null,
      'permanent_block_on_total' => true,
      'cooldown_minutes' => 5
    ];
  }
  
  return $limits;
}

/**
 * Verifica limite total de giros por período
 */
function checkTotalSpinLimit(PDO $pdo, int $rouletteId, string $ipHash): array {
    $limits = getRouletteLimits($pdo, $rouletteId);
    
    // Se não há limite total definido, retorna ok
    if (empty($limits['max_total_spins']) || !$limits['enabled']) {
        return ['ok' => true];
    }
    
    $maxTotal = (int)$limits['max_total_spins'];
    $period = $limits['total_spins_period'] ?? 'forever';
    $permanentBlock = (bool)($limits['permanent_block_on_total'] ?? true);
    
    // Calcula o início do período
    $periodStart = null;
    switch ($period) {
        case 'day':
            $periodStart = date('Y-m-d 00:00:00');
            break;
        case 'week':
            $periodStart = date('Y-m-d 00:00:00', strtotime('monday this week'));
            break;
        case 'month':
            $periodStart = date('Y-m-01 00:00:00');
            break;
        case 'forever':
        default:
            // Sem período - conta todos os giros desde o início
            $periodStart = null;
            break;
    }
    
    // Conta giros no período
    $sql = "SELECT COUNT(*) FROM spin_logs 
            WHERE roulette_id = ? 
            AND ip_hash = ?";
    $params = [$rouletteId, $ipHash];
    
    if ($periodStart) {
        $sql .= " AND created_at >= ?";
        $params[] = $periodStart;
    }
    
    $st = $pdo->prepare($sql);
    $st->execute($params);
    $countTotal = (int)$st->fetchColumn();
    
    if ($countTotal >= $maxTotal) {
        // Atingiu o limite total - bloqueia o usuário
        $blockReason = "Limite total de {$maxTotal} giros excedido";
        $blockUntil = null;
        
        // Se não for bloqueio permanente, calcula o fim do período
        if (!$permanentBlock && $periodStart) {
            switch ($period) {
                case 'day':
                    $blockUntil = date('Y-m-d 23:59:59');
                    break;
                case 'week':
                    $blockUntil = date('Y-m-d 23:59:59', strtotime('sunday this week'));
                    break;
                case 'month':
                    $blockUntil = date('Y-m-t 23:59:59');
                    break;
            }
        }
        
        // Insere na tabela blocked_ips
        $st = $pdo->prepare("INSERT INTO blocked_ips (ip_hash, roulette_id, reason, blocked_until, permanent_block) 
                            VALUES (?, ?, ?, ?, ?)");
        $st->execute([
            $ipHash, 
            $rouletteId, 
            $blockReason, 
            $blockUntil,
            $permanentBlock ? 1 : 0
        ]);
        
        return [
            'ok' => false,
            'code' => 'total_limit_exceeded',
            'limit_type' => 'total',
            'current_count' => $countTotal,
            'max_limit' => $maxTotal,
            'period' => $period,
            'retry_after' => $blockUntil ? (strtotime($blockUntil) - time()) : null,
            'message' => "Você atingiu o limite máximo de {$maxTotal} giros " . 
                        ($period === 'forever' ? 'definitivo' : "por {$period}") . 
                        ". Acesso bloqueado" . ($permanentBlock ? ' permanentemente' : ' até o final do período') . '.'
        ];
    }
    
    return ['ok' => true];
}

/**
 * Verifica limites do usuário (hora, dia, sessão, total)
 */
function checkUserLimits(PDO $pdo, int $rouletteId, string $ipHash): array {
  $limits = getRouletteLimits($pdo, $rouletteId);
  
  // Se segurança desabilitada, retorna ok
  if (!$limits['enabled']) {
      return ['ok' => true];
  }
  
  $now = time();
  $hour_ago = $now - 3600;
  $day_ago = $now - 86400;
  $session_start = $now - 3600; // Sessão de 1 hora

  // Contagem por hora
  $st = $pdo->prepare("SELECT COUNT(*) FROM spin_logs 
                      WHERE roulette_id = ? 
                      AND ip_hash = ? 
                      AND created_at >= FROM_UNIXTIME(?)");
  $st->execute([$rouletteId, $ipHash, $hour_ago]);
  $count_hour = (int)$st->fetchColumn();

  // Contagem por dia
  $st->execute([$rouletteId, $ipHash, $day_ago]);
  $count_day = (int)$st->fetchColumn();

  // Contagem por sessão
  $st->execute([$rouletteId, $ipHash, $session_start]);
  $count_session = (int)$st->fetchColumn();

  // Verifica limites
  if ($count_hour >= $limits['max_spins_per_hour']) {
    $next_reset = $hour_ago + 3600;
    return [
      'ok' => false,
      'code' => 'hourly_limit_exceeded',
      'limit_type' => 'hora',
      'current_count' => $count_hour,
      'max_limit' => $limits['max_spins_per_hour'],
      'retry_after' => $next_reset - $now,
      'message' => 'Limite horário excedido'
    ];
  }
  
  if ($count_day >= $limits['max_spins_per_day']) {
    $next_reset = $day_ago + 86400;
    return [
      'ok' => false,
      'code' => 'daily_limit_exceeded',
      'limit_type' => 'dia',
      'current_count' => $count_day,
      'max_limit' => $limits['max_spins_per_day'],
      'retry_after' => $next_reset - $now,
      'message' => 'Limite diário excedido'
    ];
  }
  
  if ($count_session >= $limits['max_spins_per_session']) {
    // Bloqueia temporariamente
    $blockUntil = date('Y-m-d H:i:s', $now + ($limits['cooldown_minutes'] * 60));
    $st = $pdo->prepare("INSERT INTO blocked_ips (ip_hash, roulette_id, reason, blocked_until) 
                        VALUES (?, ?, ?, ?)");
    $st->execute([$ipHash, $rouletteId, 'Limite de sessão excedido', $blockUntil]);
    
    return [
      'ok' => false,
      'code' => 'session_limit_exceeded',
      'limit_type' => 'sessão',
      'current_count' => $count_session,
      'max_limit' => $limits['max_spins_per_session'],
      'retry_after' => $limits['cooldown_minutes'] * 60,
      'message' => 'Limite de sessão excedido'
    ];
  }

  // ===== NOVA VERIFICAÇÃO: LIMITE TOTAL =====
  $totalLimitCheck = checkTotalSpinLimit($pdo, $rouletteId, $ipHash);
  if (!$totalLimitCheck['ok']) {
    error_log("❌ Limite total excedido: " . $totalLimitCheck['code']);
    return $totalLimitCheck;
  }
  
  return ['ok' => true];
}

/**
 * Detecta padrões suspeitos de uso
 */
function detectSuspiciousPattern(PDO $pdo, string $ipHash, int $rouletteId): array {
  // Verifica spins muito rápidos
  $st = $pdo->prepare("SELECT COUNT(*) as rapid_spins 
                      FROM spin_logs 
                      WHERE roulette_id = ? 
                      AND ip_hash = ? 
                      AND created_at >= DATE_SUB(NOW(), INTERVAL 10 SECOND)");
  $st->execute([$rouletteId, $ipHash]);
  $rapidSpins = (int)$st->fetchColumn();
  
  // Verifica múltiplos ACKs falhos
  $st = $pdo->prepare("SELECT COUNT(*) as failed_acks 
                      FROM spin_logs 
                      WHERE roulette_id = ? 
                      AND ip_hash = ? 
                      AND status = 'pending' 
                      AND created_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE)");
  $st->execute([$rouletteId, $ipHash]);
  $failedAcks = (int)$st->fetchColumn();
  
  $suspicious = false;
  $reasons = [];
  
  if ($rapidSpins > 5) {
    $suspicious = true;
    $reasons[] = "Muitos spins em curto período: $rapidSpins em 10 segundos";
  }
  
  if ($failedAcks > 10) {
    $suspicious = true;
    $reasons[] = "Muitos ACKs pendentes: $failedAcks em 1 minuto";
  }
  
  return [
    'suspicious' => $suspicious,
    'reasons' => $reasons,
    'rapid_spins' => $rapidSpins,
    'failed_acks' => $failedAcks
  ];
}

/**
 * Rate limit rápido (mantido do sistema anterior)
 */
function rateLimit(PDO $pdo, int $rid, string $ipHash): array {
  $win = 12; // 12 segundos
  $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];
}

/* ---------- 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)];
}

/* ---------- ✅ CORREÇÃO: Padrão L/W + sorteio com índice consistente ---------- */
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'] = '';
    
    // ✅ CORREÇÃO: Garantir índice baseado na ordem dos segmentos
    $index = findSegmentIndex($segments, (int)$seg['id']);
    $debug['chosen_index'] = $index;
    $debug['chosen_segment_id'] = (int)$seg['id'];
    $debug['segment_order'] = $seg['segment_order'] ?? $index;
    
    return $seg;
  }

  $debug['pattern'] = $pat;

  // ✅✅✅ CORREÇÃO CRÍTICA: Usar TOTAL DE SPINS (não apenas ACKs) para sequência infinita
  $total_spins = spinCount($pdo, $rid);
  $debug['total_spins'] = $total_spins;

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

  // prepara subsets
  $wins  = array_values(array_filter($segments, function($s) { 
    return ($s['type'] === 'WIN'); 
  }));
  $loses = array_values(array_filter($segments, function($s) { 
    return ($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';
  } else {
    // L
    if ($loses) {
      $sub = [];
      $seg = weightedPick($loses, $sub, 'forced_L_weighted_loses');
      $debug['lose_pick_debug'] = $sub;
      $seg['__forced'] = 'L';
    } else {
      // sem LOSE → fallback
      $sub = [];
      $seg = weightedPick($segments, $sub, 'forced_L_fallback_weighted');
      $debug['lose_fallback_debug'] = $sub;
      $seg['__forced'] = 'L';
    }
  }

  // ✅✅✅ CORREÇÃO CRÍTICA: Garantir índice consistente baseado na ordem dos segmentos
  $index = findSegmentIndex($segments, (int)$seg['id']);
  $debug['chosen_index'] = $index;
  $debug['chosen_segment_id'] = (int)$seg['id'];
  $debug['segment_order'] = $seg['segment_order'] ?? $index;
  
  return $seg;
}

/* ---------- ✅ NOVA FUNÇÃO: Encontrar índice baseado na ordem dos segmentos ---------- */
function findSegmentIndex(array $segments, int $segmentId): int {
  foreach ($segments as $index => $segment) {
    if ((int)$segment['id'] === $segmentId) {
      return $index;
    }
  }
  return 0; // Fallback seguro
}

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

/* ---------- 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]);
}

/* ---------- ✅ NOVA FUNÇÃO: Calcular ângulo para índice específico ---------- */
function calculateAngleForIndex(int $index, int $totalSegments, string $pointerPosition = 'right'): float {
  // Constantes matemáticas
  $TAU = 2 * M_PI;
  $SLICE = $TAU / $totalSegments;
  
  // Ângulo médio do segmento
  $sectorMid = $index * $SLICE + $SLICE / 2;
  
  // Offset do ponteiro baseado na posição
  $pointerOffset = 0;
  switch (strtolower($pointerPosition)) {
    case 'bottom': $pointerOffset = M_PI / 2; break;
    case 'left':   $pointerOffset = M_PI; break;
    case 'top':    $pointerOffset = M_PI * 1.5; break;
    case 'right':
    default:       $pointerOffset = 0; break;
  }
  
  // Ângulo final (normalizado)
  $finalAngle = -$sectorMid + $pointerOffset;
  
  // Normalizar para 0-2PI
  $finalAngle = fmod($finalAngle, $TAU);
  if ($finalAngle < 0) $finalAngle += $TAU;
  
  return $finalAngle;
}

/* ---------- ✅ NOVA FUNÇÃO: Verificar sincronização ângulo-índice ---------- */
function verifyAngleIndexSync(int $index, float $angle, int $totalSegments, string $pointerPosition = 'right'): array {
  $TAU = 2 * M_PI;
  $SLICE = $TAU / $totalSegments;
  
  // Calcular ângulo esperado
  $expectedAngle = calculateAngleForIndex($index, $totalSegments, $pointerPosition);
  
  // Calcular índice a partir do ângulo
  $pointerOffset = 0;
  switch (strtolower($pointerPosition)) {
    case 'bottom': $pointerOffset = M_PI / 2; break;
    case 'left':   $pointerOffset = M_PI; break;
    case 'top':    $pointerOffset = M_PI * 1.5; break;
    case 'right':
    default:       $pointerOffset = 0; break;
  }
  
  $normalizedAngle = fmod($pointerOffset - $angle, $TAU);
  if ($normalizedAngle < 0) $normalizedAngle += $TAU;
  
  $calculatedIndex = (int)floor($normalizedAngle / $SLICE) % $totalSegments;
  
  return [
    'expected_angle' => $expectedAngle,
    'actual_angle' => $angle,
    'calculated_index' => $calculatedIndex,
    'target_index' => $index,
    'angle_difference' => abs($expectedAngle - $angle),
    'index_match' => $calculatedIndex === $index,
    'normalized_angle' => $normalizedAngle,
    'pointer_offset' => $pointerOffset
  ];
}