<?php

declare(strict_types=1);

namespace ShieldX\Security;

defined('ABSPATH') || exit;

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

final class LoginSecuritySettings
{
    public const OPTION_NAME = 'shieldx_login_security_settings';
    private const OWNER_RECOVERY_TOKEN_TTL = 3600;
    private const EMERGENCY_RECOVERY_TOKEN_TTL = 7776000;

    /** @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 : []);
        if (function_exists('update_option') && (!is_array($settings) || $settings !== $instance->settings)) {
            update_option(self::OPTION_NAME, $instance->settings, false);
        }

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

        return $instance;
    }

    /**
     * @return array<string,mixed>
     */
    public function all(): array
    {
        return $this->settings;
    }

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

    /**
     * @return array<int,string>
     */
    public function twoFactorRequiredRoles(): array
    {
        return (array) ($this->settings['two_factor_required_roles'] ?? []);
    }

    /**
     * @return array<int,string>
     */
    public function sessionProtectedRoles(): array
    {
        return (array) ($this->settings['session_protected_roles'] ?? []);
    }

    public function sessionPolicyEnabled(): bool
    {
        return !empty($this->settings['session_policy_enabled']);
    }

    public function sessionMaximumAge(): int
    {
        return (int) ($this->settings['session_maximum_age'] ?? 604800);
    }

    public function sessionIdleTimeout(): int
    {
        return (int) ($this->settings['session_idle_timeout'] ?? 7200);
    }

    public function magicLoginEnabled(): bool
    {
        return !empty($this->settings['magic_login_enabled']);
    }

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

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

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

    public function emergencyRecoveryToken(): string
    {
        return self::decryptEmergencyRecoveryToken((string) $this->settings['emergency_recovery_token']);
    }

    public function ownerRecoveryTokenAvailable(): bool
    {
        return $this->ownerRecoveryTokenHash() !== ''
            && (string) ($this->settings['owner_recovery_token_used_at'] ?? '') === ''
            && $this->ownerRecoveryTokenExpiresAt() >= time();
    }

    public function matchesOwnerRecoveryToken(string $token, string $ip = ''): bool
    {
        $token = trim($token);
        $hash = $this->ownerRecoveryTokenHash();
        $expectedIpHash = (string) ($this->settings['owner_recovery_token_ip_hash'] ?? '');
        $actualIpHash = self::hashOwnerRecoveryIp($ip);

        if ($token === '' || $hash === '' || !$this->ownerRecoveryTokenAvailable()) {
            return false;
        }

        if ($expectedIpHash !== '' && ($actualIpHash === '' || !hash_equals($expectedIpHash, $actualIpHash))) {
            return false;
        }

        return hash_equals($hash, self::hashOwnerRecoveryToken($token));
    }

    public function matchesEmergencyRecoveryToken(string $token): bool
    {
        $token = trim($token);
        $hash = $this->emergencyRecoveryTokenHash();
        $createdAt = self::timestampFromDate((string) ($this->settings['emergency_recovery_token_created_at'] ?? ''));

        return $token !== ''
            && $hash !== ''
            && $createdAt > 0
            && $createdAt >= time() - self::EMERGENCY_RECOVERY_TOKEN_TTL
            && hash_equals($hash, self::hashEmergencyRecoveryToken($token));
    }

    public function rotateOwnerRecoveryToken(string $ipHash = ''): string
    {
        $token = TwoFactor::randomToken(32);
        $createdAt = time();
        $this->save([
            'owner_recovery_token_hash' => self::hashOwnerRecoveryToken($token),
            'owner_recovery_token_created_at' => gmdate('c', $createdAt),
            'owner_recovery_token_expires_at' => gmdate('c', $createdAt + self::OWNER_RECOVERY_TOKEN_TTL),
            'owner_recovery_token_used_at' => '',
            'owner_recovery_token_ip_hash' => self::sanitizeHash($ipHash),
        ]);

        return $token;
    }

    public function ensureEmergencyRecoveryToken(): string
    {
        $token = $this->emergencyRecoveryToken();
        if ($token !== '') {
            return $token;
        }

        return $this->emergencyRecoveryTokenHash() !== '' ? '' : $this->rotateEmergencyRecoveryToken();
    }

    public function rotateEmergencyRecoveryToken(): string
    {
        $token = TwoFactor::randomToken(32);
        $this->save([
            'emergency_recovery_token' => self::protectEmergencyRecoveryToken($token),
            'emergency_recovery_token_hash' => self::hashEmergencyRecoveryToken($token),
            'emergency_recovery_token_created_at' => self::now(),
        ]);

        return $token;
    }

    public function markEmergencyRecoveryUsed(): void
    {
        $this->save([
            'emergency_recovery_used_at' => self::now(),
        ]);
        $this->rotateEmergencyRecoveryToken();
    }

    public function clearOwnerRecoveryToken(): void
    {
        $this->save([
            'owner_recovery_token_hash' => '',
            'owner_recovery_token_created_at' => '',
            'owner_recovery_token_expires_at' => '',
            'owner_recovery_token_ip_hash' => '',
        ]);
    }

