<?php

namespace GriffinForms\Frontend\Security;

use \GriffinForms\Traits\Singleton;

/**
 * Class BotLimiter
 *
 * Handles bot request limiting and security checks for GriffinForms frontend.
 * Responsible for rate limiting, key management, and integration with
 * WordPress transients (or pluggable stores) to prevent automated or abusive requests.
 */
class BotLimiter extends \GriffinForms\Frontend\Security\Format
{
    use Singleton;

    /** @var string Database table used when not using transients (optional) */
    protected $db_table = '';

    /** @var int Active form id for scoping */
    protected $form_id = 0;

    /** @var string Unique client hash (ip|form|user) */
    protected $client_hash = '';

    /** @var string Transient key (burst) */
    protected $transient_key_burst = '';

    /** @var string Transient key (window) */
    protected $transient_key_window = '';

    /**
     * Hardcoded default rate limiting values until settings are wired everywhere.
     */
    protected $rate_burst_limit = 5;     // allow 5 actions per short burst window
    protected $rate_burst_ttl = 15;      // 15 seconds burst window
    protected $rate_window_limit = 30;   // 30 actions per rolling window
    protected $rate_window_ttl = 300;    // 5 minutes rolling window

    /** @var string Storage backend: 'transient' or 'db' */
    protected $storage_backend = 'transient';

    /** @var string Event type for scoping (e.g., 'submit', 'upload', 'view') */
    protected $event_type = 'submit';

    /**
     * Initialize for a given form. Computes keys and loads limits once.
     */
    public function init(int $form_id): void
    {
        $this->form_id = $form_id;
        $this->setDbTable();
        $this->setClientHash();
        $this->setTransientKeyBurst();
        $this->setTransientKeyWindow();

        // Optional: load limits from settings (fallback to defaults)
        if (isset($this->settings)) {
            $burst_limit  = (int) $this->settings->getOption('rate_burst_limit',  $this->rate_burst_limit);
            $burst_ttl    = (int) $this->settings->getOption('rate_burst_ttl',    $this->rate_burst_ttl);
            $window_limit = (int) $this->settings->getOption('rate_window_limit', $this->rate_window_limit);
            $window_ttl   = (int) $this->settings->getOption('rate_window_ttl',   $this->rate_window_ttl);

            $this->rate_burst_limit  = max(0, $burst_limit);
            $this->rate_burst_ttl    = max(1, $burst_ttl);
            $this->rate_window_limit = max(0, $window_limit);
            $this->rate_window_ttl   = max(1, $window_ttl);
        }

        $this->determineStorageBackend();
    }

    /** Resolve storage table name if a DB store is used. */
    protected function setDbTable(): void
    {
        $this->db_table = $this->config->getTable('ratelimiter');
    }

    /** Compute and assign client hash (ip|form|user). */
    protected function setClientHash(): string
    {
        $ip      = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field($_SERVER['REMOTE_ADDR']) : '0.0.0.0';
        $user_id = (int) get_current_user_id();
        $salt    = $ip . '|' . (int) $this->form_id . '|' . $user_id . '|' . (string) $this->event_type;
        $this->client_hash = md5($salt);
        return $this->client_hash;
    }

    /** Build a transient key */
    protected function transientKey(string $type): string
    {
        return 'gf_rate_' . $type . '_' . $this->client_hash;
    }

    /** Assign burst transient key */
    protected function setTransientKeyBurst(): void
    {
        $this->transient_key_burst = $this->transientKey('burst');
    }

    /** Assign window transient key */
    protected function setTransientKeyWindow(): void
    {
        $this->transient_key_window = $this->transientKey('window');
    }

