<?php
declare(strict_types=1);

/**
 * /includes/security_ip_rules.php
 * IP rules (block list) helpers:
 * - CRUD helpers for admin panel
 * - IP/CIDR matching for enforcement in login/register
 */

/* polyfills for PHP < 8 */
if (!function_exists('str_starts_with')) {
  function str_starts_with(string $haystack, string $needle): bool {
    if ($needle === '') return true;
    return strncmp($haystack, $needle, strlen($needle)) === 0;
  }
}
if (!function_exists('str_contains')) {
  function str_contains(string $haystack, string $needle): bool {
    if ($needle === '') return true;
    return strpos($haystack, $needle) !== false;
  }
}

if (!function_exists('security_ip_rules_has_table')) {
  function security_ip_rules_has_table(PDO $pdo): bool {
    try {
      $st = $pdo->query("SHOW TABLES LIKE 'security_ip_rules'");
      return (bool)$st && (bool)$st->fetchColumn();
    } catch (Throwable $e) {
      return false;
    }
  }
}

/**
 * Перевіряє, що схема таблиці придатна для INSERT без id:
 * - id має бути AUTO_INCREMENT (ідеально) або хоча б мати дефолт/генерацію
 */
if (!function_exists('security_ip_rules_schema_ok')) {
  function security_ip_rules_schema_ok(PDO $pdo): bool {
    if (!security_ip_rules_has_table($pdo)) return false;

    try {
      $st = $pdo->query("SHOW COLUMNS FROM security_ip_rules LIKE 'id'");
      $col = $st ? $st->fetch(PDO::FETCH_ASSOC) : null;
      if (!$col) return false;

      $extra = strtolower((string)($col['Extra'] ?? ''));
      if (str_contains($extra, 'auto_increment')) return true;

      // якщо не auto_increment — найчастіше INSERT без id не пройде
      return false;
    } catch (Throwable $e) {
      return false;
    }
  }
}

if (!function_exists('security_ip_normalize')) {
  function security_ip_normalize(string $ipOrCidr): string {
    $s = trim($ipOrCidr);
    $s = preg_replace('~\s+~u', '', $s) ?? $s; // remove spaces
    return $s;
  }
}

if (!function_exists('security_is_valid_ip')) {
  function security_is_valid_ip(string $ip): bool {
    return filter_var($ip, FILTER_VALIDATE_IP) !== false;
  }
}

if (!function_exists('security_is_valid_cidr_v4')) {
  function security_is_valid_cidr_v4(string $cidr): bool {
    $cidr = security_ip_normalize($cidr);
    if (!str_contains($cidr, '/')) return false;
    [$ip, $mask] = explode('/', $cidr, 2);
    if (!security_is_valid_ip($ip)) return false;
    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false) return false;
    if ($mask === '' || !ctype_digit($mask)) return false;
    $m = (int)$mask;
    return ($m >= 0 && $m <= 32);
  }
}

if (!function_exists('security_ip_in_cidr_v4')) {
  function security_ip_in_cidr_v4(string $ip, string $cidr): bool {
    $ip = trim($ip);
    $cidr = security_ip_normalize($cidr);

    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false) return false;
    if (!security_is_valid_cidr_v4($cidr)) return false;

    [$net, $mask] = explode('/', $cidr, 2);
    $mask = (int)$mask;

    $ipLong  = ip2long($ip);
    $netLong = ip2long($net);
    if ($ipLong === false || $netLong === false) return false;

    // mask 0 => everything
    if ($mask === 0) return true;

    $maskLong = (-1 << (32 - $mask));
    $maskLong = $maskLong & 0xFFFFFFFF;

    return (($ipLong & $maskLong) === ($netLong & $maskLong));
  }
}

if (!function_exists('security_ip_rule_matches')) {
  function security_ip_rule_matches(string $ip, string $ipOrCidr): bool {
    $ip = trim($ip);
    $ipOrCidr = security_ip_normalize($ipOrCidr);

    // exact IP
    if (!str_contains($ipOrCidr, '/')) {
      if (!security_is_valid_ip($ipOrCidr)) return false;
      return strcasecmp($ip, $ipOrCidr) === 0;
    }

    // CIDR (в цьому MVP: тільки IPv4 CIDR)
    return security_ip_in_cidr_v4($ip, $ipOrCidr);
  }
}