    public function markOwnerRecoveryTokenUsed(): void
    {
        $this->save([
            'owner_recovery_token_hash' => '',
            'owner_recovery_token_created_at' => '',
            'owner_recovery_token_expires_at' => '',
            'owner_recovery_token_used_at' => self::now(),
            'owner_recovery_token_ip_hash' => '',
        ]);
    }

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

    public static function hashOwnerRecoveryToken(string $token): string
    {
        return hash_hmac('sha256', trim($token), self::tokenSalt());
    }

    public static function hashOwnerRecoveryIp(string $ip): string
    {
        $ip = trim($ip);

        return $ip === '' ? '' : hash_hmac('sha256', $ip, self::tokenSalt() . '|ip');
    }

    public static function hashEmergencyRecoveryToken(string $token): string
    {
        return hash_hmac('sha256', trim($token), self::emergencyTokenSalt());
    }

    public static function defaultRecoveryEmail(): string
    {
        return '';
    }

    /**
     * @param array<string,mixed> $settings
     * @return array<string,mixed>
     */
    private static function normalize(array $settings): array
    {
        $recoveryEmail = self::sanitizeEmail((string) ($settings['recovery_email'] ?? ''));

        if ($recoveryEmail !== '' && $recoveryEmail === self::adminEmail()) {
            $recoveryEmail = '';
        }

        $emergencyTokenInput = (string) ($settings['emergency_recovery_token'] ?? '');
        $emergencyPlainToken = self::decryptEmergencyRecoveryToken($emergencyTokenInput);
        $emergencyToken = CryptoBox::isV2($emergencyTokenInput) && $emergencyPlainToken !== '' ? $emergencyTokenInput : '';
        $emergencyHash = self::sanitizeHash((string) ($settings['emergency_recovery_token_hash'] ?? ''));

        if ($emergencyPlainToken !== '' && $emergencyHash === '') {
            $emergencyHash = self::hashEmergencyRecoveryToken($emergencyPlainToken);
        }
        $emergencyCreatedAt = self::sanitizeDate((string) ($settings['emergency_recovery_token_created_at'] ?? ''));
        if ($emergencyPlainToken !== '' && $emergencyCreatedAt === '') {
            $emergencyCreatedAt = self::now();
        }

        return [
            'enabled' => self::toBool($settings['enabled'] ?? true),
            'two_factor_required_roles' => self::sanitizeRoles($settings['two_factor_required_roles'] ?? []),
            'session_policy_enabled' => self::toBool($settings['session_policy_enabled'] ?? false),
            'session_protected_roles' => self::sanitizeRoles($settings['session_protected_roles'] ?? []),
            'session_maximum_age' => max(3600, min(2592000, (int) ($settings['session_maximum_age'] ?? 604800))),
            'session_idle_timeout' => max(900, min(604800, (int) ($settings['session_idle_timeout'] ?? 7200))),
            'magic_login_enabled' => self::toBool($settings['magic_login_enabled'] ?? false),
            'recovery_email' => $recoveryEmail,
            'owner_recovery_token_hash' => self::sanitizeHash((string) ($settings['owner_recovery_token_hash'] ?? '')),
            'owner_recovery_token_created_at' => self::sanitizeDate((string) ($settings['owner_recovery_token_created_at'] ?? '')),
            'owner_recovery_token_expires_at' => self::normalizeOwnerRecoveryExpiresAt($settings),
            'owner_recovery_token_used_at' => self::sanitizeDate((string) ($settings['owner_recovery_token_used_at'] ?? '')),
            'owner_recovery_token_ip_hash' => self::sanitizeHash((string) ($settings['owner_recovery_token_ip_hash'] ?? '')),
            'emergency_recovery_token' => $emergencyToken,
            'emergency_recovery_token_hash' => $emergencyHash,
            'emergency_recovery_token_created_at' => $emergencyCreatedAt,
            'emergency_recovery_used_at' => self::sanitizeDate((string) ($settings['emergency_recovery_used_at'] ?? '')),
            'activation_email_sent_at' => self::sanitizeDate((string) ($settings['activation_email_sent_at'] ?? '')),
            'last_recovery_email_sent_at' => self::sanitizeDate((string) ($settings['last_recovery_email_sent_at'] ?? '')),
            'recovery_email_status' => self::sanitizeStatus((string) ($settings['recovery_email_status'] ?? 'pending')),
            'recovery_email_error' => self::sanitizeText((string) ($settings['recovery_email_error'] ?? '')),
            'last_magic_login_email_sent_at' => self::sanitizeDate((string) ($settings['last_magic_login_email_sent_at'] ?? '')),
            'magic_login_email_status' => self::sanitizeStatus((string) ($settings['magic_login_email_status'] ?? 'pending')),
            'magic_login_email_error' => self::sanitizeText((string) ($settings['magic_login_email_error'] ?? '')),
        ];
    }

