<?php

declare(strict_types=1);

namespace ShieldX\Security;

defined('ABSPATH') || exit;

use ShieldX\Core\RequestCache;
use ShieldX\Support\CryptoBox;

final class HumanChallengeSettings
{
    public const OPTION_NAME = 'shieldx_human_challenge_settings';
    private const ENCRYPTED_SECRET_PREFIX = 'enc:v2:';

    /** @var array<string,mixed> */
    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<string,mixed>
     */
    public function all(bool $includeSecret = false): array
    {
        $settings = $this->settings;
        $settings['endpoint_url'] = $this->endpointUrl();
        $settings['secret_key_configured'] = $this->secretKey() !== '';
        $settings['configured'] = $this->isConfigured();

        if ($includeSecret) {
            $settings['secret_key_plain'] = $this->secretKey();
        }

        unset($settings['secret_key']);

        return $settings;
    }

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

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

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

    /**
     * Generate the local identifiers SHIELD-X needs to talk to a Cap.js
     * Challenge (Captcha)-compatible endpoint. The Cap.js service can still override these,
     * but admins should not have to invent strong keys by hand.
     *
     * @return array<string,mixed>
     */
    public function generatedStandaloneDefaults(): array
    {
        if (!self::secretStorageReady()) {
            throw new \RuntimeException('SHIELD-X cannot store the Cap.js Challenge (Captcha) secret until WordPress salts and OpenSSL are available.');
        }

        $siteKey = $this->siteKey();
        if ($siteKey === '') {
            $siteKey = self::generateKey('sx-cap-', 18);
        }

        $secretKey = $this->secretKey();
        if ($secretKey === '') {
            $secretKey = self::generateKey('sx-cap-secret-', 32);
        }

        $endpoint = $this->endpointUrl();
        if ($endpoint === '') {
            $endpoint = self::defaultEndpointUrl($siteKey);
        }

        return [
            'provider' => 'cap',
            'endpoint_url' => $endpoint,
            'widget_script_url' => self::defaultWidgetScriptUrl(),
            'site_key' => $siteKey,
            'secret_key' => $secretKey,
        ];
    }

    public function saveGeneratedStandaloneDefaults(): void
    {
        $this->save($this->generatedStandaloneDefaults());
    }

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

    public function endpointUrl(): string
    {
        $endpoint = (string) $this->settings['endpoint_url'];
        $siteKey = $this->siteKey();

        if ($this->provider() === 'cap' && $siteKey !== '' && self::isLocalStandaloneEndpoint($endpoint, $siteKey)) {
            return self::defaultEndpointUrl($siteKey);
        }

        return $endpoint;
    }

    public function widgetScriptUrl(): string
    {
        return self::defaultWidgetScriptUrl();
    }

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

    public function secretKey(): string
    {
        return self::unprotectSecret((string) $this->settings['secret_key']);
    }

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

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

    public function failOpen(): bool
    {
        return $this->failBehavior() === 'fail_open';
    }

    public function enabledForLogin(): bool
    {
        return !empty($this->settings['login_enabled']) && $this->isConfigured();
    }

    public function enabledForLoginFullscreen(): bool
    {
        return !empty($this->settings['login_fullscreen_enabled']) && $this->isConfigured();
    }

    public function enabledForPasswordReset(): bool
    {
        return !empty($this->settings['password_reset_enabled']) && $this->isConfigured();
    }

    public function enabledForWaf(): bool
    {
        return !empty($this->settings['waf_enabled']) && $this->isConfigured();
    }

    public function isConfigured(): bool
    {
        return $this->provider() === 'cap'
            && $this->endpointUrl() !== ''
            && $this->siteKey() !== ''
            && $this->secretKey() !== ''
            && self::secretStorageReady();
    }

