<?php

class Eurostocks_Botchecker {

  public $ip;
  public $forwarded;
  public $options;
  public $ipv4_csv;
  public $ipv6_csv;
  private $local_country_overrides;


  public function __construct(){
    $this->options = get_option('eurostocks-anti-spam');
    if (!is_array($this->options)) {
      $this->options = array();
    }

    $use_cloudflare_ip = isset($this->options['use_cloudflare_ip']) && (int) $this->options['use_cloudflare_ip'] === 1;

    $resolved_ip = '';
    if ($use_cloudflare_ip && isset($_SERVER['HTTP_CF_CONNECTING_IP']) && $_SERVER['HTTP_CF_CONNECTING_IP'] !== '') {
      $resolved_ip = sanitize_text_field(wp_unslash($_SERVER['HTTP_CF_CONNECTING_IP']));
      // Validate the resolved IP from CF-Connecting-IP header
      if (!filter_var($resolved_ip, FILTER_VALIDATE_IP)) {
        // Fallback to REMOTE_ADDR if invalid
        if (isset($_SERVER['REMOTE_ADDR'])) {
          $resolved_ip = sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR']));
        } else {
          $resolved_ip = '';
        }
      }
    } elseif (isset($_SERVER['REMOTE_ADDR'])) {
      $resolved_ip = sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR']));
    }

    if ('' !== $resolved_ip) {
      $this->ip = $this->normalize_ip($resolved_ip);
    }
    else{
      $this->ip = false;
    }

    if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])){
      $this->forwarded = $this->normalize_ip(sanitize_text_field($_SERVER['HTTP_X_FORWARDED_FOR']));
    }
    else{
      $this->forwarded = false;
    }
    $this->ipv4_csv = plugin_dir_path( __FILE__ ) . 'data/asn-country-ipv4-num.csv';
    $this->ipv6_csv = plugin_dir_path( __FILE__ ) . 'data/asn-country-ipv6-num.csv';
    $this->local_country_overrides = array(
      '127.0.0.1'   => 'local',
      '::1'         => 'local',
      '172.21.0.1'  => 'local',
      '192.168.65.1'=> 'nl',
    );

    error_log('User IP: ' . $this->ip);
  }

  private function normalize_ip($ip) {
    if (strpos($ip, ':ffff:') !== false) {
        // Extract the IPv4 part after the last colon
        $ip = substr($ip, strrpos($ip, ':') + 1);
    }
    return $ip;
}

  public function checkIp($page_type = 'general'){
    $options = $this->options;

    // check if bcmath is enabled
    if (!function_exists('bcadd')) {
      error_log('BCMath extension is not enabled. Please enable it to use the Eurostocks plugin.');
      return array(
        'status'  => 'allowed',
        'message' => '',
      );
    }

    $page_type = $this->prepare_page_type($page_type);

    if (!isset($options['iplist'])){
      $iplist = '';
    }else{
      $iplist = $options['iplist'];
    }

    if (!isset($options['block_unknown'])){
      $block_unknown = 0;
    }else{
      $block_unknown = (int) $options['block_unknown'];
    }

    $ip_array = array_map('trim', explode(',', $iplist));
    if (in_array($this->ip, $ip_array, true)) {
      $this->record_ip_event('blocked', 'manual', $page_type);
      $this->log_request($page_type, 1);
      return array(
        'status'  => 'blocked',
        'reason'  => 'ip_blocklist',
        'reason_key' => 'manual',
        'message' => __('Blocked from accessing', 'eurostocks'),
      );
    }
    
    global $wpdb;
    $table_name_eurostocks_ip = $wpdb->prefix . 'eurostocks_ip';

    // count rows
    $safe_page_type = substr($page_type, 0, 50);
    $request_counts = array();
    if ($this->ip) {
      $request_counts = $this->get_request_counts($table_name_eurostocks_ip);
    }

    $remote_addr_count = isset($request_counts[$safe_page_type]) ? (int) $request_counts[$safe_page_type] : 0;

    $verification_status = $this->ip ? Eurostocks_Email_Verification::get_status($this->ip) : array('status' => Eurostocks_Email_Verification::STATUS_NONE);
    $verification_state = isset($verification_status['status']) ? $verification_status['status'] : Eurostocks_Email_Verification::STATUS_NONE;
    $has_verified = Eurostocks_Email_Verification::STATUS_VERIFIED === $verification_state;

    if(isset($options['allow_countries'])){
      $allow_countries = $options['allow_countries'];
      if(isset($allow_countries) && !empty($allow_countries)){
        error_log('Allowed countries: ' . implode(',', $allow_countries));
        $country = $this->getCountryByIp($this->ip, $this->ipv4_csv, $this->ipv6_csv);
        error_log('Detected country: ' . $country);
        if($country == 'local'){
          $this->log_request($page_type, 0);
          return array('status' => 'allowed');
        }elseif($block_unknown == 1 && $country == null){
          $this->log_request($page_type, 1);
          $this->record_ip_event('blocked', 'country_unknown', $safe_page_type);
          return array(
            'status'  => 'blocked',
            'reason'  => 'country_unknown',
            'reason_key' => 'country_unknown',
            'message' => __('Blocked from accessing - Error code UKN3000', 'eurostocks'),
          );
        }
        if($country != null && !in_array(strtolower($country), $allow_countries)){
          $this->log_request($page_type, 1);
          $this->record_ip_event('blocked', 'country_not_allowed', $safe_page_type, array('details' => array('country' => strtolower($country))));
          return array(
            'status'  => 'blocked',
            'reason'  => 'country_not_allowed',
            'reason_key' => 'country_not_allowed',
            'message' => __('Blocked from accessing - Error code CC3000', 'eurostocks'),
          );
        }
      } else {
        // Geen landen geselecteerd - blokkeer alles behalve lokale IP's
        $country = $this->getCountryByIp($this->ip, $this->ipv4_csv, $this->ipv6_csv);
        if($country != 'local'){
          $this->log_request($page_type, 1);
          $this->record_ip_event('blocked', 'no_countries_allowed', $safe_page_type);
          return array(
            'status'  => 'blocked',
            'reason'  => 'no_countries_allowed',
            'reason_key' => 'no_countries_allowed',
            'message' => __('Blocked from accessing - Error code NC3000', 'eurostocks'),
          );
        }
      }
    } else {
      // allow_countries optie bestaat niet - blokkeer alles behalve lokale IP's
      $country = $this->getCountryByIp($this->ip, $this->ipv4_csv, $this->ipv6_csv);
      if($country != 'local'){
        $this->log_request($page_type, 1);
        $this->record_ip_event('blocked', 'no_countries_configured', $safe_page_type);
        return array(
          'status'  => 'blocked',
          'reason'  => 'no_countries_configured',
          'reason_key' => 'no_countries_configured',
          'message' => __('Blocked from accessing - Error code NC3001', 'eurostocks'),
        );
      }
    }

    $detail_requests = isset($request_counts['vehicle_detail']) ? (int) $request_counts['vehicle_detail'] : 0;
    $search_requests = isset($request_counts['vehicle_list']) ? (int) $request_counts['vehicle_list'] : 0;
    $ratio_threshold = Eurostocks_Anti_Spam_Settings::get_ratio_min_detail_requests($options);
    $ratio_percent_limit = Eurostocks_Anti_Spam_Settings::get_ratio_percent_limit($options);
    $request_ratio = Eurostocks_Anti_Spam_Settings::calculate_detail_to_search_ratio($detail_requests, $search_requests);

    // Require email verification when the detail/search ratio exceeds the configured limit.
    if ($detail_requests >= $ratio_threshold && $request_ratio >= $ratio_percent_limit) {
      $reason_key = Eurostocks_Anti_Spam_Settings::get_reason_key_for_page_type('ratio', 'ratio_limit');

      if (!$has_verified) {
        $this->record_ip_event('verify_email', $reason_key, 'ratio', array(
          'ratio'           => $request_ratio,
          'detail_requests' => $detail_requests,
          'search_requests' => $search_requests,
          'threshold'       => $ratio_threshold,
          'ratio_limit'     => $ratio_percent_limit,
        ));

        $this->log_request($page_type, 0);
        $verify_message = Eurostocks_Anti_Spam_Settings::get_text('limit_verify_message', $options);

        return array(
          'status'    => 'verify_email',
          'reason'    => 'ratio_limit',
          'reason_key'=> $reason_key,
          'message'   => $verify_message,
          'limit'     => (float) $ratio_percent_limit,
          'count'     => (float) $request_ratio,
          'page_type' => 'ratio',
        );
      }
    }

    $autoban = Eurostocks_Anti_Spam_Settings::get_page_type_limit($page_type, 'autoban', $options);
    error_log('Remote addr count: ' . $remote_addr_count . ', Autoban limit: ' . $autoban);

    if($autoban > 0 && $remote_addr_count >= $autoban){
      $ip_array = array_map('trim', explode("\n", $iplist));
      if(!in_array($this->ip, $ip_array)){
        $iplist = trim($iplist . "\n" . $this->ip);
        $options['iplist'] = trim($iplist);
        update_option('eurostocks-anti-spam', $options);
      }
      $this->log_request($page_type, 1);
      $reason_key = Eurostocks_Anti_Spam_Settings::get_reason_key_for_page_type($safe_page_type, 'too_many_requests');
      $this->record_ip_event('blocked', $reason_key, $safe_page_type, array(
        'limit' => (int) $autoban,
        'count' => (int) $remote_addr_count,
      ));
      $limit_block_message = Eurostocks_Anti_Spam_Settings::get_text('limit_block_message', $options);
      return array(
        'status'  => 'blocked',
        'reason'  => 'too_many_requests',
        'reason_key' => $reason_key,
        'message' => $limit_block_message,
      );
    }

    $allowed_limit = Eurostocks_Anti_Spam_Settings::get_page_type_limit($page_type, 'email_verification_limit', $options);
    $global_limit_context = $this->determine_email_limit_context($request_counts, $options, $safe_page_type);
    $verification_required = !$has_verified && (null !== $global_limit_context || Eurostocks_Email_Verification::STATUS_PENDING === $verification_state);

    if ($verification_required) {
      $limit_data = null !== $global_limit_context ? $global_limit_context : array(
        'page_type' => $safe_page_type,
        'limit'     => (int) $allowed_limit,
        'count'     => (int) $remote_addr_count,
      );

      $reason_key = Eurostocks_Anti_Spam_Settings::get_reason_key_for_page_type($limit_data['page_type'], 'limit_reached');
      $this->record_ip_event('verify_email', $reason_key, $limit_data['page_type'], array(
        'limit' => isset($limit_data['limit']) ? (int) $limit_data['limit'] : 0,
        'count' => isset($limit_data['count']) ? (int) $limit_data['count'] : 0,
      ));

      $this->log_request($page_type, 0);
      $verify_message = Eurostocks_Anti_Spam_Settings::get_text('limit_verify_message', $options);
      return array(
        'status'  => 'verify_email',
        'reason'  => 'limit_reached',
        'reason_key' => $reason_key,
        'message' => $verify_message,
        'limit'   => isset($limit_data['limit']) ? (int) $limit_data['limit'] : 0,
        'count'   => isset($limit_data['count']) ? (int) $limit_data['count'] : 0,
        'page_type' => $limit_data['page_type'],
      );
    }

    $this->log_request($page_type, 0);

    return array(
      'status'  => 'allowed',
      'message' => '',
    );
  }

  private function log_request($page_type, $blocked_flag){
    if (!$this->ip) {
      return;
    }

    global $wpdb;
    $table_name_eurostocks_ip = $wpdb->prefix . 'eurostocks_ip';
    $safe_page_type = substr(sanitize_text_field($page_type), 0, 50);

    $wpdb->insert(
      $table_name_eurostocks_ip,
      array(
        'remote_addr'   => $this->ip,
        'forwarded_for' => $this->forwarded,
        'blocked'       => (int) $blocked_flag,
        'page_type'     => $safe_page_type,
      ),
      array('%s', '%s', '%d', '%s')
    );
  }

  private function prepare_page_type($page_type) {
    $prepared = is_string($page_type) ? sanitize_key($page_type) : 'general';
    if ('' === $prepared) {
      $prepared = 'general';
    }

    return substr($prepared, 0, 50);
  }

  private function get_request_counts($table_name) {
    global $wpdb;

    if (!$this->ip) {
      return array();
    }

    $results = $wpdb->get_results(
      $wpdb->prepare(
        "SELECT page_type, COUNT(*) as total FROM %i WHERE remote_addr = %s AND registration_date > (NOW() - INTERVAL 24 HOUR) GROUP BY page_type",
        $table_name,
        $this->ip
      ),
      ARRAY_A
    );

    $counts = array();
    if (is_array($results)) {
      foreach ($results as $row) {
        $type = isset($row['page_type']) ? sanitize_key((string) $row['page_type']) : 'general';
        if ('' === $type) {
          $type = 'general';
        }
        $counts[$type] = isset($row['total']) ? (int) $row['total'] : 0;
      }
    }

    return $counts;
  }

  private function determine_email_limit_context($request_counts, $options, $current_page_type) {
    if (!is_array($request_counts)) {
      $request_counts = array();
    }

    $page_types = $this->get_tracked_page_types($options, $current_page_type);
    foreach ($page_types as $type) {
      $limit = Eurostocks_Anti_Spam_Settings::get_page_type_limit($type, 'email_verification_limit', $options);
      if ($limit <= 0) {
        continue;
      }

      $count = isset($request_counts[$type]) ? (int) $request_counts[$type] : 0;
      if ($count >= $limit) {
        return array(
          'page_type' => $type,
          'limit'     => (int) $limit,
          'count'     => $count,
        );
      }
    }

    return null;
  }

  private function get_tracked_page_types($options, $current_page_type) {
    $page_types = array('general');
    $page_types[] = $current_page_type;

    $defaults = Eurostocks_Anti_Spam_Settings::get_default_page_types();
    if (is_array($defaults)) {
      $page_types = array_merge($page_types, array_keys($defaults));
    }

    if (isset($options['per_page_limits']) && is_array($options['per_page_limits'])) {
      $page_types = array_merge($page_types, array_keys($options['per_page_limits']));
    }

    $page_types = array_map(static function ($type) {
      $prepared = sanitize_key((string) $type);
      return '' === $prepared ? null : $prepared;
    }, $page_types);

    $page_types = array_filter($page_types);

    return array_values(array_unique($page_types));
  }

  private function record_ip_event($status, $reason_key, $page_type = 'general', $details = array()) {
    if (!$this->ip) {
      return;
    }

    Eurostocks_Anti_Spam_Settings::record_ip_event($this->ip, array(
      'status' => $status,
      'reason_key' => $reason_key,
      'page_type' => $page_type,
      'details' => $details,
    ));
  }


  function getCountryByIp($ip, $csvFileIPv4, $csvFileIPv6) {
    if (!$ip) {
      return null;
    }

    if (isset($this->local_country_overrides[$ip])) {
      return $this->local_country_overrides[$ip];
    }

    if ($this->isIPv4($ip)) {
        $ipLong = ip2long($ip);
        $csvFile = $csvFileIPv4;
    }
    // check if local ip
    else if($ip == '172.21.0.1'){
      return 'local';
    }
     else {
      $ipLong = $this->ipToBigInt($ip);
      $csvFile = $csvFileIPv6;
    }

    if (($handle = fopen($csvFile, "r")) !== FALSE) {
        while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) {
            $rangeStart = $data[0];
            $rangeEnd = $data[1];
            $country = $data[2];

            if ($this->compareLargeNumbers($ipLong, $rangeStart) >= 0 && $this->compareLargeNumbers($ipLong, $rangeEnd) <= 0) {
                return $country;
            }
        }
        fclose($handle);
    }

    return null;
}

private function isIPv4($ip) {
    return strpos($ip, ':') === false;
}

private function compareLargeNumbers($num1, $num2) {
  // Remove leading zeros
  $num1 = ltrim($num1, '0');
  $num2 = ltrim($num2, '0');

  // Compare lengths
  if (strlen($num1) > strlen($num2)) {
      return 1;
  } else if (strlen($num1) < strlen($num2)) {
      return -1;
  }

  // Compare digit by digit
  for ($i = 0; $i < strlen($num1); $i++) {
      if ($num1[$i] > $num2[$i]) {
          return 1;
      } else if ($num1[$i] < $num2[$i]) {
          return -1;
      }
  }

  // If we haven't returned yet, the numbers are equal
  return 0;
}

private function ipToBigInt($ip) {
  // Convert IP address to packed binary string
  $packedIp = inet_pton($ip);

  // Check if the IP address is IPv4 or IPv6
  if (strlen($packedIp) == 4) {
      // IPv4
      $unpacked = unpack('N', $packedIp);
  } else {
      // IPv6
      $unpacked = unpack('N2', $packedIp);
      $unpacked = bcadd(bcmul($unpacked[1], bcpow(2, 32)), $unpacked[2]);
  }

  return strval($unpacked[1]);
}

}