    /**
     * Determine the storage backend to use: 'transient' or 'db'
     */
    protected function determineStorageBackend(): void
    {
        if (isset($this->settings)) {
            $storage = $this->settings->getOption('rate_limit_storage', '');
            if ($storage === 'transient' || $storage === 'db') {
                $this->storage_backend = $storage;
                return;
            }
        }

        if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) {
            $this->storage_backend = 'transient';
        } else {
            $this->storage_backend = 'db';
        }
    }

    /**
     * Check if using DB storage backend
     *
     * @return bool
     */
    protected function isUsingDb(): bool
    {
        return $this->storage_backend === 'db';
    }

    /**
     * Get counter data from DB for given type and TTL
     *
     * @param string $type
     * @param int $ttl
     * @return array
     */
    protected function getDbCounter(string $type, int $ttl): array
    {
        global $wpdb;

        $sql = $wpdb->prepare(
            "SELECT count, first_ts, updated_ts FROM {$this->db_table} WHERE client_hash = %s AND type = %s LIMIT 1",
            $this->client_hash,
            $type
        );
        $row = $wpdb->get_row($sql, ARRAY_A);

        $now = time();
        $default = ['count' => 0, 'first_ts' => $now, 'updated_ts' => $now];

        if (!$row) {
            return $default;
        }
        // Check if expired
        if (($now - (int) $row['updated_ts']) > $ttl) {
            return $default;
        }

        return [
            'count' => (int) $row['count'],
            'first_ts' => (int) $row['first_ts'],
            'updated_ts' => (int) $row['updated_ts'],
        ];
    }

    /**
     * Save counter data to DB for given type
     *
     * @param string $type
     * @param array $data
     */
    protected function saveDbCounter(string $type, array $data): void
    {
        global $wpdb;

        $now = time();
        $count = isset($data['count']) ? (int) $data['count'] : 0;
        $first_ts = isset($data['first_ts']) ? (int) $data['first_ts'] : $now;
        $updated_ts = $now;

        $result = $wpdb->replace(
            $this->db_table,
            [
                'client_hash' => $this->client_hash,
                'type'        => $type,
                'count'       => $count,
                'first_ts'    => $first_ts,
                'updated_ts'  => $updated_ts,
            ],
            [
                '%s', '%s', '%d', '%d', '%d'
            ]
        );

        if ($result === false) {
            if (isset($this->log)) {
                $this->log->add('error', 'ratelimit', (int) $this->form_id, get_current_user_id(), "DB counter save failed for {$type}", 'security');
            }
        } else {
            if (isset($this->log)) {
                $this->log->add('info', 'ratelimit', (int) $this->form_id, get_current_user_id(), "DB counter saved for {$type}", 'security');
            }
        }
    }

    /** Get burst transient or DB value, defaulting to a fresh counter. */
    public function getTransientValueBurst(): array
    {
        if ($this->isUsingDb()) {
            return $this->getDbCounter('burst', $this->rate_burst_ttl);
        }
        return $this->getTransientWithDefault($this->transient_key_burst);
    }

    /** Get window transient or DB value, defaulting to a fresh counter. */
    public function getTransientValueWindow(): array
    {
        if ($this->isUsingDb()) {
            return $this->getDbCounter('window', $this->rate_window_ttl);
        }
        return $this->getTransientWithDefault($this->transient_key_window);
    }

    /** Shared getter with safe defaults for transient storage. */
    protected function getTransientWithDefault(string $key): array
    {
        $value = get_transient($key);
        return is_array($value) ? $value : ['count' => 0, 'first_ts' => time()];
    }

    /**
     * Increment counters for burst and rolling windows by a given amount.
     */
    public function markEventBy(int $count): void
    {
        if (current_user_can('manage_options')) {
            return; // do not count admins
        }
        $count = max(1, (int) $count);

        // Safety: if init() was never called, storage backend may still be default. Try to align with settings.
        if (!$this->form_id && isset($this->settings)) {
            $backend_pref = $this->settings->getOption('rate_limit_storage', '');
            if ($backend_pref === 'db' && $this->storage_backend !== 'db') {
                $this->determineStorageBackend();
            }
        }

        if ($this->isUsingDb()) {
            // Burst window
            $burst = $this->getDbCounter('burst', $this->rate_burst_ttl);
            $burst['count'] = (isset($burst['count']) ? (int)$burst['count'] : 0) + $count;
            if (empty($burst['first_ts'])) {
                $burst['first_ts'] = time();
            }
            $this->saveDbCounter('burst', $burst);

            // Rolling window
            $window = $this->getDbCounter('window', $this->rate_window_ttl);
            $window['count'] = (isset($window['count']) ? (int)$window['count'] : 0) + $count;
            if (empty($window['first_ts'])) {
                $window['first_ts'] = time();
            }
            $this->saveDbCounter('window', $window);
        } else {
            // Burst window
            $burst = $this->getTransientValueBurst();
            $burst['count'] = (isset($burst['count']) ? (int)$burst['count'] : 0) + $count;
            set_transient($this->transient_key_burst, $burst, $this->rate_burst_ttl);

            // Rolling window
            $window = $this->getTransientValueWindow();
            $window['count'] = (isset($window['count']) ? (int)$window['count'] : 0) + $count;
            set_transient($this->transient_key_window, $window, $this->rate_window_ttl);
        }
        if (isset($this->log)) {
            $this->log->add('info', 'ratelimit', (int) $this->form_id, get_current_user_id(), "Incremented burst/window counters by {$count}", 'security');
        }
    }

    public function markEvent(): void
    {
        $this->markEventBy(1);
    }

    /**
     * Check if current client is rate limited based on configured thresholds.
     */
    public function isRateLimited(): bool
    {
        if (current_user_can('manage_options')) {
            return false;
        }

        // Safety: ensure backend matches settings even if init was skipped for this instance
        if (!$this->form_id && isset($this->settings)) {
            $backend_pref = $this->settings->getOption('rate_limit_storage', '');
            if ($backend_pref === 'db' && $this->storage_backend !== 'db') {
                $this->determineStorageBackend();
            }
        }

        if ($this->isUsingDb()) {
            $burst  = $this->getDbCounter('burst', $this->rate_burst_ttl);
            $window = $this->getDbCounter('window', $this->rate_window_ttl);

            $burst_count  = isset($burst['count'])  ? (int) $burst['count']  : 0;
            $window_count = isset($window['count']) ? (int) $window['count'] : 0;
        } else {
            $burst  = $this->getTransientValueBurst();
            $window = $this->getTransientValueWindow();

            $burst_count  = isset($burst['count'])  ? (int) $burst['count']  : 0;
            $window_count = isset($window['count']) ? (int) $window['count'] : 0;
        }

        if ($this->rate_burst_limit > 0 && $burst_count > $this->rate_burst_limit) {
            if (isset($this->log)) {
                $this->log->add('error', 'ratelimit', (int) $this->form_id, get_current_user_id(), "Rate limit exceeded: burst={$burst_count}, window={$window_count}", 'security');
            }
            return true;
        }
        if ($this->rate_window_limit > 0 && $window_count > $this->rate_window_limit) {
            if (isset($this->log)) {
                $this->log->add('error', 'ratelimit', (int) $this->form_id, get_current_user_id(), "Rate limit exceeded: burst={$burst_count}, window={$window_count}", 'security');
            }
            return true;
        }
        return false;
    }

    /**
     * Set the logical event type (e.g., 'submit', 'upload', 'view') and recompute keys.
     * Defaults to 'submit' if an unknown value is provided.
     */
    public function setEventType(string $event_type = 'submit'): void
    {
        $allowed = ['submit', 'upload', 'view'];
        $this->event_type = in_array($event_type, $allowed, true) ? $event_type : 'submit';
        // Recompute client hash & keys to include event type in the salt
        $this->setClientHash();
        $this->setTransientKeyBurst();
        $this->setTransientKeyWindow();
        // Ensure backend is consistent for this event scope as well
        $this->determineStorageBackend();
    }

    /**
     * Optionally override limits at runtime (per event type / endpoint).
     */
    public function setOverrides(array $overrides): void
    {
        if (isset($overrides['burst_limit'])) {
            $this->rate_burst_limit = max(0, (int) $overrides['burst_limit']);
        }
        if (isset($overrides['burst_ttl'])) {
            $this->rate_burst_ttl = max(1, (int) $overrides['burst_ttl']);
        }
        if (isset($overrides['window_limit'])) {
            $this->rate_window_limit = max(0, (int) $overrides['window_limit']);
        }
        if (isset($overrides['window_ttl'])) {
            $this->rate_window_ttl = max(1, (int) $overrides['window_ttl']);
        }
    }
}