<?php

declare(strict_types=1);

namespace ShieldX\Security;

defined('ABSPATH') || exit;

use ShieldX\Core\RequestCache;

final class HardeningSettings
{
    public const OPTION_NAME = 'shieldx_hardening_settings';
    public const RESERVED_ADMIN_GATE_KEY = 'secretpath';

    public const RESPONSE_HEADER_MIME_TYPE_PROTECTION = 'mime_type_protection';
    public const RESPONSE_HEADER_FRAME_EMBEDDING_PROTECTION = 'frame_embedding_protection';
    public const RESPONSE_HEADER_REFERRER_PRIVACY = 'referrer_privacy';
    public const RESPONSE_HEADER_CROSS_DOMAIN_POLICY = 'cross_domain_policy';
    public const RESPONSE_HEADER_HIDE_POWERED_BY = 'hide_powered_by';
    public const RESPONSE_HEADER_HSTS = 'hsts';
    public const RESPONSE_HEADER_PERMISSIONS_POLICY = 'permissions_policy';

    /**
     * @var array<string,bool|string|array<string,bool>>
     */
    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;
        }

        $savedSettings = function_exists('get_option') ? get_option(self::OPTION_NAME, null) : null;
        $settings = is_array($savedSettings) ? $savedSettings : [];
        $savedKey = array_key_exists('admin_gate_key', $settings) ? self::sanitizeAdminGateKey((string) $settings['admin_gate_key']) : '';
        $needsGeneratedKey = $savedKey === '' || self::isReservedAdminGateKey($savedKey);
        $needsResponseHeaderUpgrade = is_array($savedSettings)
            && (!array_key_exists('response_headers', $settings)
                || array_key_exists('security_headers_enabled', $settings)
                || array_key_exists('hsts_enabled', $settings));

        $instance = new self($settings);

        if (($needsGeneratedKey || $needsResponseHeaderUpgrade) && function_exists('update_option')) {
            update_option(self::OPTION_NAME, $instance->settings, false);
        }

        RequestCache::rememberOption(self::class, self::OPTION_NAME, $instance);

        return $instance;
    }

    /**
     * @return array<string,bool|string|array<string,bool>>
     */
    public function all(): array
    {
        return [
            'hide_wordpress_signature' => $this->hideWordPressSignature(),
            'force_ssl_enabled' => $this->forceSslEnabled(),
            'disable_file_edit' => $this->disableFileEdit(),
            'response_headers' => $this->responseHeaders(),
            'generic_login_errors' => $this->genericLoginErrors(),
            'block_xmlrpc' => $this->blockXmlRpc(),
            'disable_pingbacks' => $this->disablePingbacks(),
            'disable_admin_scripts_concatenation' => $this->disableAdminScriptsConcatenation(),
            'block_author_scans' => $this->blockAuthorScans(),
            'bot_protection_enabled' => $this->botProtectionEnabled(),
            'direct_core_access_protection' => $this->directCoreAccessProtection(),
            'sensitive_file_protection' => $this->sensitiveFileProtection(),
            'directory_browsing_protection' => $this->directoryBrowsingProtection(),
            'data_php_execution_protection' => $this->dataPhpExecutionProtection(),
            'admin_gate_enabled' => $this->adminGateEnabled(),
            'admin_gate_key' => $this->adminGateKey(),
            'admin_gate_strict_cookie' => $this->adminGateStrictCookie(),
            'admin_entry_url' => $this->adminEntryUrl(),
        ];
    }

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

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

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

    /**
     * @return array<string,bool>
     */
    public function responseHeaders(): array
    {
        return $this->settings['response_headers'];
    }

    public function responseHeaderEnabled(string $header): bool
    {
        return in_array($header, self::responseHeaderKeys(), true)
            && !empty($this->settings['response_headers'][$header]);
    }

    public function hasResponseHeaders(): bool
    {
        return in_array(true, $this->settings['response_headers'], true);
    }

    /**
     * @return array<int,string>
     */
    public static function responseHeaderKeys(): array
    {
        return array_keys(self::defaultResponseHeaders());
    }

    /**
     * @return array<string,bool>
     */
    public static function defaultResponseHeaders(): array
    {
        return [
            self::RESPONSE_HEADER_MIME_TYPE_PROTECTION => true,
            self::RESPONSE_HEADER_FRAME_EMBEDDING_PROTECTION => true,
            self::RESPONSE_HEADER_REFERRER_PRIVACY => true,
            self::RESPONSE_HEADER_CROSS_DOMAIN_POLICY => true,
            self::RESPONSE_HEADER_HIDE_POWERED_BY => true,
            self::RESPONSE_HEADER_HSTS => false,
            self::RESPONSE_HEADER_PERMISSIONS_POLICY => false,
        ];
    }

    /**
     * @param array<int,string> $selected
     * @return array<string,bool>
     */
    public static function responseHeadersFromSelection(array $selected): array
    {
        $selected = array_flip($selected);
        $headers = [];

        foreach (self::responseHeaderKeys() as $header) {
            $headers[$header] = isset($selected[$header]);
        }

        return $headers;
    }

    /**
     * @param array<string,mixed> $headers
     */
    public static function defaultResponseHeadersEnabled(array $headers): bool
    {
        foreach (self::defaultResponseHeaders() as $header => $enabledByDefault) {
            if ($enabledByDefault && empty($headers[$header])) {
                return false;
            }
        }

        return true;
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

    public function adminEntryUrl(): string
    {
        $path = '?' . rawurlencode($this->adminGateKey());

        return function_exists('admin_url') ? admin_url('/' . $path) : '/wp-admin/' . $path;
    }

    /**
     * @param array<string,mixed>|null $advancedSettings
     */
    public function save(bool $hideSignature, bool $adminGateEnabled, string $adminGateKey, bool $forceSslEnabled, bool $disableFileEdit, array $responseHeaders, ?array $advancedSettings = null): void
    {
        $advancedSettings = $advancedSettings === null ? $this->advancedSettings() : $advancedSettings;
        $adminGateKey = self::sanitizeAdminGateKey($adminGateKey);
        if ($adminGateKey === '' || ($adminGateEnabled && self::isReservedAdminGateKey($adminGateKey))) {
            throw new \InvalidArgumentException('A valid unique SHIELD-X admin gate keyword is required.');
        }

        $this->settings = self::normalize(array_merge([
            'hide_wordpress_signature' => $hideSignature,
            'force_ssl_enabled' => $forceSslEnabled,
            'disable_file_edit' => $disableFileEdit,
            'response_headers' => $responseHeaders,
            'admin_gate_enabled' => $adminGateEnabled,
            'admin_gate_key' => $adminGateKey,
        ], $advancedSettings));

        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<string,bool|string|array<string,bool>>
     */
    private static function normalize(array $settings): array
    {
        return [
            'hide_wordpress_signature' => array_key_exists('hide_wordpress_signature', $settings)
                ? self::toBool($settings['hide_wordpress_signature'])
                : true,
            'force_ssl_enabled' => array_key_exists('force_ssl_enabled', $settings)
                ? self::toBool($settings['force_ssl_enabled'])
                : false,
            'disable_file_edit' => array_key_exists('disable_file_edit', $settings)
                ? self::toBool($settings['disable_file_edit'])
                : true,
            'response_headers' => self::normalizeResponseHeaders($settings),
            'generic_login_errors' => array_key_exists('generic_login_errors', $settings)
                ? self::toBool($settings['generic_login_errors'])
                : false,
            'block_xmlrpc' => array_key_exists('block_xmlrpc', $settings)
                ? self::toBool($settings['block_xmlrpc'])
                : false,
            'disable_pingbacks' => array_key_exists('disable_pingbacks', $settings)
                ? self::toBool($settings['disable_pingbacks'])
                : false,
            'disable_admin_scripts_concatenation' => array_key_exists('disable_admin_scripts_concatenation', $settings)
                ? self::toBool($settings['disable_admin_scripts_concatenation'])
                : false,
            'block_author_scans' => array_key_exists('block_author_scans', $settings)
                ? self::toBool($settings['block_author_scans'])
                : false,
            'bot_protection_enabled' => array_key_exists('bot_protection_enabled', $settings)
                ? self::toBool($settings['bot_protection_enabled'])
                : false,
            'direct_core_access_protection' => array_key_exists('direct_core_access_protection', $settings)
                ? self::toBool($settings['direct_core_access_protection'])
                : false,
            'sensitive_file_protection' => array_key_exists('sensitive_file_protection', $settings)
                ? self::toBool($settings['sensitive_file_protection'])
                : false,
            'directory_browsing_protection' => array_key_exists('directory_browsing_protection', $settings)
                ? self::toBool($settings['directory_browsing_protection'])
                : false,
            'data_php_execution_protection' => array_key_exists('data_php_execution_protection', $settings)
                ? self::toBool($settings['data_php_execution_protection'])
                : false,
            // Keep the admin gate opt-in so fresh installs do not look broken
            // when someone opens the normal /wp-admin/ URL before setup.
            'admin_gate_enabled' => array_key_exists('admin_gate_enabled', $settings)
                ? self::toBool($settings['admin_gate_enabled'])
                : false,
            'admin_gate_key' => array_key_exists('admin_gate_key', $settings)
                ? (self::sanitizeAdminGateKey((string) $settings['admin_gate_key']) ?: self::generateAdminGateKey())
                : self::generateAdminGateKey(),
            'admin_gate_strict_cookie' => array_key_exists('admin_gate_strict_cookie', $settings)
                ? self::toBool($settings['admin_gate_strict_cookie'])
                : false,
        ];
    }

    /**
     * @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);
    }

    public static function isReservedAdminGateKey(string $key): bool
    {
        return self::sanitizeAdminGateKey($key) === self::RESERVED_ADMIN_GATE_KEY;
    }

    public static function isValidAdminGateKey(string $key): bool
    {
        return self::sanitizeAdminGateKey($key) !== '';
    }

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

        if (function_exists('sanitize_key')) {
            $key = sanitize_key($key);
        } else {
            $key = strtolower((string) preg_replace('/[^a-zA-Z0-9_-]/', '', $key));
        }

        return preg_match('/^[a-z0-9_-]{3,48}$/', $key) === 1 ? $key : '';
    }

    private static function generateAdminGateKey(): string
    {
        try {
            return 'sx-' . bin2hex(random_bytes(8));
        } catch (\Throwable $error) {
            if (function_exists('wp_generate_password')) {
                $generated = sanitize_key((string) wp_generate_password(20, false, false));
                if ($generated !== '') {
                    return $generated;
                }
            }
        }

        return '';
    }

    /**
     * @return array<string,bool>
     */
    private function advancedSettings(): array
    {
        return [
            'block_xmlrpc' => $this->blockXmlRpc(),
            'generic_login_errors' => $this->genericLoginErrors(),
            'disable_pingbacks' => $this->disablePingbacks(),
            'disable_admin_scripts_concatenation' => $this->disableAdminScriptsConcatenation(),
            'block_author_scans' => $this->blockAuthorScans(),
            'bot_protection_enabled' => $this->botProtectionEnabled(),
            'direct_core_access_protection' => $this->directCoreAccessProtection(),
            'sensitive_file_protection' => $this->sensitiveFileProtection(),
            'directory_browsing_protection' => $this->directoryBrowsingProtection(),
            'data_php_execution_protection' => $this->dataPhpExecutionProtection(),
            'admin_gate_strict_cookie' => $this->adminGateStrictCookie(),
        ];
    }

    /**
     * @param array<string,mixed> $settings
     * @return array<string,bool>
     */
    private static function normalizeResponseHeaders(array $settings): array
    {
        $defaults = self::defaultResponseHeaders();
        $configured = isset($settings['response_headers']) && is_array($settings['response_headers'])
            ? $settings['response_headers']
            : null;

        if ($configured === null) {
            // Existing installations receive the full low-risk baseline. HSTS
            // remains opt-in and keeps its prior explicit setting.
            $defaults[self::RESPONSE_HEADER_HSTS] = array_key_exists('hsts_enabled', $settings)
                ? self::toBool($settings['hsts_enabled'])
                : false;

            return $defaults;
        }

        foreach ($defaults as $header => $default) {
            $defaults[$header] = array_key_exists($header, $configured)
                ? self::toBool($configured[$header])
                : $default;
        }

        return $defaults;
    }
}