    public static function secretStorageReady(): bool
    {
        $auth = function_exists('wp_salt') ? (string) wp_salt('auth') : '';
        $secure = function_exists('wp_salt') ? (string) wp_salt('secure_auth') : '';

        return CryptoBox::isAvailable()
            && $auth !== ''
            && $secure !== '';
    }

    /**
     * @param array<string,mixed> $settings
     * @return array<string,mixed>
     */
    private static function normalize(array $settings): array
    {
        $secretInput = (string) ($settings['secret_key'] ?? '');
        $secret = self::isProtectedSecret($secretInput)
            ? $secretInput
            : self::protectSecret($secretInput);
        $loginEnabled = self::toBool($settings['login_enabled'] ?? false);
        $loginFullscreenEnabled = self::toBool($settings['login_fullscreen_enabled'] ?? false);
        $passwordResetEnabled = self::toBool($settings['password_reset_enabled'] ?? false);
        $wafEnabled = self::toBool($settings['waf_enabled'] ?? false);
        $provider = self::sanitizeProvider((string) ($settings['provider'] ?? 'cap'));
        $siteKey = self::sanitizeKey((string) ($settings['site_key'] ?? ''));

        return [
            'provider' => $provider,
            'endpoint_url' => self::sanitizeEndpointUrl((string) ($settings['endpoint_url'] ?? ''), $siteKey),
            'widget_script_url' => self::defaultWidgetScriptUrl(),
            'site_key' => $siteKey,
            'secret_key' => $secret,
            'timeout_seconds' => max(2, min(20, (int) ($settings['timeout_seconds'] ?? 5))),
            'fail_behavior' => self::sanitizeFailBehavior((string) ($settings['fail_behavior'] ?? 'fail_closed')),
            'login_enabled' => $loginEnabled,
            'login_fullscreen_enabled' => $loginFullscreenEnabled,
            'password_reset_enabled' => $passwordResetEnabled,
            'waf_enabled' => $wafEnabled,
            'last_test_status' => self::sanitizeText((string) ($settings['last_test_status'] ?? '')),
            'last_tested_at' => self::sanitizeText((string) ($settings['last_tested_at'] ?? '')),
        ];
    }

    private static function sanitizeProvider(string $provider): string
    {
        return in_array($provider, ['none', 'cap'], true) ? $provider : 'none';
    }

    private static function sanitizeFailBehavior(string $behavior): string
    {
        return in_array($behavior, ['fail_closed', 'fail_open'], true) ? $behavior : 'fail_closed';
    }

    public static function isSafeRemoteEndpointUrl(string $url): bool
    {
        $url = trim($url);
        if ($url === '' || !filter_var($url, FILTER_VALIDATE_URL)) {
            return false;
        }

        $parts = self::parseUrl($url);
        if (!is_array($parts)) {
            return false;
        }

        $scheme = strtolower((string) ($parts['scheme'] ?? ''));
        $host = strtolower(trim((string) ($parts['host'] ?? ''), "[] \t\n\r\0\x0B"));
        if ($scheme !== 'https' || $host === '') {
            return false;
        }

        if (in_array($host, ['localhost', 'localhost.localdomain'], true)) {
            return false;
        }

        foreach (['.local', '.localhost', '.internal', '.home.arpa'] as $privateSuffix) {
            if (substr($host, -strlen($privateSuffix)) === $privateSuffix) {
                return false;
            }
        }

        if (filter_var($host, FILTER_VALIDATE_IP)) {
            return self::isPublicIp($host);
        }

        if (strpos($host, '.') === false) {
            return false;
        }

        $addresses = self::publicEndpointHostAddresses($host);
        if (is_array($addresses) && !empty($addresses)) {
            foreach ($addresses as $address) {
                if (!self::isPublicIp((string) $address)) {
                    return false;
                }
            }
        }

        return true;
    }