if (!function_exists('security_ip_rules_list')) {
  function security_ip_rules_list(PDO $pdo, int $limit = 500): array {
    if (!security_ip_rules_has_table($pdo)) return [];
    $limit = max(1, min(2000, $limit));
    $st = $pdo->query("
      SELECT r.*, u.username AS created_by_username
      FROM security_ip_rules r
      LEFT JOIN users u ON u.id = r.created_by
      ORDER BY r.id DESC
      LIMIT {$limit}
    ");
    return $st ? ($st->fetchAll() ?: []) : [];
  }
}

if (!function_exists('security_ip_rules_add')) {
  function security_ip_rules_add(PDO $pdo, string $ipOrCidr, bool $blockLogin, bool $blockRegister, string $reason, ?int $createdBy): array {
    $ipOrCidr = security_ip_normalize($ipOrCidr);
    $reason = trim($reason);

    if ($ipOrCidr === '') return [false, 'Empty IP/CIDR'];

    // allow single IP (v4/v6) OR v4 CIDR
    $isCidr = str_contains($ipOrCidr, '/');
    if ($isCidr) {
      if (!security_is_valid_cidr_v4($ipOrCidr)) return [false, 'Invalid CIDR'];
    } else {
      if (!security_is_valid_ip($ipOrCidr)) return [false, 'Invalid IP'];
    }

    if (!security_ip_rules_has_table($pdo)) return [false, 'Table not found'];
    if (!security_ip_rules_schema_ok($pdo)) return [false, 'Bad schema'];

    // якщо правило вже є — оновимо (щоб не плодити дублі)
    try {
      $stFind = $pdo->prepare("SELECT id FROM security_ip_rules WHERE ip_or_cidr=? LIMIT 1");
      $stFind->execute([$ipOrCidr]);
      $existingId = (int)$stFind->fetchColumn();

      if ($existingId > 0) {
        $stUp = $pdo->prepare("
          UPDATE security_ip_rules
          SET block_login=?, block_register=?, reason=?, created_by=COALESCE(?, created_by)
          WHERE id=?
          LIMIT 1
        ");
        $ok = $stUp->execute([
          $blockLogin ? 1 : 0,
          $blockRegister ? 1 : 0,
          ($reason !== '' ? $reason : null),
          ($createdBy && $createdBy > 0 ? $createdBy : null),
          $existingId,
        ]);

        if (!$ok) {
          $ei = $stUp->errorInfo();
          $msg = (string)($ei[2] ?? 'DB error');
          return [false, 'DB: ' . $msg];
        }

        return [true, 'Updated'];
      }
    } catch (Throwable $e) {
      return [false, 'DB: ' . $e->getMessage()];
    }

    // інакше — додаємо нове
    try {
      $st = $pdo->prepare("
        INSERT INTO security_ip_rules (ip_or_cidr, block_login, block_register, reason, created_by)
        VALUES (?, ?, ?, ?, ?)
      ");
      $ok = $st->execute([
        $ipOrCidr,
        $blockLogin ? 1 : 0,
        $blockRegister ? 1 : 0,
        ($reason !== '' ? $reason : null),
        ($createdBy && $createdBy > 0 ? $createdBy : null),
      ]);

      if (!$ok) {
        $ei = $st->errorInfo();
        $msg = (string)($ei[2] ?? 'DB error');
        return [false, 'DB: ' . $msg];
      }

      return [true, ''];
    } catch (Throwable $e) {
      return [false, 'DB: ' . $e->getMessage()];
    }
  }
}

if (!function_exists('security_ip_rules_delete')) {
  function security_ip_rules_delete(PDO $pdo, int $id): bool {
    if ($id <= 0) return false;
    if (!security_ip_rules_has_table($pdo)) return false;

    try {
      $st = $pdo->prepare("DELETE FROM security_ip_rules WHERE id=? LIMIT 1");
      return (bool)$st->execute([$id]);
    } catch (Throwable $e) {
      return false;
    }
  }
}

if (!function_exists('security_ip_is_blocked')) {
  /**
   * @return array{blocked:bool, reason:string, rule:?array}
   */
  function security_ip_is_blocked(PDO $pdo, string $ip, string $scope /* login|register */): array {
    $ip = trim($ip);
    if ($ip === '' || !security_is_valid_ip($ip)) {
      return ['blocked' => false, 'reason' => '', 'rule' => null];
    }
    if (!security_ip_rules_has_table($pdo)) {
      return ['blocked' => false, 'reason' => '', 'rule' => null];
    }

    $isLogin = ($scope === 'login');
    $isReg   = ($scope === 'register');

    $st = $pdo->query("
      SELECT id, ip_or_cidr, block_login, block_register, reason, created_at, created_by
      FROM security_ip_rules
      WHERE (block_login=1 OR block_register=1)
      ORDER BY id DESC
      LIMIT 2000
    ");
    $rows = $st ? ($st->fetchAll() ?: []) : [];

    foreach ($rows as $r) {
      $rule = (string)($r['ip_or_cidr'] ?? '');
      if ($rule === '') continue;
      if (!security_ip_rule_matches($ip, $rule)) continue;

      $bl = (int)($r['block_login'] ?? 0) === 1;
      $br = (int)($r['block_register'] ?? 0) === 1;

      if (($isLogin && $bl) || ($isReg && $br)) {
        $reason = (string)($r['reason'] ?? '');
        return [
          'blocked' => true,
          'reason'  => $reason !== '' ? $reason : 'Blocked',
          'rule'    => $r,
        ];
      }
    }

    return ['blocked' => false, 'reason' => '', 'rule' => null];
  }
}