<?php
/**
 * Bounded event logger for Polanger Firewall.
 *
 * @package Polanger_Dashboard_Tools
 */

if ( ! defined( 'ABSPATH' ) ) {
  exit;
}

/**
 * Stores a small rolling event buffer without creating heavy request logs.
 */
class PDT_Firewall_Logger {

  /**
   * Option key for recent events.
   */
  const OPTION_NAME = 'pdt_firewall_recent_events';

  /**
   * Maximum recent events kept in the option.
   */
  const MAX_EVENTS = 80;

  /**
   * Log one event.
   *
   * @param string $decision Event decision.
   * @param string $reason   Reason code.
   * @param array  $context  Extra context.
   * @return void
   */
  public function log_event( $decision, $reason, $context = array() ) {
    $decision = sanitize_key( $decision );
    $reason   = sanitize_key( $reason );

    if ( '' === $decision || '' === $reason ) {
      return;
    }

    $context = is_array( $context ) ? $context : array();
    $ip      = isset( $context['ip'] ) ? sanitize_text_field( $context['ip'] ) : '';
    $uri     = isset( $context['uri'] ) ? $this->sanitize_uri( $context['uri'] ) : $this->get_request_uri();
    $method  = isset( $context['method'] ) ? sanitize_key( $context['method'] ) : $this->get_request_method();
    $bucket  = md5( $decision . '|' . $reason . '|' . $ip . '|' . $uri . '|' . $method );
    $coarse_bucket = md5( $decision . '|' . $reason . '|' . $ip . '|' . $method );

    if ( get_transient( 'pdt_fw_log_' . $bucket ) ) {
      return;
    }

    $coarse_key   = 'pdt_fw_log_coarse_' . $coarse_bucket;
    $coarse_count = absint( get_transient( $coarse_key ) );
    if ( $coarse_count >= 5 ) {
      return;
    }

    set_transient( $coarse_key, $coarse_count + 1, MINUTE_IN_SECONDS );
    set_transient( 'pdt_fw_log_' . $bucket, 1, MINUTE_IN_SECONDS );

    $events = $this->get_events();
    array_unshift(
      $events,
      array(
        'time'     => current_time( 'mysql' ),
        'decision' => $decision,
        'reason'   => $reason,
        'ip'       => $ip,
        'method'   => $method,
        'uri'      => $uri,
        'details'  => isset( $context['details'] ) ? sanitize_text_field( $context['details'] ) : '',
      )
    );

    $events = array_slice( $events, 0, self::MAX_EVENTS );
    update_option( self::OPTION_NAME, $events, false );
  }

  /**
   * Return recent events.
   *
   * @return array
   */
  public function get_events() {
    $events = get_option( self::OPTION_NAME, array() );
    if ( ! is_array( $events ) ) {
      return array();
    }

    $clean = array();

    foreach ( $events as $event ) {
      if ( ! is_array( $event ) ) {
        continue;
      }

      $clean[] = array(
        'time'     => isset( $event['time'] ) ? sanitize_text_field( $event['time'] ) : '',
        'decision' => isset( $event['decision'] ) ? sanitize_key( $event['decision'] ) : '',
        'reason'   => isset( $event['reason'] ) ? sanitize_key( $event['reason'] ) : '',
        'ip'       => isset( $event['ip'] ) ? sanitize_text_field( $event['ip'] ) : '',
        'method'   => isset( $event['method'] ) ? sanitize_key( $event['method'] ) : '',
        'uri'      => isset( $event['uri'] ) ? $this->sanitize_uri( $event['uri'] ) : '',
        'details'  => isset( $event['details'] ) ? sanitize_text_field( $event['details'] ) : '',
      );
    }

    $pruned = $this->prune_events_by_retention( $clean );
    if ( count( $pruned ) !== count( $clean ) ) {
      update_option( self::OPTION_NAME, $pruned, false );
    }

    return $pruned;
  }

  /**
   * Clear recent events.
   *
   * @return void
   */
  public function clear() {
    delete_option( self::OPTION_NAME );
  }

  /**
   * Prune saved events using the configured retention window.
   *
   * @return void
   */
  public function prune_saved_events() {
    $events = $this->get_events();
    update_option( self::OPTION_NAME, $events, false );
  }

  /**
   * Remove events older than the configured retention window.
   *
   * @param array $events Event rows.
   * @return array
   */
  private function prune_events_by_retention( $events ) {
    $seconds = $this->get_retention_seconds();
    if ( $seconds <= 0 ) {
      return $events;
    }

    $now    = strtotime( current_time( 'mysql' ) );
    $cutoff = $now - $seconds;
    $kept   = array();

    foreach ( $events as $event ) {
      $event_time = ! empty( $event['time'] ) ? strtotime( $event['time'] ) : false;
      if ( false === $event_time || $event_time >= $cutoff ) {
        $kept[] = $event;
      }
    }

    return $kept;
  }

  /**
   * Return retention interval in seconds.
   *
   * @return int
   */
  private function get_retention_seconds() {
    $settings  = get_option( 'pdt_firewall_settings', array() );
    $retention = is_array( $settings ) && ! empty( $settings['event_retention'] ) ? sanitize_key( $settings['event_retention'] ) : 'monthly';

    switch ( $retention ) {
      case 'daily':
        return DAY_IN_SECONDS;
      case 'weekly':
        return WEEK_IN_SECONDS;
      case 'yearly':
        return 365 * DAY_IN_SECONDS;
      case 'monthly':
      default:
        return 30 * DAY_IN_SECONDS;
    }
  }

  /**
   * Get current request URI.
   *
   * @return string
   */
  private function get_request_uri() {
    if ( empty( $_SERVER['REQUEST_URI'] ) ) {
      return '';
    }

    return $this->sanitize_uri( wp_unslash( $_SERVER['REQUEST_URI'] ) );
  }

  /**
   * Get current request method.
   *
   * @return string
   */
  private function get_request_method() {
    if ( empty( $_SERVER['REQUEST_METHOD'] ) ) {
      return 'GET';
    }

    return sanitize_key( wp_unslash( $_SERVER['REQUEST_METHOD'] ) );
  }

  /**
   * Sanitize a relative request URI for compact logging.
   *
   * @param mixed $uri Request URI.
   * @return string
   */
  private function sanitize_uri( $uri ) {
    $uri = sanitize_text_field( (string) $uri );
    $uri = str_replace( "\0", '%00', $uri );

    return substr( $uri, 0, 300 );
  }
}
