<?php
/**
 * Rules repository.
 *
 * @package Polanger_Shield
 */

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

class Polanger_Shield_Rules {

  /**
   * Allowed rule types.
   *
   * @var string[]
   */
  const RULE_TYPES = array( 'page_block', 'element_hide', 'note', 'demo_lock' );

  /**
   * Per-request cache for active rules grouped by type.
   *
   * @var array<string, array>
   */
  private static $active_rules_cache = array();

  /**
   * Per-request cache for active rules filtered by context.
   *
   * @var array<string, array>
   */
  private static $active_rules_context_cache = array();

  /**
   * Per-request cache for individual rules.
   *
   * @var array<int, object|null>
   */
  private static $rule_cache = array();

  /**
   * Per-request cache for the active rule count.
   *
   * @var int|null
   */
  private static $count_active_cache = null;

  /**
   * Insert rule.
   *
   * @param array $data Rule data.
   * @return int
   */
  public static function insert( $data ) {
    global $wpdb;
    $table = Polanger_Shield_DB::table_name();

    $payload = array(
      'rule_type'              => self::sanitize_rule_type( $data['rule_type'] ?? '' ),
      'status'                 => 'active',
      'screen_id'              => sanitize_text_field( $data['screen_id'] ?? '' ),
      'page_hook'              => sanitize_text_field( $data['page_hook'] ?? '' ),
      'page_slug'              => sanitize_text_field( $data['page_slug'] ?? '' ),
      'request_uri'            => sanitize_text_field( $data['request_uri'] ?? '' ),
      'selector'               => self::sanitize_selector( $data['selector'] ?? '' ),
      'selector_strength'      => max( 0, min( 100, absint( $data['selector_strength'] ?? 0 ) ) ),
      'note'                   => wp_kses_post( $data['note'] ?? '' ),
      'rule_meta'              => wp_json_encode( self::sanitize_rule_meta( $data['rule_meta'] ?? array() ) ),
      'target_user_ids'        => wp_json_encode( self::sanitize_user_ids( $data['target_user_ids'] ?? array() ) ),
      'note_visible_user_ids'  => wp_json_encode( self::sanitize_user_ids( $data['note_visible_user_ids'] ?? array() ) ),
      'block_url'              => ! empty( $data['block_url'] ) ? 1 : 0,
      'created_by'             => absint( $data['created_by'] ?? get_current_user_id() ),
      'created_at'             => current_time( 'mysql' ),
    );

    $formats = array(
      '%s',
      '%s',
      '%s',
      '%s',
      '%s',
      '%s',
      '%s',
      '%d',
      '%s',
      '%s',
      '%s',
      '%s',
      '%d',
      '%d',
      '%s',
    );

    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    $inserted = $wpdb->insert( $table, $payload, $formats );
    if ( false === $inserted ) {
      return 0;
    }

    self::flush_runtime_cache();

    return absint( $wpdb->insert_id );
  }

  /**
   * Soft-delete rule.
   *
   * @param int $rule_id Rule ID.
   * @return bool
   */
  public static function disable( $rule_id ) {
    global $wpdb;
    $table = Polanger_Shield_DB::table_name();
    $rule_id = absint( $rule_id );
    if ( ! $rule_id ) {
      return false;
    }

    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    $updated = $wpdb->update(
      $table,
      array(
        'status'     => 'disabled',
        'updated_at' => current_time( 'mysql' ),
      ),
      array( 'id' => $rule_id ),
      array( '%s', '%s' ),
      array( '%d' )
    );

    if ( false !== $updated ) {
      self::flush_runtime_cache();
    }

    return false !== $updated;
  }

  /**
   * Hard-delete rule.
   *
   * @param int $rule_id Rule ID.
   * @return bool
   */
  public static function delete( $rule_id ) {
    global $wpdb;
    $table = Polanger_Shield_DB::table_name();
    $rule_id = absint( $rule_id );
    if ( ! $rule_id ) {
      return false;
    }

    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    $deleted = $wpdb->delete( $table, array( 'id' => $rule_id ), array( '%d' ) );

    if ( false !== $deleted ) {
      self::flush_runtime_cache();
    }

    return false !== $deleted;
  }

  /**
   * Get active rules for a rule type and context.
   *
   * @param string $rule_type Rule type.
   * @param array  $context Current context.
   * @return array
   */
  public static function get_active_rules_for_context( $rule_type, $context ) {
    $rule_type = self::sanitize_rule_type( $rule_type );
    if ( '' === $rule_type ) {
      return array();
    }

    $context = is_array( $context ) ? $context : array();
    $cache_key = $rule_type . ':' . md5( maybe_serialize( $context ) );

    if ( isset( self::$active_rules_context_cache[ $cache_key ] ) ) {
      return self::$active_rules_context_cache[ $cache_key ];
    }

    $rows = self::get_active_rules( $rule_type );
    if ( empty( $rows ) ) {
      self::$active_rules_context_cache[ $cache_key ] = array();
      return array();
    }

    $matched = array();
    foreach ( $rows as $row ) {
      if ( self::rule_matches_context( $row, $context ) ) {
        $matched[] = $row;
      }
    }

    self::$active_rules_context_cache[ $cache_key ] = $matched;

    return $matched;
  }

  /**
   * Get latest rules for management page.
   *
   * @param int $limit Row limit.
   * @return array
   */
  public static function get_latest_rules( $limit = 200 ) {
    global $wpdb;
    $table = Polanger_Shield_DB::table_name();
    $limit = max( 1, absint( $limit ) );

    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    return $wpdb->get_results(
      $wpdb->prepare(
        "SELECT * FROM {$table} ORDER BY id DESC LIMIT %d",
        $limit
      )
    );
  }