    private static function sanitizeEndpointUrl(string $url, string $siteKey): string
    {
        $url = self::sanitizeUrlWithSchemes($url, ['http', 'https']);
        if ($url === '') {
            return '';
        }

        if ($siteKey !== '' && self::isLocalStandaloneEndpoint($url, $siteKey)) {
            return $url;
        }
        if (self::isCurrentSiteOrigin($url)) {
            return '';
        }

        return self::isSafeRemoteEndpointUrl($url) ? $url : '';
    }

    /**
     * @param array<int,string> $schemes
     */
    private static function sanitizeUrlWithSchemes(string $url, array $schemes): string
    {
        $url = trim($url);

        if ($url === '') {
            return '';
        }

        if (function_exists('esc_url_raw')) {
            $url = esc_url_raw($url, $schemes);
        }

        $parts = self::parseUrl($url);
        if (!is_array($parts) || empty($parts['scheme']) || empty($parts['host'])) {
            return '';
        }

        return in_array(strtolower((string) $parts['scheme']), $schemes, true) ? $url : '';
    }

    /**
     * @return array<string,mixed>|false
     */
    private static function parseUrl(string $url)
    {
        if (!function_exists('wp_parse_url')) {
            return false;
        }

        $parts = wp_parse_url($url);

        return is_array($parts) ? $parts : false;
    }

    private static function isPublicIp(string $ip): bool
    {
        return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;
    }

    /**
     * @return array<int,string>|null
     */
    private static function publicEndpointHostAddresses(string $host): ?array
    {
        if (!function_exists('gethostbynamel')) {
            return null;
        }

        $cacheKey = 'shieldx_human_challenge_dns_' . hash('sha256', strtolower($host));
        if (function_exists('get_transient')) {
            $cached = get_transient($cacheKey);
            if (is_array($cached) && isset($cached['addresses']) && is_array($cached['addresses'])) {
                return array_values(array_filter(array_map('strval', $cached['addresses'])));
            }
        }

        $addresses = gethostbynamel($host);
        $addresses = is_array($addresses) ? array_values(array_filter(array_map('strval', $addresses))) : [];

        if (function_exists('set_transient')) {
            set_transient($cacheKey, ['addresses' => $addresses], 21600);
        }

        return $addresses;
    }

    private static function sanitizeKey(string $key): string
    {
        $key = trim($key);

        if (function_exists('sanitize_text_field')) {
            $key = sanitize_text_field($key);
        }

        return substr($key, 0, 256);
    }

