<?php
/**
 * RateLimitingTable — standalone class file.
 *
 * @package UltraguardPro\modules\firewall\db
 */

namespace UltraguardPro\modules\firewall\db;

use UltraguardPro\db\BaseTable;
use UltraguardPro\core\DatabaseCrud;

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

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Rate limiting needs direct live reads/writes against throttling records.
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Rate limiting queries use validated internal table identifiers only.

class RateLimitingTable extends BaseTable {

    protected $table_name  = 'ultraguard_rate_limiting';
    protected $cache_group = 'ultraguard_rate_limiting';

    public function get_defaults(): array {
        return [
            'id'            => 0,
            'ip_address'    => '',
            'endpoint'      => '',
            'request_count' => 1,
            'window_start'  => '',
            'window_end'    => '',
            'is_blocked'    => 0,
            'created_at'    => '',
            'updated_at'    => '',
        ];
    }

    protected function validate_data( $data ) {
        if ( empty( $data['ip_address'] ) || empty( $data['endpoint'] ) ) { return false; }
        return $data;
    }

    protected function sanitize_data( $data ): array {
        $out = [];
        foreach ( $data as $k => $v ) {
            $out[ $k ] = match ( $k ) {
                'request_count' => absint( $v ),
                'is_blocked'    => (int) (bool) $v,
                default         => is_string( $v ) ? sanitize_text_field( $v ) : $v,
            };
        }
        return $out;
    }

    /**
     * Increment request counter for IP + endpoint, creating the row if needed.
     * Returns the new request_count.
     *
     * Uses INSERT ... ON DUPLICATE KEY UPDATE so concurrent requests (or a
     * stale row left by a previous window) never produce a MySQL
     * "Duplicate entry" error for the (ip_address, endpoint) unique key.
     *
     * Window-reset logic (inline in SQL):
     *   - If the existing row window_end is in the past -> reset counter to 1
     *     and open a new window.
     *   - Otherwise -> increment the counter in place.
     *
     * @param string $ip
     * @param string $endpoint
     * @param int    $window_seconds  Length of the counting window in seconds.
     * @return int   The current request count after this hit.
     */
    public function hit( string $ip, string $endpoint, int $window_seconds = 60 ): int {
        global $wpdb;

        $now        = current_time( 'mysql', true );
        $window_end = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp', true ) + $window_seconds );
        $table      = $wpdb->prefix . $this->table_name;

        $sql = $wpdb->prepare(
            "INSERT INTO `{$table}`
                (ip_address, endpoint, request_count, window_start, window_end, is_blocked, created_at, updated_at)
             VALUES
                (%s, %s, 1, %s, %s, 0, %s, %s)
             ON DUPLICATE KEY UPDATE
                request_count = IF(window_end < %s, 1,                    request_count + 1),
                window_start  = IF(window_end < %s, VALUES(window_start), window_start),
                window_end    = IF(window_end < %s, VALUES(window_end),   window_end),
                updated_at    = VALUES(updated_at)",
            $ip,
            $endpoint,
            $now,
            $window_end,
            $now,
            $now,
            $now,
            $now,
            $now
        ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is a validated internal identifier.

        $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared

        // Read back the authoritative count (the upsert may have reset or incremented it).
        $count = (int) $wpdb->get_var(  // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- query built via prepare(); table name is a validated internal identifier.
            $wpdb->prepare(
                "SELECT request_count FROM `{$table}` WHERE ip_address = %s AND endpoint = %s LIMIT 1",
                $ip,
                $endpoint
            )
        );

        return max( 1, $count );
    }

    public function is_blocked( string $ip, string $endpoint ): bool {
        return DatabaseCrud::record_exists_not_expired(
            $this->table_name,
            [ 'ip_address' => $ip, 'endpoint' => $endpoint, 'is_blocked' => 1 ],
            'window_end'
        );
    }

    public function cleanup_expired(): int {
        return $this->delete_where( [ 'window_end <' => current_time( 'mysql', true ) ] );
    }

    protected function get_searchable_fields(): array {
        return [ 'ip_address', 'endpoint' ];
    }
}
