<?php
/**
 * Security class for KimaAI AI modules.
 *
 * @class Security
 * @package KimaAI\Ai
 * @since 1.2.5
 */

declare(strict_types=1);

namespace KimaAI\Ai;

use KimaAI\Admin\Settings;

defined('ABSPATH') || exit;

class Security {

  /**
   * Comma-separated banned words.
   *
   * @var string|null
   */
  public ?string $banned_words = null;

  /**
   * Comma-separated banned IPs or CIDR ranges.
   *
   * @var string|null
   */
  public ?string $banned_ips = null;

  /**
   * Create the AI security gatekeeper.
   */
  public function __construct(
    public Settings $settings
  ) {}

  /**
   * Register security-related chatbot hooks.
   */
  public function registerHooks(): void {
    $chatbot_settings = $this->settings->getOption(null, 'chatbot', 'settings');
    $this->banned_words = $chatbot_settings['bannedWords'] ?? null;
    $this->banned_ips = $chatbot_settings['bannedIps'] ?? null;

    if (!empty($this->banned_words)) {
      add_filter('kimaai_chatbot_gate', [$this, 'checkBannedWords'], 10, 2);
    }

    if (!empty($this->banned_ips)) {
      add_filter('kimaai_chatbot_gate', [$this, 'checkBannedIps']);
    }
  }

  /**
   * Checks if the message contains any banned words.
   *
   * @hook kimaai_chatbot_gate
   */
  public function checkBannedWords(bool $ok, string $message) {
    if (!$ok || empty($this->banned_words)) {
      return $ok;
    }

    $banned_words = explode(',', $this->banned_words);

    foreach ($banned_words as $word) {
      $pattern = '/\b' . preg_quote(trim($word), '/') . '\b/iu';
      if (preg_match($pattern, $message)) {
        $message = sprintf(
        /* translators: 1: banned word */
          __('Your message contains a banned word: %1$s, 🔒', 'kimaai'),
          esc_html($word)
        );

        return $message;
      }
    }

    return true;
  }

  /**
   * Checks if the IP address is banned.
   *
   * @hook kimaai_chatbot_gate
   */
  public function checkBannedIps(bool $ok) {
    if (!$ok || empty($this->banned_ips)) {
      return $ok;
    }
    $banned_ips = explode(',', $this->banned_ips);

    $current_ip = $this->getIpAddress();

    foreach ($banned_ips as $banned_ip) {
      if (trim($banned_ip) === $current_ip) {
        return "Your IP address is banned: $current_ip, 🔒";
      }

      // Check if the banned IP is a CIDR range
      if (str_contains($banned_ip, '/')) {
        list($range, $netmask) = explode('/', $banned_ip);
        if ($this->ipInRange($current_ip, $range, (int) $netmask)) {
          $message = sprintf(
          /* translators: 1: banned IP range */
            __('Your IP address is banned in the range: %1$s, 🔒', 'kimaai'),
            esc_html($current_ip)
          );

          return $message;
        }
      }
    }

    return true;
  }


  /**
   * Resolve the current request IP address.
   *
   * @return string
   */
  public function getIpAddress() {
    $ip_keys = [
      'HTTP_CF_CONNECTING_IP',
      'HTTP_CLIENT_IP',
      'HTTP_X_FORWARDED_FOR',
      'HTTP_X_FORWARDED',
      'HTTP_X_CLUSTER_CLIENT_IP',
      'HTTP_X_REAL_IP',
      'HTTP_FORWARDED_FOR',
      'HTTP_FORWARDED',
      'REMOTE_ADDR',
    ];
    foreach ($ip_keys as $key) {
      if (array_key_exists($key, $_SERVER) === true) {
        $server_value = sanitize_text_field(wp_unslash($_SERVER[$key]));
        $ips = explode(',', $server_value);
        foreach ($ips as $ip) {
          $ip = trim($ip);
          if ($this->validateIP($ip)) {
            return $ip;
          }
        }
      }
    }

    $remote_addr = isset($_SERVER['REMOTE_ADDR'])
    ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR']))
    : '';

    return $remote_addr !== '' ? $remote_addr : '127.0.0.1';
  }

  /**
   * Validate a public IPv4 or IPv6 address.
   */
  private function validateIP($ip): bool {
    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
      return false;
    }
    return true;
  }

  /**
   * Check whether an IP address falls within a CIDR range.
   */
  private function ipInRange($ip, $range, int $netmask): bool {
    if (!str_contains($range, '/')) {
      $range .= '/32'; // Convert single IP to CIDR notation
    }
    list($range_ip, $subnet) = explode('/', $range, 2);
    if (filter_var($range_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
      $ip_bin = ip2long($ip);
      $range_ip_bin = ip2long($range_ip);
      $subnet_mask = 0xFFFFFFFF << (32 - $subnet);
      return ($ip_bin & $subnet_mask) === ($range_ip_bin & $subnet_mask);
    }
    elseif (filter_var($range_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
      $ip_bin = inet_pton($ip);
      $range_ip_bin = inet_pton($range_ip);
      $subnet_mask = str_repeat("\xFF", $netmask >> 3) . str_repeat("\x00", 16 - ($netmask >> 3));
      return ($ip_bin & $subnet_mask) === ($range_ip_bin & $subnet_mask);
    }
    return false;
  }
}
