<?php

declare(strict_types=1);

namespace ShieldX\Security;

defined('ABSPATH') || exit;

use ShieldX\Core\RequestCache;

final class RateLimitSettings
{
    public const OPTION_NAME = 'shieldx_rate_limit_settings';
    public const ENDPOINT_PROFILES = [
        'login_post',
        'xmlrpc_post',
        'rest_user_enumeration',
        'comment_submission',
        'search',
    ];

    /**
     * @var array{request_rate_enabled:bool,request_rate_mode:string,request_limit:int,request_window_seconds:int,burst_limit:int,burst_window_seconds:int,login_protection_enabled:bool,login_failure_limit:int,login_window_seconds:int,login_lockout_seconds:int}
     */
    private $settings;

    /**
     * @param array<string,mixed> $settings
     */
    public function __construct(array $settings = [])
    {
        $this->settings = self::normalize($settings);
    }

    public static function fromWordPress(): self
    {
        $cached = RequestCache::get(self::class);
        if ($cached instanceof self) {
            return $cached;
        }

        $settings = function_exists('get_option') ? get_option(self::OPTION_NAME, []) : [];

        $instance = new self(is_array($settings) ? $settings : []);
        RequestCache::rememberOption(self::class, self::OPTION_NAME, $instance);

        return $instance;
    }

    /**
     * @return array{request_rate_enabled:bool,request_rate_mode:string,request_limit:int,request_window_seconds:int,burst_limit:int,burst_window_seconds:int,login_protection_enabled:bool,login_failure_limit:int,login_window_seconds:int,login_lockout_seconds:int}
     */
    public function all(): array
    {
        return $this->settings;
    }

    public function requestRateEnabled(): bool
    {
        return $this->settings['request_rate_enabled'];
    }

    public function requestRateMode(): string
    {
        return $this->settings['request_rate_mode'];
    }

    public function requestLimit(): int
    {
        return $this->settings['request_limit'];
    }

    public function requestWindowSeconds(): int
    {
        return $this->settings['request_window_seconds'];
    }

    public function burstLimit(): int
    {
        return $this->settings['burst_limit'];
    }

    public function burstWindowSeconds(): int
    {
        return $this->settings['burst_window_seconds'];
    }

    public function loginProtectionEnabled(): bool
    {
        return $this->settings['login_protection_enabled'];
    }

    public function loginFailureLimit(): int
    {
        return $this->settings['login_failure_limit'];
    }

    public function loginWindowSeconds(): int
    {
        return $this->settings['login_window_seconds'];
    }

    public function loginLockoutSeconds(): int
    {
        return $this->settings['login_lockout_seconds'];
    }

    /**
     * @return array<string,array{enabled:bool,mode:string,limit:int,window_seconds:int,burst_limit:int,burst_window_seconds:int}>
     */
    public function endpointProfiles(): array
    {
        return (array) ($this->settings['endpoint_profiles'] ?? []);
    }

    /**
     * @return array{enabled:bool,mode:string,limit:int,window_seconds:int,burst_limit:int,burst_window_seconds:int}
     */
    public function endpointProfile(string $profile): array
    {
        $profiles = $this->endpointProfiles();

        return isset($profiles[$profile]) && is_array($profiles[$profile])
            ? $profiles[$profile]
            : self::normalizeEndpointProfile([]);
    }

    /**
     * @param array<string,mixed> $settings
     */
    public function save(array $settings): void
    {
        $this->settings = self::normalize($settings);

        if (function_exists('update_option')) {
            update_option(self::OPTION_NAME, $this->settings, false);
        }

        RequestCache::rememberOption(self::class, self::OPTION_NAME, $this);
    }

    /**
     * @param array<string,mixed> $settings
     * @return array{request_rate_enabled:bool,request_rate_mode:string,request_limit:int,request_window_seconds:int,burst_limit:int,burst_window_seconds:int,login_protection_enabled:bool,login_failure_limit:int,login_window_seconds:int,login_lockout_seconds:int}
     */
    private static function normalize(array $settings): array
    {
        $loginWindowSeconds = self::boundedInt($settings['login_window_seconds'] ?? 900, 60, 86400);
        $loginLockoutSeconds = self::boundedInt($settings['login_lockout_seconds'] ?? 900, 60, 86400);
        $loginLockoutSeconds = max($loginLockoutSeconds, $loginWindowSeconds);

        return [
            'request_rate_enabled' => array_key_exists('request_rate_enabled', $settings)
                ? self::toBool($settings['request_rate_enabled'])
                : true,
            'request_rate_mode' => self::sanitizeMode((string) ($settings['request_rate_mode'] ?? 'log_only')),
            'request_limit' => self::boundedInt($settings['request_limit'] ?? 120, 10, 5000),
            'request_window_seconds' => self::boundedInt($settings['request_window_seconds'] ?? 60, 10, 3600),
            'burst_limit' => self::boundedInt($settings['burst_limit'] ?? 40, 5, 1000),
            'burst_window_seconds' => self::boundedInt($settings['burst_window_seconds'] ?? 10, 5, 300),
            'login_protection_enabled' => array_key_exists('login_protection_enabled', $settings)
                ? self::toBool($settings['login_protection_enabled'])
                : true,
            'login_failure_limit' => self::boundedInt($settings['login_failure_limit'] ?? 5, 2, 50),
            'login_window_seconds' => $loginWindowSeconds,
            'login_lockout_seconds' => $loginLockoutSeconds,
            'endpoint_profiles' => self::normalizeEndpointProfiles($settings['endpoint_profiles'] ?? []),
        ];
    }

    /**
     * @param mixed $profiles
     * @return array<string,array{enabled:bool,mode:string,limit:int,window_seconds:int,burst_limit:int,burst_window_seconds:int}>
     */
    private static function normalizeEndpointProfiles($profiles): array
    {
        $profiles = is_array($profiles) ? $profiles : [];
        $normalized = [];

        foreach (self::ENDPOINT_PROFILES as $profile) {
            $normalized[$profile] = self::normalizeEndpointProfile(
                isset($profiles[$profile]) && is_array($profiles[$profile]) ? $profiles[$profile] : []
            );
        }

        return $normalized;
    }

    /**
     * @param array<string,mixed> $profile
     * @return array{enabled:bool,mode:string,limit:int,window_seconds:int,burst_limit:int,burst_window_seconds:int}
     */
    private static function normalizeEndpointProfile(array $profile): array
    {
        return [
            'enabled' => self::toBool($profile['enabled'] ?? false),
            'mode' => self::sanitizeMode((string) ($profile['mode'] ?? 'log_only')),
            'limit' => self::boundedInt($profile['limit'] ?? 30, 2, 5000),
            'window_seconds' => self::boundedInt($profile['window_seconds'] ?? 60, 10, 3600),
            'burst_limit' => self::boundedInt($profile['burst_limit'] ?? 10, 2, 1000),
            'burst_window_seconds' => self::boundedInt($profile['burst_window_seconds'] ?? 10, 5, 300),
        ];
    }

    /**
     * @param mixed $value
     */
    private static function toBool($value): bool
    {
        if (is_bool($value)) {
            return $value;
        }

        return in_array((string) $value, ['1', 'true', 'yes', 'on'], true);
    }

    private static function sanitizeMode(string $mode): string
    {
        return in_array($mode, ['log_only', 'enforce'], true) ? $mode : 'log_only';
    }

    /**
     * @param mixed $value
     */
    private static function boundedInt($value, int $min, int $max): int
    {
        $number = is_numeric($value) ? (int) $value : $min;

        return max($min, min($max, $number));
    }
}
