<?php
/**
 * Abstract Two-Factor Authentication Provider
 *
 * Base class for all 2FA providers that handles common functionality like
 * encryption, user meta management, and interface implementation patterns.
 *
 * @package     WPUltimateSecurity\UltimateSecurity\Modules\Authentication\TwoFactor\Providers
 * @since       1.0.0
 */

declare(strict_types=1);

namespace WPUltimateSecurity\UltimateSecurity\Modules\Authentication\TwoFactor\Providers;

use WPUltimateSecurity\UltimateSecurity\Contracts\TwoFactorProviderInterface;
use WPUltimateSecurity\UltimateSecurity\Modules\Authentication\TwoFactor\Services\EncryptionService;

/**
 * Abstract base class for Two-Factor Authentication providers
 *
 * Provides common functionality and patterns for 2FA providers including:
 * - Secret encryption/decryption
 * - User meta management
 * - Standard interface method implementations
 *
 * Concrete providers should extend this class and implement the abstract methods.
 *
 * @abstract
 * @implements TwoFactorProviderInterface
 */
abstract class AbstractTwoFactorProvider implements TwoFactorProviderInterface
{
    // =========================================================================
    // Abstract Methods - Must be implemented by concrete providers
    // =========================================================================

    /**
     * Get the provider's unique slug identifier
     *
     * @return string Provider slug (e.g., 'totp', 'hotp', 'email')
     */
    abstract protected function getSlug(): string;

    /**
     * Get the settings key for this provider
     *
     * @return string Settings key (e.g., '2fa_totp', '2fa_email')
     */
    abstract protected function getSettings(): string;

    /**
     * Get the user meta key for storing the secret
     *
     * @return string Meta key (e.g., 'ultimate_security_2fa_totp_secret')
     */
    abstract protected function getSecretMetaKey(): string;

    /**
     * Generate a new secret for the user
     *
     * @param int $userId User ID
     * @return string|false The raw secret or false on failure
     */
    abstract protected function generateSecret(int $userId);

    /**
     * Perform provider-specific code verification
     *
     * @param int    $userId User ID
     * @param string $code   The code to verify
     * @param string $secret The decrypted secret
     * @return bool True if code is valid
     */
    abstract protected function doVerifyCode(int $userId, string $code, string $secret): bool;

    // =========================================================================
    // TwoFactorProviderInterface Implementation
    // =========================================================================

    /**
     * {@inheritdoc}
     */
    public function isEnabledForUser(int $userId): bool
    {
        return $this->hasSecret($userId);
    }

    /**
     * {@inheritdoc}
     */
    public function enableForUser(int $userId): bool
    {
        // Check if secret already exists
        if ($this->hasSecret($userId)) {
            return true;
        }

        // Generate new secret
        $secret = $this->generateSecret($userId);
        if ($secret === false) {
            return false;
        }

        // Store encrypted secret
        return $this->storeSecret($userId, $secret);
    }

    /**
     * {@inheritdoc}
     */
    public function disableForUser(int $userId): bool
    {
        return $this->deleteSecret($userId);
    }

    /**
     * {@inheritdoc}
     *
     * Default implementation for app-based providers (no code sending needed).
     * Override in providers that need to send codes (e.g., Email, SMS).
     */
    public function sendCode(int $userId): bool
    {
        // App-based providers don't need to send codes
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function verifyCode(int $userId, string $code): bool
    {
        $secret = $this->getDecryptedSecret($userId);
        if ($secret === false) {
            return false;
        }

        // Sanitize code - remove whitespace
        $code = preg_replace('/\s+/', '', $code);

        return $this->doVerifyCode($userId, $code, $secret);
    }

    /**
     * {@inheritdoc}
     */
    public function getProviderSlug(): string
    {
        return $this->getSlug();
    }

    /**
     * {@inheritdoc}
     */
    public function getSettingsKey(): string
    {
        return $this->getSettings();
    }

    // =========================================================================
    // Secret Management Methods
    // =========================================================================

    /**
     * Check if user has a secret stored
     *
     * @param int $userId User ID
     * @return bool True if secret exists
     */
    protected function hasSecret(int $userId): bool
    {
        $secret = get_user_meta($userId, $this->getSecretMetaKey(), true);
        return !empty($secret);
    }

    /**
     * Get the decrypted secret for a user
     *
     * @param int $userId User ID
     * @return string|false The decrypted secret or false
     */
    protected function getDecryptedSecret(int $userId)
    {
        $encryptedSecret = get_user_meta($userId, $this->getSecretMetaKey(), true);

        if (empty($encryptedSecret)) {
            return false;
        }

        // Decrypt if encryption is enabled
        if (EncryptionService::is_encryption_enabled()) {
            $decrypted = EncryptionService::decrypt($encryptedSecret);
            return $decrypted !== false ? $decrypted : false;
        }

        return $encryptedSecret;
    }

    /**
     * Store an encrypted secret for a user
     *
     * @param int    $userId User ID
     * @param string $secret Raw secret to store
     * @return bool True on success
     */
    protected function storeSecret(int $userId, string $secret): bool
    {
        $valueToStore = $secret;

        // Encrypt if encryption is enabled
        if (EncryptionService::is_encryption_enabled()) {
            $encrypted = EncryptionService::encrypt($secret);
            if ($encrypted === false) {
                return false;
            }
            $valueToStore = $encrypted;
        }

        return (bool) update_user_meta($userId, $this->getSecretMetaKey(), $valueToStore);
    }

    /**
     * Delete the secret for a user
     *
     * @param int $userId User ID
     * @return bool True on success
     */
    protected function deleteSecret(int $userId): bool
    {
        return delete_user_meta($userId, $this->getSecretMetaKey());
    }

    /**
     * Get additional meta keys to clean up when disabling
     *
     * Override in providers that store additional user meta.
     *
     * @return array Array of meta keys to delete
     */
    protected function getAdditionalMetaKeys(): array
    {
        return [];
    }

    /**
     * Clean up all user meta for this provider
     *
     * @param int $userId User ID
     * @return bool True on success
     */
    public function cleanupUserMeta(int $userId): bool
    {
        $success = $this->deleteSecret($userId);

        foreach ($this->getAdditionalMetaKeys() as $metaKey) {
            delete_user_meta($userId, $metaKey);
        }

        return $success;
    }

    // =========================================================================
    // Utility Methods
    // =========================================================================

    /**
     * Get user data safely
     *
     * @param int $userId User ID
     * @return \WP_User|false User object or false
     */
    protected function getUser(int $userId)
    {
        return get_userdata($userId);
    }

    /**
     * Validate user exists
     *
     * @param int $userId User ID
     * @return bool True if user exists
     */
    protected function userExists(int $userId): bool
    {
        return $this->getUser($userId) !== false;
    }

    /**
     * Perform timing-safe string comparison
     *
     * @param string $expected Expected value
     * @param string $actual   Actual value
     * @return bool True if strings match
     */
    protected function timingSafeCompare(string $expected, string $actual): bool
    {
        return hash_equals($expected, $actual);
    }
}
