<?php

namespace PloverCRM\Common;

/**
 * Single standalone password manager for mobile and external web clients.
 *
 * Each CRM-enabled user has at most one password at a time. The password is
 * stored encrypted at rest so it can be shown again in trusted UI surfaces.
 */
class StandalonePasswordManager
{
    const TABLE_NAME = 'plover_crm_access_passwords';
    const SCHEMA_VERSION_OPTION = 'plover_crm_access_passwords_schema_version';
    const SCHEMA_VERSION = '1';
    const PASSWORD_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789';
    const PASSWORD_LENGTH = 12;
    const USAGE_TOUCH_WINDOW = 900;
    const LOGIN_RATE_LIMIT_IP_PREFIX = 'plover_crm_login_limit_ip_';
    const LOGIN_RATE_LIMIT_USER_PREFIX = 'plover_crm_login_limit_user_';
    const RECOVERY_TRANSIENT_PREFIX = 'plover_crm_password_recovery_';
    const RECOVERY_EMAIL_COOLDOWN_PREFIX = 'plover_crm_password_recovery_cooldown_email_';
    const RECOVERY_IP_LIMIT_PREFIX = 'plover_crm_password_recovery_limit_ip_';
    const ENCRYPTION_CONTEXT = 'plovercrm-standalone-password';
    const RECOVERY_STATUS_VALID = 'valid';
    const RECOVERY_STATUS_INVALID = 'invalid';
    const RECOVERY_STATUS_EXPIRED = 'expired';
    const RECOVERY_STATUS_USED = 'used';
    const RECOVERY_STATUS_ACCESS_REVOKED = 'access_revoked';
    const RECOVERY_STATUS_GENERATION_FAILED = 'generation_failed';
    const RECOVERY_RESULT_SENT = 'sent';
    const RECOVERY_RESULT_COOLDOWN = 'cooldown';
    const RECOVERY_RESULT_SKIPPED = 'skipped';
    const RECOVERY_RESULT_FAILED = 'failed';
    const RECOVERY_EMAIL_COOLDOWN_SECONDS = 60;
    const RECOVERY_IP_LIMIT_MAX = 50;
    const RECOVERY_IP_LIMIT_WINDOW = HOUR_IN_SECONDS;

    public static function init(): void
    {
        self::maybeCreateTable();
        add_action('deleted_user', [self::class, 'handleDeletedUser']);
        add_action('wpmu_delete_user', [self::class, 'handleDeletedUser']);
    }

    public static function getTableName(): string
    {
        global $wpdb;

        return $wpdb->prefix . self::TABLE_NAME;
    }

    public static function maybeCreateTable(): void
    {
        global $wpdb;

        $table = self::getTableName();
        $tableExists = self::tableExists($table);

        if (
            $tableExists &&
            get_option(self::SCHEMA_VERSION_OPTION) === self::SCHEMA_VERSION
        ) {
            return;
        }

        $charsetCollate = $wpdb->get_charset_collate();

        $sql = "CREATE TABLE $table (
            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            user_id bigint(20) unsigned NOT NULL,
            password_encrypted longtext NOT NULL,
            rotated_by_user_id bigint(20) unsigned DEFAULT NULL,
            last_rotated_at datetime NOT NULL,
            last_used_at datetime DEFAULT NULL,
            last_used_ip varchar(45) DEFAULT NULL,
            created_at datetime NOT NULL,
            updated_at datetime NOT NULL,
            PRIMARY KEY (id),
            UNIQUE KEY user_id (user_id)
        ) $charsetCollate;";

        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta($sql);