    /**
     * @param mixed $roles
     * @return array<int,string>
     */
    private static function sanitizeRoles($roles): array
    {
        if (is_string($roles)) {
            $roles = preg_split('/[\s,;]+/', $roles) ?: [];
        }
        if (!is_array($roles)) {
            return [];
        }

        $clean = [];
        foreach ($roles as $role) {
            $role = strtolower(trim((string) $role));
            if (preg_match('/^[a-z0-9_][a-z0-9_-]{0,63}$/', $role) === 1) {
                $clean[$role] = $role;
            }
        }
        ksort($clean);

        return array_values($clean);
    }

    /**
     * @param mixed $value
     */
    private static function toBool($value): bool
    {
        if (is_string($value)) {
            return !in_array(strtolower($value), ['0', 'false', 'no', 'off'], true);
        }

        return (bool) $value;
    }

    private static function sanitizeEmail(string $email): string
    {
        $email = trim($email);

        if (function_exists('sanitize_email')) {
            $email = sanitize_email($email);
        }

        return filter_var($email, FILTER_VALIDATE_EMAIL) ? $email : '';
    }

    private static function adminEmail(): string
    {
        $email = function_exists('get_option') ? (string) get_option('admin_email', '') : '';

        return self::sanitizeEmail($email);
    }

    private static function sanitizeHash(string $hash): string
    {
        $hash = trim($hash);

        return preg_match('/^[a-f0-9]{64}$/', $hash) === 1 ? $hash : '';
    }

    private static function sanitizeToken(string $token): string
    {
        $token = trim($token);

        return preg_match('/^[a-f0-9]{32,128}$/', $token) === 1 ? $token : '';
    }

    private static function protectEmergencyRecoveryToken(string $token): string
    {
        $token = self::sanitizeToken($token);
        if ($token === '') {
            return '';
        }

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

    private static function decryptEmergencyRecoveryToken(string $stored): string
    {
        $stored = trim($stored);
        if (CryptoBox::isV2($stored)) {
            try {
                return self::sanitizeToken(CryptoBox::reveal($stored, self::emergencyEncryptionKey()));
            } catch (\Throwable $error) {
                return '';
            }
        }

        return '';
    }

    private static function sanitizeDate(string $date): string
    {
        $date = trim($date);

        return preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/', $date) === 1 ? substr($date, 0, 25) : '';
    }

    private static function timestampFromDate(string $date): int
    {
        $timestamp = strtotime(trim($date));

        return $timestamp !== false ? (int) $timestamp : 0;
    }

    private static function sanitizeStatus(string $status): string
    {
        return in_array($status, ['pending', 'sent', 'failed'], true) ? $status : 'pending';
    }

    private static function sanitizeText(string $text): string
    {
        $text = function_exists('wp_strip_all_tags')
            ? wp_strip_all_tags($text)
            : (preg_replace('/<[^>]*>/', '', $text) ?: '');
        $text = trim($text);
        $text = preg_replace('/[\x00-\x1F\x7F]+/', ' ', $text) ?: '';

        return substr(trim($text), 0, 220);
    }

    private static function now(): string
    {
        if (function_exists('current_time')) {
            $timestamp = strtotime((string) current_time('c', true));
            if ($timestamp !== false) {
                return gmdate('c', $timestamp);
            }
        }

        return gmdate('c');
    }

    /**
     * @param array<string,mixed> $settings
     */
    private static function normalizeOwnerRecoveryExpiresAt(array $settings): string
    {
        $expiresAt = self::sanitizeDate((string) ($settings['owner_recovery_token_expires_at'] ?? ''));
        if ($expiresAt !== '') {
            return $expiresAt;
        }

        $hash = self::sanitizeHash((string) ($settings['owner_recovery_token_hash'] ?? ''));
        $createdAt = self::sanitizeDate((string) ($settings['owner_recovery_token_created_at'] ?? ''));
        if ($hash === '' || $createdAt === '') {
            return '';
        }

        $timestamp = strtotime($createdAt);
        if ($timestamp === false) {
            return '';
        }

        return gmdate('c', $timestamp + self::OWNER_RECOVERY_TOKEN_TTL);
    }

    private function ownerRecoveryTokenExpiresAt(): int
    {
        $expiresAt = (string) ($this->settings['owner_recovery_token_expires_at'] ?? '');
        $timestamp = $expiresAt !== '' ? strtotime($expiresAt) : false;

        return $timestamp === false ? 0 : (int) $timestamp;
    }

    private static function tokenSalt(): string
    {
        $salt = function_exists('wp_salt') ? (string) wp_salt('auth') : '';
        if ($salt === '') {
            throw new \RuntimeException('WordPress authentication salts are required for SHIELD-X recovery tokens.');
        }

        return $salt;
    }

    private static function emergencyTokenSalt(): string
    {
        $salt = function_exists('wp_salt') ? (string) wp_salt('secure_auth') : '';
        if ($salt === '') {
            throw new \RuntimeException('WordPress secure authentication salts are required for SHIELD-X emergency recovery tokens.');
        }

        return $salt;
    }

    private static function emergencyEncryptionKey(): string
    {
        return hash('sha256', self::emergencyTokenSalt(), true);
    }

}