  /**
   * Get rule by ID.
   *
   * @param int $rule_id Rule ID.
   * @return object|null
   */
  public static function get_by_id( $rule_id ) {
    global $wpdb;
    $rule_id = absint( $rule_id );
    if ( ! $rule_id ) {
      return null;
    }

    if ( array_key_exists( $rule_id, self::$rule_cache ) ) {
      return self::$rule_cache[ $rule_id ];
    }

    $table = Polanger_Shield_DB::table_name();
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    $rule = $wpdb->get_row(
      $wpdb->prepare(
        "SELECT * FROM {$table} WHERE id = %d",
        $rule_id
      )
    );

    self::$rule_cache[ $rule_id ] = $rule;

    return $rule;
  }

  /**
   * Count active rules.
   *
   * @return int
   */
  public static function count_active() {
    if ( null !== self::$count_active_cache ) {
      return self::$count_active_cache;
    }

    global $wpdb;
    $table = Polanger_Shield_DB::table_name();
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    self::$count_active_cache = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} WHERE status = 'active'" );

    return self::$count_active_cache;
  }

  /**
   * Get all active rules for a specific type.
   *
   * @param string $rule_type Rule type.
   * @return array
   */
  public static function get_active_rules( $rule_type ) {
    global $wpdb;

    $rule_type = self::sanitize_rule_type( $rule_type );
    if ( '' === $rule_type ) {
      return array();
    }

    if ( isset( self::$active_rules_cache[ $rule_type ] ) ) {
      return self::$active_rules_cache[ $rule_type ];
    }

    $table = Polanger_Shield_DB::table_name();
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    self::$active_rules_cache[ $rule_type ] = $wpdb->get_results(
      $wpdb->prepare(
        "SELECT * FROM {$table} WHERE status = 'active' AND rule_type = %s ORDER BY id DESC",
        $rule_type
      )
    );

    return self::$active_rules_cache[ $rule_type ];
  }

  /**
   * Flush per-request repository caches after rule mutations.
   *
   * @return void
   */
  private static function flush_runtime_cache() {
    self::$active_rules_cache         = array();
    self::$active_rules_context_cache = array();
    self::$rule_cache                 = array();
    self::$count_active_cache         = null;
  }

  /**
   * Context matcher.
   *
   * @param object $rule Rule row.
   * @param array  $context Runtime context.
   * @return bool
   */
  public static function rule_matches_context( $rule, $context ) {
    $screen_id_rule = isset( $rule->screen_id ) ? (string) $rule->screen_id : '';
    $page_hook_rule = isset( $rule->page_hook ) ? (string) $rule->page_hook : '';
    $page_slug_rule = isset( $rule->page_slug ) ? (string) $rule->page_slug : '';
    $uri_rule       = isset( $rule->request_uri ) ? (string) $rule->request_uri : '';

    if ( '' !== $page_slug_rule && ! empty( $context['page_slug'] ) && $page_slug_rule === (string) $context['page_slug'] ) {
      return true;
    }

    if ( '' !== $screen_id_rule && ! empty( $context['screen_id'] ) && $screen_id_rule === (string) $context['screen_id'] ) {
      return true;
    }

    if ( '' !== $page_hook_rule && ! empty( $context['page_hook'] ) && $page_hook_rule === (string) $context['page_hook'] ) {
      return true;
    }

    if ( '' !== $uri_rule && ! empty( $context['request_uri'] ) && false !== strpos( (string) $context['request_uri'], $uri_rule ) ) {
      return true;
    }

    return false;
  }

  /**
   * Sanitize rule type.
   *
   * @param string $rule_type Raw type.
   * @return string
   */
  public static function sanitize_rule_type( $rule_type ) {
    $rule_type = sanitize_key( (string) $rule_type );
    if ( ! in_array( $rule_type, self::RULE_TYPES, true ) ) {
      return '';
    }

    return $rule_type;
  }

  /**
   * Sanitize CSS selector preserving valid selector symbols.
   *
   * @param string $selector Raw selector.
   * @return string
   */
  public static function sanitize_selector( $selector ) {
    $selector = (string) $selector;
    $selector = wp_strip_all_tags( $selector );
    $selector = trim( preg_replace( '/\s+/', ' ', $selector ) );
    if ( strlen( $selector ) > 512 ) {
      $selector = substr( $selector, 0, 512 );
    }

    return $selector;
  }

  /**
   * Sanitize rule meta array recursively.
   *
   * @param mixed $rule_meta Raw meta.
   * @return array
   */
  public static function sanitize_rule_meta( $rule_meta ) {
    if ( is_string( $rule_meta ) && '' !== $rule_meta ) {
      $decoded = json_decode( $rule_meta, true );
      if ( is_array( $decoded ) ) {
        $rule_meta = $decoded;
      }
    }

    if ( ! is_array( $rule_meta ) ) {
      return array();
    }

    return self::sanitize_meta_value( $rule_meta );
  }

  /**
   * Sanitize user ID list.
   *
   * @param array $user_ids Raw list.
   * @return array
   */
  private static function sanitize_user_ids( $user_ids ) {
    return Polanger_Shield_Permissions::normalize_user_ids( $user_ids );
  }

  /**
   * Sanitize scalar/array meta values.
   *
   * @param mixed $value Meta value.
   * @return mixed
   */
  private static function sanitize_meta_value( $value ) {
    if ( is_array( $value ) ) {
      $sanitized = array();
      foreach ( $value as $key => $item ) {
        $sanitized[ sanitize_key( (string) $key ) ] = self::sanitize_meta_value( $item );
      }

      return $sanitized;
    }

    if ( is_bool( $value ) ) {
      return $value;
    }

    if ( is_int( $value ) || is_float( $value ) ) {
      return $value;
    }

    return sanitize_text_field( (string) $value );
  }
}