        if (self::tableExists($table)) {
            update_option(self::SCHEMA_VERSION_OPTION, self::SCHEMA_VERSION, false);
        }
    }

    /**
     * Return the current password bundle for a CRM-enabled user.
     *
     * If a password does not exist yet, one is generated automatically so the
     * rest of the product can rely on a single credential model.
     *
     * @return array|null
     */
    public static function getPasswordBundle(int $userId, array $args = []): ?array
    {
        $user = CrmUserManager::getPublicProfileById($userId);
        $wpUserId = (int) ($user['wp_user_id'] ?? 0);

        if (!$user || $wpUserId <= 0 || !RoleManager::userHasAccess($wpUserId)) {
            return null;
        }

        $row = self::getCredentialRow($wpUserId);

        if (!$row) {
            return self::regeneratePassword($userId, $args);
        }

        $password = self::decryptPassword((string) $row['password_encrypted']);

        if ($password === null) {
            return self::regeneratePassword($userId, $args);
        }

        return self::buildBundle($user, $password, $row);
    }

    /**
     * Replace the existing password with a fresh one.
     *
     * @return array|null
     */
    public static function regeneratePassword(int $userId, array $args = []): ?array
    {
        $user = CrmUserManager::getPublicProfileById($userId);
        $wpUserId = (int) ($user['wp_user_id'] ?? 0);

        if (!$user || $wpUserId <= 0 || !RoleManager::userHasAccess($wpUserId)) {
            return null;
        }

        $password = self::generatePassword();
        $encryptedPassword = self::encryptPassword($password);

        if ($encryptedPassword === null) {
            return null;
        }

        global $wpdb;

        $now = current_time('mysql', true);
        $rotatedByUserId = !empty($args['rotated_by_user_id']) ? (int) $args['rotated_by_user_id'] : null;
        $existing = self::getCredentialRow($wpUserId);
        $saved = false;

        if ($existing) {
            $saved = $wpdb->update(
                self::getTableName(),
                [
                    'password_encrypted' => $encryptedPassword,
                    'rotated_by_user_id' => $rotatedByUserId,
                    'last_rotated_at' => $now,
                    'updated_at' => $now,
                ],
                [
                    'user_id' => $wpUserId,
                ],
                ['%s', '%d', '%s', '%s'],
                ['%d']
            ) !== false;
        } else {
            $saved = $wpdb->insert(
                self::getTableName(),
                [
                    'user_id' => $wpUserId,
                    'password_encrypted' => $encryptedPassword,
                    'rotated_by_user_id' => $rotatedByUserId,
                    'last_rotated_at' => $now,
                    'last_used_at' => null,
                    'last_used_ip' => null,
                    'created_at' => $now,
                    'updated_at' => $now,
                ],
                ['%d', '%s', '%d', '%s', '%s', '%s', '%s', '%s']
            ) !== false;
        }

        if (!$saved) {
            return null;
        }

        $row = self::getCredentialRow($wpUserId);

        if (!$row) {
            return null;
        }

        return self::buildBundle($user, $password, $row);
    }

    public static function hasPassword(int $userId): bool
    {
        $wpUserId = CrmUserManager::getWordPressUserIdByPublicId($userId);

        if (!$wpUserId) {
            return false;
        }

        return self::getCredentialRow($wpUserId) !== null;
    }

    public static function revokePasswordForUser(int $userId): bool
    {
        $wpUserId = CrmUserManager::getWordPressUserIdByPublicId($userId);

        if (!$wpUserId) {
            return false;
        }

        return self::revokePasswordForWordPressUserId($wpUserId);
    }

    /**
     * Revoke standalone credentials when the WordPress user is deleted.
     */
    public static function handleDeletedUser(int $userId): void
    {
        self::revokePasswordForWordPressUserId($userId);
    }

    /**
     * Validate standalone username/password credentials.
     *
     * @return array|null
     */
    public static function validateCredentials(string $username, string $password): ?array
    {
        $username = sanitize_user($username, true);
        $normalizedPassword = self::normalizePassword($password);

        if ($username === '' || $normalizedPassword === '') {
            return null;
        }

        if (self::isLoginRateLimited($username)) {
            return null;
        }

        $crmUser = CrmUserManager::getByUsername($username);

        if (
            !$crmUser ||
            empty($crmUser['wp_user_id']) ||
            !RoleManager::userHasAccess((int) $crmUser['wp_user_id'])
        ) {
            self::recordFailedLoginAttempt($username);
            return null;
        }

        $row = self::getCredentialRow((int) $crmUser['wp_user_id']);

        if (!$row) {
            self::recordFailedLoginAttempt($username);
            return null;
        }

        $storedPassword = self::decryptPassword((string) $row['password_encrypted']);

        if ($storedPassword === null || !hash_equals(self::normalizePassword($storedPassword), $normalizedPassword)) {
            self::recordFailedLoginAttempt($username);
            return null;
        }

        self::clearFailedLoginAttempts($username);
        self::touchCredential($row);

        return [
            'user_id' => (int) $crmUser['wp_user_id'],
            'username' => $crmUser['username'],
        ];
    }

    public static function isLoginRateLimited(string $username): bool
    {
        $username = sanitize_user($username, true);

        if ($username === '') {
            return false;
        }

        return !self::canAttemptLogin($username);
    }

    public static function sendRecoveryLink(string $email): array
    {
        $email = sanitize_email(trim($email));

        if ($email === '') {
            return self::buildRecoveryResult(self::RECOVERY_RESULT_SKIPPED);
        }

        if (!self::checkRecoveryIpLimit()) {
            return self::buildRecoveryResult(self::RECOVERY_RESULT_COOLDOWN);
        }

        $user = CrmUserManager::getByEmail($email);

        if (!$user) {
            $wpUser = get_user_by('email', $email);

            if ($wpUser && user_can($wpUser, 'manage_options')) {
                $user = CrmUserManager::ensureWordPressAdministratorRecord((int) $wpUser->ID);
            }
        }

        if (!$user || !RoleManager::userHasAccess((int) $user['wp_user_id'])) {
            return self::buildRecoveryResult(self::RECOVERY_RESULT_SKIPPED);
        }

        if (self::isRecoveryEmailCoolingDown($email)) {
            return self::buildRecoveryResult(self::RECOVERY_RESULT_COOLDOWN);
        }

        $token = self::generateRecoveryToken((int) ($user['id'] ?? 0));

        if (!$token) {
            return self::buildRecoveryResult(self::RECOVERY_RESULT_FAILED);
        }

        if (!self::sendRecoveryEmail($user, $token)) {
            return self::buildRecoveryResult(self::RECOVERY_RESULT_FAILED);
        }

        self::markRecoveryEmailSent($email);

        return self::buildRecoveryResult(self::RECOVERY_RESULT_SENT);
    }

    public static function validateRecoveryTokenExists(string $token): bool
    {
        return self::getRecoveryTokenStatus($token) === self::RECOVERY_STATUS_VALID;
    }

    public static function getRecoveryTokenStatus(string $token): string
    {
        $data = self::getRecoveryTokenData($token);

        if (!$data) {
            return self::RECOVERY_STATUS_INVALID;
        }

        if (!empty($data['used'])) {
            return self::RECOVERY_STATUS_USED;
        }

        if ((int) ($data['expires'] ?? 0) < time()) {
            delete_transient(self::RECOVERY_TRANSIENT_PREFIX . $token);
            return self::RECOVERY_STATUS_EXPIRED;
        }

        $publicUserId = self::resolveRecoveryPublicUserId($data);

        if (!$publicUserId) {
            delete_transient(self::RECOVERY_TRANSIENT_PREFIX . $token);
            return self::RECOVERY_STATUS_ACCESS_REVOKED;
        }

        $wpUserId = CrmUserManager::getWordPressUserIdByPublicId($publicUserId);

        if (!$wpUserId || !RoleManager::userHasAccess($wpUserId)) {
            delete_transient(self::RECOVERY_TRANSIENT_PREFIX . $token);
            return self::RECOVERY_STATUS_ACCESS_REVOKED;
        }

        return self::RECOVERY_STATUS_VALID;
    }

    public static function getRecoveryTokenMessage(string $status): string
    {
        if ($status === self::RECOVERY_STATUS_USED) {
            return 'This reset link has already been used. Request a new one if you still need to reset your password.';
        }

        if ($status === self::RECOVERY_STATUS_EXPIRED) {
            return 'This reset link has expired. Request a new one from the sign-in screen.';
        }

        if ($status === self::RECOVERY_STATUS_ACCESS_REVOKED) {
            return 'This account no longer has access to the CRM app.';
        }

        if ($status === self::RECOVERY_STATUS_GENERATION_FAILED) {
            return 'We could not create a new password right now. Please request a fresh reset link and try again.';
        }

        return 'This reset link is invalid. Request a new one from the sign-in screen.';
    }

    /**
     * Regenerate a password from a recovery link.
     *
     * @return array|\WP_Error
     */
    public static function regeneratePasswordFromRecovery(string $token)
    {
        $status = self::getRecoveryTokenStatus($token);

        if ($status !== self::RECOVERY_STATUS_VALID) {
            return new \WP_Error('invalid_token', self::getRecoveryTokenMessage($status));
        }

        $data = self::getRecoveryTokenData($token);

        if (!$data) {
            return new \WP_Error('invalid_token', self::getRecoveryTokenMessage(self::RECOVERY_STATUS_INVALID));
        }

        $publicUserId = self::resolveRecoveryPublicUserId($data);

        if (!$publicUserId) {
            delete_transient(self::RECOVERY_TRANSIENT_PREFIX . $token);
            return new \WP_Error('access_revoked', self::getRecoveryTokenMessage(self::RECOVERY_STATUS_ACCESS_REVOKED));
        }

        $issued = self::regeneratePassword($publicUserId, [
            'rotated_by_user_id' => null,
        ]);

        if (!$issued) {
            return new \WP_Error(
                'password_generation_failed',
                self::getRecoveryTokenMessage(self::RECOVERY_STATUS_GENERATION_FAILED)
            );
        }

        $data['used'] = true;
        $data['used_at'] = time();
        $data['public_user_id'] = $publicUserId;
        set_transient(
            self::RECOVERY_TRANSIENT_PREFIX . $token,
            $data,
            self::getRecoveryTokenTtl($data)
        );

        return [
            'user_id' => $publicUserId,
            'password' => $issued['password'],
            'record' => $issued['record'],
            'share' => $issued['share'],
        ];
    }

    private static function buildBundle(array $user, string $password, array $row): array
    {
        return [
            'password' => $password,
            'record' => self::serializeRow($user, $row),
            'share' => self::buildSharePayload($user, $password, $row),
        ];
    }

    private static function serializeRow(array $user, array $row): array
    {
        $rotatedByWpUserId = !empty($row['rotated_by_user_id']) ? (int) $row['rotated_by_user_id'] : null;
        $rotatedByCrmUserId = $rotatedByWpUserId
            ? CrmUserManager::getPublicIdByWordPressUserId($rotatedByWpUserId)
            : null;

        return [
            'user_id' => (int) ($user['id'] ?? 0),
            'crm_user_id' => (int) ($user['id'] ?? 0),
            'wp_user_id' => (int) ($user['wp_user_id'] ?? 0),
            'username' => (string) ($user['username'] ?? ''),
            'display_name' => (string) ($user['display_name'] ?? ''),
            'email' => (string) ($user['email'] ?? ''),
            'created_at' => (string) ($row['created_at'] ?? ''),
            'updated_at' => (string) ($row['updated_at'] ?? ''),
            'last_rotated_at' => (string) ($row['last_rotated_at'] ?? ''),
            'last_used_at' => !empty($row['last_used_at']) ? (string) $row['last_used_at'] : null,
            'last_used_ip' => !empty($row['last_used_ip']) ? (string) $row['last_used_ip'] : null,
            'rotated_by_user_id' => $rotatedByCrmUserId ? (int) $rotatedByCrmUserId : null,
            'rotated_by_wp_user_id' => $rotatedByWpUserId,
        ];
    }

    private static function buildSharePayload(array $user, string $password, array $row): array
    {
        $accessContext = RoleManager::getUserAccessContext((int) ($user['wp_user_id'] ?? 0));
        $roleLabel = self::getRoleLabel($accessContext);
        $siteUrl = home_url('/');
        $lastRotatedAt = !empty($row['last_rotated_at']) ? (string) $row['last_rotated_at'] : '';
        $rotatedTimestamp = $lastRotatedAt !== '' ? strtotime($lastRotatedAt . ' UTC') : false;
        $rotatedHuman = $rotatedTimestamp
            ? wp_date('M j, Y g:i a', $rotatedTimestamp)
            : '';

        return [
            'site_url' => $siteUrl,
            'display_name' => (string) ($user['display_name'] ?? ''),
            'username' => (string) ($user['username'] ?? ''),
            'email' => (string) ($user['email'] ?? ''),
            'role_label' => $roleLabel,
            'last_rotated_at' => $lastRotatedAt,
            'last_rotated_at_human' => $rotatedHuman,
            'message' => self::buildShareMessage([
                'site_url' => $siteUrl,
                'display_name' => (string) ($user['display_name'] ?? ''),
                'username' => (string) ($user['username'] ?? ''),
                'role_label' => $roleLabel,
                'password' => $password,
            ]),
        ];
    }

    private static function buildShareMessage(array $data): string
    {
        return trim(
            "PloverCRM sign-in details\n\n" .
                'URL: ' . $data['site_url'] . "\n" .
                'Username: ' . $data['username'] . "\n" .
                'Password: ' . $data['password']
        );
    }

    private static function getRoleLabel(array $accessContext): string
    {
        if (($accessContext['access']['source'] ?? '') === RoleManager::ACCESS_SOURCE_WORDPRESS_ADMINISTRATOR) {
            return 'WordPress Administrator';
        }

        if (!empty($accessContext['assigned_role']['name'])) {
            return (string) $accessContext['assigned_role']['name'];
        }

        if (!empty($accessContext['effective_role']['name'])) {
            return (string) $accessContext['effective_role']['name'];
        }

        return 'CRM Access';
    }

    private static function getCredentialRow(int $userId): ?array
    {
        global $wpdb;

        $row = $wpdb->get_row(
            $wpdb->prepare(
                'SELECT * FROM ' . self::getTableName() . ' WHERE user_id = %d LIMIT 1',
                $userId
            ),
            ARRAY_A
        );

        return is_array($row) ? $row : null;
    }

    private static function revokePasswordForWordPressUserId(int $wpUserId): bool
    {
        global $wpdb;

        $result = $wpdb->delete(
            self::getTableName(),
            [
                'user_id' => $wpUserId,
            ],
            ['%d']
        );

        return $result !== false;
    }

    private static function touchCredential(array $row): void
    {
        $lastUsedAt = !empty($row['last_used_at']) ? strtotime((string) $row['last_used_at'] . ' UTC') : false;
        $lastUsedIp = !empty($row['last_used_ip']) ? (string) $row['last_used_ip'] : '';
        $currentIp = self::getUserIP();

        if (
            $lastUsedAt &&
            (time() - $lastUsedAt) < self::USAGE_TOUCH_WINDOW &&
            $lastUsedIp === $currentIp
        ) {
            return;
        }

        global $wpdb;

        $now = current_time('mysql', true);
        $wpdb->update(
            self::getTableName(),
            [
                'last_used_at' => $now,
                'last_used_ip' => $currentIp,
                'updated_at' => $now,
            ],
            [
                'user_id' => (int) $row['user_id'],
            ],
            ['%s', '%s', '%s'],
            ['%d']
        );
    }

    private static function canAttemptLogin(string $username): bool
    {
        $ipKey = self::LOGIN_RATE_LIMIT_IP_PREFIX . md5(self::getUserIP());
        $userKey = self::LOGIN_RATE_LIMIT_USER_PREFIX . md5(strtolower($username));
        $ipCount = (int) get_transient($ipKey);
        $userCount = (int) get_transient($userKey);

        return $ipCount < 20 && $userCount < 10;
    }

    private static function recordFailedLoginAttempt(string $username): void
    {
        $ipKey = self::LOGIN_RATE_LIMIT_IP_PREFIX . md5(self::getUserIP());
        $userKey = self::LOGIN_RATE_LIMIT_USER_PREFIX . md5(strtolower($username));
        $ipCount = (int) get_transient($ipKey);
        $userCount = (int) get_transient($userKey);

        set_transient($ipKey, $ipCount + 1, 15 * MINUTE_IN_SECONDS);
        set_transient($userKey, $userCount + 1, 15 * MINUTE_IN_SECONDS);
    }

    private static function clearFailedLoginAttempts(string $username): void
    {
        delete_transient(self::LOGIN_RATE_LIMIT_IP_PREFIX . md5(self::getUserIP()));
        delete_transient(self::LOGIN_RATE_LIMIT_USER_PREFIX . md5(strtolower($username)));
    }

    private static function checkRecoveryIpLimit(): bool
    {
        $ip = self::getUserIP();
        $ipKey = self::RECOVERY_IP_LIMIT_PREFIX . md5($ip);
        $ipCount = (int) get_transient($ipKey);

        if ($ipCount >= self::RECOVERY_IP_LIMIT_MAX) {
            return false;
        }

        set_transient($ipKey, $ipCount + 1, self::RECOVERY_IP_LIMIT_WINDOW);

        return true;
    }

    private static function isRecoveryEmailCoolingDown(string $email): bool
    {
        $cooldownKey = self::RECOVERY_EMAIL_COOLDOWN_PREFIX . md5(strtolower($email));

        return (bool) get_transient($cooldownKey);
    }

    private static function markRecoveryEmailSent(string $email): void
    {
        $cooldownKey = self::RECOVERY_EMAIL_COOLDOWN_PREFIX . md5(strtolower($email));

        set_transient($cooldownKey, 1, self::RECOVERY_EMAIL_COOLDOWN_SECONDS);
    }

    private static function buildRecoveryResult(string $status): array
    {
        return [
            'status' => $status,
        ];
    }

    private static function generateRecoveryToken(int $publicUserId): ?string
    {
        if ($publicUserId <= 0) {
            return null;
        }

        $token = bin2hex(random_bytes(32));

        set_transient(
            self::RECOVERY_TRANSIENT_PREFIX . $token,
            [
                'public_user_id' => $publicUserId,
                'expires' => time() + HOUR_IN_SECONDS,
                'used' => false,
            ],
            HOUR_IN_SECONDS
        );

        return $token;
    }

    private static function sendRecoveryEmail(array $user, string $token): bool
    {
        $siteName = get_bloginfo('name');
        $recoveryUrl = home_url('/wp-json/plover-crm/v1/auth/recover-access?token=' . $token);
        $subject = sprintf('[%s] Reset your PloverCRM password', $siteName);
        $message = trim(
            "Hi {$user['display_name']},\n\n" .
                "We received a request to reset your PloverCRM password.\n\n" .
                "Use this secure link within the next hour to create a new password for the mobile app and web app:\n{$recoveryUrl}\n\n" .
                "Your previous password will stop working as soon as you complete the reset.\n\n" .
                "If you did not request this, you can safely ignore this email."
        );

        return wp_mail((string) $user['email'], $subject, $message);
    }

    private static function generatePassword(): string
    {
        $alphabet = self::PASSWORD_ALPHABET;
        $maxIndex = strlen($alphabet) - 1;
        $password = '';

        for ($index = 0; $index < self::PASSWORD_LENGTH; $index++) {
            $password .= $alphabet[random_int(0, $maxIndex)];
        }

        return $password;
    }

    private static function normalizePassword(string $password): string
    {
        return trim($password);
    }

    private static function encryptPassword(string $password): ?string
    {
        if (function_exists('sodium_crypto_secretbox')) {
            try {
                $key = self::getSodiumKey();
                $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
                $ciphertext = sodium_crypto_secretbox($password, $nonce, $key);

                return wp_json_encode([
                    'version' => 1,
                    'engine' => 'sodium',
                    'nonce' => base64_encode($nonce),
                    'ciphertext' => base64_encode($ciphertext),
                ]);
            } catch (\Throwable $exception) {
                // Fall through to OpenSSL.
            }
        }

        if (!function_exists('openssl_encrypt')) {
            return null;
        }

        $ivLength = openssl_cipher_iv_length('aes-256-gcm');

        if (!$ivLength) {
            return null;
        }

        try {
            $iv = random_bytes($ivLength);
            $tag = '';
            $ciphertext = openssl_encrypt(
                $password,
                'aes-256-gcm',
                self::getEncryptionKey(),
                OPENSSL_RAW_DATA,
                $iv,
                $tag,
                self::ENCRYPTION_CONTEXT
            );

            if ($ciphertext === false) {
                return null;
            }

            return wp_json_encode([
                'version' => 1,
                'engine' => 'openssl',
                'cipher' => 'aes-256-gcm',
                'iv' => base64_encode($iv),
                'tag' => base64_encode($tag),
                'ciphertext' => base64_encode($ciphertext),
            ]);
        } catch (\Throwable $exception) {
            return null;
        }
    }

    private static function decryptPassword(string $payload): ?string
    {
        $decoded = json_decode($payload, true);

        if (!is_array($decoded) || empty($decoded['engine'])) {
            return null;
        }

        if ($decoded['engine'] === 'sodium' && function_exists('sodium_crypto_secretbox_open')) {
            try {
                $plaintext = sodium_crypto_secretbox_open(
                    base64_decode((string) ($decoded['ciphertext'] ?? ''), true),
                    base64_decode((string) ($decoded['nonce'] ?? ''), true),
                    self::getSodiumKey()
                );

                return is_string($plaintext) ? $plaintext : null;
            } catch (\Throwable $exception) {
                return null;
            }
        }

        if (
            $decoded['engine'] === 'openssl' &&
            function_exists('openssl_decrypt')
        ) {
            $iv = base64_decode((string) ($decoded['iv'] ?? ''), true);
            $tag = base64_decode((string) ($decoded['tag'] ?? ''), true);
            $ciphertext = base64_decode((string) ($decoded['ciphertext'] ?? ''), true);

            if (!is_string($iv) || !is_string($tag) || !is_string($ciphertext)) {
                return null;
            }

            $plaintext = openssl_decrypt(
                $ciphertext,
                (string) ($decoded['cipher'] ?? 'aes-256-gcm'),
                self::getEncryptionKey(),
                OPENSSL_RAW_DATA,
                $iv,
                $tag,
                self::ENCRYPTION_CONTEXT
            );

            return is_string($plaintext) ? $plaintext : null;
        }

        return null;
    }

    private static function getSodiumKey(): string
    {
        return hash(
            'sha256',
            wp_salt('secure_auth') . '|' . self::ENCRYPTION_CONTEXT,
            true
        );
    }

    private static function getEncryptionKey(): string
    {
        return hash(
            'sha256',
            wp_salt('auth') . '|' . self::ENCRYPTION_CONTEXT,
            true
        );
    }

    private static function tableExists(string $table): bool
    {
        global $wpdb;

        $like = $wpdb->esc_like($table);

        return $wpdb->get_var(
            $wpdb->prepare('SHOW TABLES LIKE %s', $like)
        ) === $table;
    }

    private static function getUserIP(): string
    {
        return IpHandler::getRealUserIp();
    }

    private static function getRecoveryTokenData(string $token): ?array
    {
        $data = get_transient(self::RECOVERY_TRANSIENT_PREFIX . $token);

        return is_array($data) ? $data : null;
    }

    private static function resolveRecoveryPublicUserId(array $data): ?int
    {
        $publicUserId = (int) ($data['public_user_id'] ?? 0);

        if ($publicUserId > 0) {
            return $publicUserId;
        }

        $legacyWordPressUserId = (int) ($data['user_id'] ?? 0);

        if ($legacyWordPressUserId <= 0) {
            return null;
        }

        return CrmUserManager::getPublicIdByWordPressUserId($legacyWordPressUserId);
    }

    private static function getRecoveryTokenTtl(array $data): int
    {
        $expiresAt = (int) ($data['expires'] ?? 0);

        return max(MINUTE_IN_SECONDS, $expiresAt - time());
    }
}
