<?php
declare(strict_types=1);

/**
 * /includes/security_rate_limit.php
 *
 * Rate-limit для логіну/реєстрації:
 * - Налаштування зберігаються в app_settings
 * - Лічильники/блокування зберігаються в security_rate_limits
 *
 * Ключі app_settings:
 * - sec_login_attempts (int)   default 5
 * - sec_login_window_min (int) default 15
 * - sec_login_lock_min (int)   default 30
 * - sec_login_mode (ip|user|both) default both
 */

if (!function_exists('security_has_table')) {
  function security_has_table(PDO $pdo, string $table): bool {
    try {
      $st = $pdo->prepare("SHOW TABLES LIKE ?");
      $st->execute([$table]);
      return (bool)$st->fetchColumn();
    } catch (Throwable $e) {
      return false;
    }
  }
}

function security_app_settings_has_table(PDO $pdo): bool {
  return security_has_table($pdo, 'app_settings');
}

function security_rate_limits_has_table(PDO $pdo): bool {
  return security_has_table($pdo, 'security_rate_limits');
}

function security_rate_default_settings(): array {
  return [
    'attempts' => 5,
    'window_min' => 15,
    'lock_min' => 30,
    'mode' => 'both', // ip|user|both
  ];
}

function security_rate_get_settings(PDO $pdo): array {
  $def = security_rate_default_settings();
  if (!security_app_settings_has_table($pdo)) return $def;

  try {
    $keys = [
      'sec_login_attempts',
      'sec_login_window_min',
      'sec_login_lock_min',
      'sec_login_mode',
    ];
    $in = implode(',', array_fill(0, count($keys), '?'));
    $st = $pdo->prepare("SELECT key_name, value_text FROM app_settings WHERE key_name IN ($in)");
    $st->execute($keys);
    $rows = $st->fetchAll(PDO::FETCH_ASSOC) ?: [];
    $map = [];
    foreach ($rows as $r) $map[(string)$r['key_name']] = (string)($r['value_text'] ?? '');

    $attempts = (int)($map['sec_login_attempts'] ?? $def['attempts']);
    $window   = (int)($map['sec_login_window_min'] ?? $def['window_min']);
    $lock     = (int)($map['sec_login_lock_min'] ?? $def['lock_min']);
    $mode     = trim((string)($map['sec_login_mode'] ?? $def['mode']));

    if ($attempts <= 0) $attempts = $def['attempts'];
    if ($window <= 0)   $window   = $def['window_min'];
    if ($lock <= 0)     $lock     = $def['lock_min'];
    if (!in_array($mode, ['ip','user','both'], true)) $mode = $def['mode'];

    // обмеження “щоб не поламали”
    $attempts = max(1, min(50, $attempts));
    $window   = max(1, min(1440, $window));
    $lock     = max(1, min(10080, $lock)); // до 7 днів

    return [
      'attempts' => $attempts,
      'window_min' => $window,
      'lock_min' => $lock,
      'mode' => $mode,
    ];
  } catch (Throwable $e) {
    return $def;
  }
}