    private static function sanitizeText(string $text): string
    {
        $text = trim($text);

        if (function_exists('sanitize_text_field')) {
            $text = sanitize_text_field($text);
        }

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

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

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

    private static function protectSecret(string $secret): string
    {
        $secret = self::sanitizeKey($secret);
        if ($secret === '' || !self::secretStorageReady()) {
            return '';
        }

        try {
            return CryptoBox::protect($secret, self::encryptionKey());
        } catch (\Throwable $error) {
            return '';
        }
    }

    private static function unprotectSecret(string $payload): string
    {
        if ($payload === '' || !self::secretStorageReady()) {
            return '';
        }

        return CryptoBox::isV2($payload)
            ? self::sanitizeKey(CryptoBox::reveal($payload, self::encryptionKey()))
            : '';
    }

    private static function isProtectedSecret(string $stored): bool
    {
        return CryptoBox::isV2($stored);
    }

    private static function encryptionKey(): string
    {
        return hash('sha256', (string) wp_salt('auth') . '|shieldx-human-challenge-encryption', true);
    }

    private static function generateKey(string $prefix, int $bytes): string
    {
        try {
            return $prefix . bin2hex(random_bytes($bytes));
        } catch (\Throwable $error) {
            throw new \RuntimeException('Secure random generation is unavailable for Cap.js Challenge (Captcha) settings.');
        }
    }

    private static function defaultEndpointUrl(string $siteKey): string
    {
        if (!function_exists('home_url')) {
            return '';
        }

        // Cap.js appends challenge/redeem to this base; keep the route in the
        // query string so it does not depend on PATH_INFO or rewrite rules.
        $endpoint = (string) home_url('/index.php?shieldx_cap_site=' . rawurlencode($siteKey) . '&shieldx_cap_action=/');

        return self::sanitizeUrlWithSchemes($endpoint, ['http', 'https']);
    }

    private static function isLocalStandaloneEndpoint(string $endpoint, string $siteKey): bool
    {
        if ($endpoint === '' || !function_exists('home_url')) {
            return false;
        }

        $endpointParts = self::parseUrl($endpoint);
        $homeParts = self::parseUrl((string) home_url('/'));
        if (!is_array($endpointParts) || !is_array($homeParts)) {
            return false;
        }

        $endpointHost = strtolower((string) ($endpointParts['host'] ?? ''));
        $homeHost = strtolower((string) ($homeParts['host'] ?? ''));
        if ($endpointHost === '' || $homeHost === '') {
            return false;
        }

        if (
            $endpointHost !== $homeHost
            && !in_array($endpointHost, ['localhost', '127.0.0.1', '::1'], true)
            && !in_array($homeHost, ['localhost', '127.0.0.1', '::1'], true)
        ) {
            return false;
        }

        $querySite = self::queryText($endpointParts, 'shieldx_cap_site');
        if ($querySite === '' || !hash_equals($siteKey, $querySite)) {
            return false;
        }

        $path = self::normalizeLocalEndpointPath((string) ($endpointParts['path'] ?? ''), (string) ($homeParts['path'] ?? '/'));
        if ($path !== '/index.php') {
            return false;
        }

        $queryString = isset($endpointParts['query']) && is_string($endpointParts['query']) ? $endpointParts['query'] : '';
        $query = [];
        parse_str($queryString, $query);

        return isset($query['shieldx_cap_action']) && is_scalar($query['shieldx_cap_action']) && (string) $query['shieldx_cap_action'] === '/';
    }

    private static function isCurrentSiteOrigin(string $url): bool
    {
        if (!function_exists('home_url')) {
            return false;
        }

        $endpointParts = self::parseUrl($url);
        $homeParts = self::parseUrl((string) home_url('/'));
        if (!is_array($endpointParts) || !is_array($homeParts)) {
            return false;
        }

        return strtolower((string) ($endpointParts['host'] ?? '')) === strtolower((string) ($homeParts['host'] ?? ''));
    }

    /**
     * @param array<string,mixed> $parts
     */
    private static function queryText(array $parts, string $key): string
    {
        $queryString = isset($parts['query']) && is_string($parts['query']) ? $parts['query'] : '';
        if ($queryString === '') {
            return '';
        }

        $query = [];
        parse_str($queryString, $query);
        $value = $query[$key] ?? '';

        return is_scalar($value) ? self::sanitizeKey((string) $value) : '';
    }

    private static function normalizeLocalEndpointPath(string $path, string $homePath): string
    {
        $path = '/' . ltrim($path !== '' ? $path : '/', '/');
        $homePath = '/' . trim($homePath !== '' ? $homePath : '/', '/');

        if ($homePath !== '/' && strpos($path, rtrim($homePath, '/') . '/') === 0) {
            $path = substr($path, strlen(rtrim($homePath, '/')));
            $path = '/' . ltrim($path, '/');
        }

        if (strpos($path, '/index.php/') === 0) {
            $path = substr($path, strlen('/index.php'));
            $path = '/' . ltrim($path, '/');
        }

        return $path !== '' ? $path : '/';
    }

    private static function defaultWidgetScriptUrl(): string
    {
        if (!defined('SHIELDX_PLUGIN_FILE') || !function_exists('plugins_url')) {
            return '';
        }

        return self::sanitizeUrlWithSchemes((string) plugins_url('assets/vendor/cap/cap.min.js', SHIELDX_PLUGIN_FILE), ['http', 'https']);
    }
}