function security_rate_save_settings(PDO $pdo, int $attempts, int $windowMin, int $lockMin, string $mode): void {
  if (!security_app_settings_has_table($pdo)) {
    throw new RuntimeException('app_settings table missing');
  }
  $attempts = max(1, min(50, $attempts));
  $windowMin = max(1, min(1440, $windowMin));
  $lockMin = max(1, min(10080, $lockMin));
  if (!in_array($mode, ['ip','user','both'], true)) $mode = 'both';

  $now = date('Y-m-d H:i:s');

  $items = [
    'sec_login_attempts' => (string)$attempts,
    'sec_login_window_min' => (string)$windowMin,
    'sec_login_lock_min' => (string)$lockMin,
    'sec_login_mode' => $mode,
  ];

  $pdo->beginTransaction();
  try {
    $st = $pdo->prepare("
      INSERT INTO app_settings (key_name, value_text, updated_at)
      VALUES (?, ?, ?)
      ON DUPLICATE KEY UPDATE value_text=VALUES(value_text), updated_at=VALUES(updated_at)
    ");
    foreach ($items as $k => $v) {
      $st->execute([$k, $v, $now]);
    }
    $pdo->commit();
  } catch (Throwable $e) {
    $pdo->rollBack();
    throw $e;
  }
}

/**
 * Побудова scope-ключів (для таблиці security_rate_limits)
 */
function security_rate_scopes(string $mode, string $ip, int $userId = 0): array {
  $ip = trim($ip);
  $userId = (int)$userId;

  $scopes = [];
  if ($mode === 'ip') {
    if ($ip !== '') $scopes[] = ['type' => 'ip', 'key' => $ip];
  } elseif ($mode === 'user') {
    if ($userId > 0) $scopes[] = ['type' => 'user', 'key' => (string)$userId];
  } else { // both (combined)
    if ($ip !== '' && $userId > 0) {
      $scopes[] = ['type' => 'ip_user', 'key' => $ip . '|' . $userId];
    }
    // як fallback (щоб “combined” працював навіть якщо userId невідомий):
    if ($ip !== '') $scopes[] = ['type' => 'ip', 'key' => $ip];
    if ($userId > 0) $scopes[] = ['type' => 'user', 'key' => (string)$userId];
  }

  // прибираємо дублікати
  $uniq = [];
  foreach ($scopes as $s) {
    $k = $s['type'] . '|' . $s['key'];
    $uniq[$k] = $s;
  }
  return array_values($uniq);
}

/**
 * Перевірка: чи зараз заблоковано (true/false) + locked_until
 */
function security_rate_is_locked(PDO $pdo, array $scopes): array {
  if (!security_rate_limits_has_table($pdo)) return ['locked' => false, 'locked_until' => null];

  $now = date('Y-m-d H:i:s');
  foreach ($scopes as $s) {
    try {
      $st = $pdo->prepare("
        SELECT locked_until
        FROM security_rate_limits
        WHERE scope_type=? AND scope_key=? AND locked_until IS NOT NULL AND locked_until > ?
        LIMIT 1
      ");
      $st->execute([(string)$s['type'], (string)$s['key'], $now]);
      $lu = $st->fetchColumn();
      if ($lu) {
        return ['locked' => true, 'locked_until' => (string)$lu];
      }
    } catch (Throwable $e) {
      // ignore and continue
    }
  }
  return ['locked' => false, 'locked_until' => null];
}

/**
 * Реєстрація невдалої спроби:
 * - якщо вікно пройшло — починаємо заново
 * - якщо досягли attempts — ставимо locked_until
 */
function security_rate_register_failure(PDO $pdo, array $settings, array $scopes): void {
  if (!security_rate_limits_has_table($pdo)) return;

  $attemptsLimit = (int)$settings['attempts'];
  $windowMin     = (int)$settings['window_min'];
  $lockMin       = (int)$settings['lock_min'];

  $nowTs = time();
  $now   = date('Y-m-d H:i:s', $nowTs);
  $windowStartTs = $nowTs - ($windowMin * 60);

  foreach ($scopes as $s) {
    $type = (string)$s['type'];
    $key  = (string)$s['key'];

    $pdo->beginTransaction();
    try {
      // тягнемо рядок FOR UPDATE
      $st = $pdo->prepare("SELECT id, attempts, first_at, locked_until FROM security_rate_limits WHERE scope_type=? AND scope_key=? LIMIT 1 FOR UPDATE");
      $st->execute([$type, $key]);
      $row = $st->fetch(PDO::FETCH_ASSOC);

      if (!$row) {
        // новий
        $newAttempts = 1;
        $firstAt = $now;
        $lockedUntil = null;
        if ($newAttempts >= $attemptsLimit) {
          $lockedUntil = date('Y-m-d H:i:s', $nowTs + ($lockMin * 60));
        }
        $ins = $pdo->prepare("
          INSERT INTO security_rate_limits (scope_type, scope_key, attempts, first_at, last_at, locked_until, updated_at)
          VALUES (?, ?, ?, ?, ?, ?, ?)
        ");
        $ins->execute([$type, $key, $newAttempts, $firstAt, $now, $lockedUntil, $now]);
        $pdo->commit();
        continue;
      }

      $id = (int)$row['id'];
      $firstAtStr = (string)($row['first_at'] ?? '');
      $firstAtTs = $firstAtStr !== '' ? strtotime($firstAtStr) : 0;

      // якщо first_at поза вікном — ресет
      if ($firstAtTs > 0 && $firstAtTs < $windowStartTs) {
        $newAttempts = 1;
        $firstAt = $now;
      } else {
        $newAttempts = (int)($row['attempts'] ?? 0) + 1;
        $firstAt = $firstAtStr !== '' ? $firstAtStr : $now;
      }

      $lockedUntil = (string)($row['locked_until'] ?? '');
      $newLockedUntil = null;

      // якщо вже заблоковано і ще не пройшло — лишаємо як є
      if ($lockedUntil !== '' && strtotime($lockedUntil) > $nowTs) {
        $newLockedUntil = $lockedUntil;
      } else {
        if ($newAttempts >= $attemptsLimit) {
          $newLockedUntil = date('Y-m-d H:i:s', $nowTs + ($lockMin * 60));
        }
      }

      $upd = $pdo->prepare("
        UPDATE security_rate_limits
        SET attempts=?, first_at=?, last_at=?, locked_until=?, updated_at=?
        WHERE id=?
      ");
      $upd->execute([$newAttempts, $firstAt, $now, $newLockedUntil, $now, $id]);

      $pdo->commit();
    } catch (Throwable $e) {
      $pdo->rollBack();
      // не валимо логін, просто не рахуємо
    }
  }
}

/**
 * При успішному логіні можна очищати лічильники.
 */
function security_rate_clear(PDO $pdo, array $scopes): void {
  if (!security_rate_limits_has_table($pdo)) return;
  foreach ($scopes as $s) {
    try {
      $st = $pdo->prepare("DELETE FROM security_rate_limits WHERE scope_type=? AND scope_key=?");
      $st->execute([(string)$s['type'], (string)$s['key']]);
    } catch (Throwable $e) {}
  }
}