<?php
/**
 * Simple Backup Code Service
 *
 * Provides backup/recovery codes for 2FA in the free plugin.
 * This is a simplified version without session tracking (Pro feature).
 *
 * @package     WPUltimateSecurity\UltimateSecurity\Modules\Authentication\TwoFactor\Services
 * @since       2.1.0
 */

declare(strict_types=1);

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

/**
 * Simple Backup Code Service
 *
 * Handles generation and verification of backup/recovery codes for 2FA.
 * Uses WordPress user meta for storage with hashed codes.
 *
 * @since 2.1.0
 */
class SimpleBackupCodeService
{
    /**
     * User meta key for backup codes
     */
    private const META_KEY_CODES = 'ultimate_security_2fa_backup_codes';

    /**
     * User meta key for generation timestamp
     */
    private const META_KEY_GENERATED_AT = 'ultimate_security_2fa_backup_codes_generated_at';

    /**
     * Default number of backup codes to generate
     */
    private const DEFAULT_CODE_COUNT = 10;

    /**
     * Generate backup/recovery codes for a user.
     *
     * @param int $user_id User ID.
     * @param int $count Number of codes to generate (default 10).
     * @return array Generated codes (unhashed, shown only once).
     */
    public static function generate_codes(int $user_id, int $count = self::DEFAULT_CODE_COUNT): array
    {
        $codes = [];

        for ($i = 0; $i < $count; $i++) {
            // Generate 8-character alphanumeric codes (e.g., A1B2C3D4)
            $codes[] = strtoupper(bin2hex(random_bytes(4)));
        }

        // Hash codes before storing
        $hashed_codes = array_map(function ($code) {
            return wp_hash_password($code);
        }, $codes);

        update_user_meta($user_id, self::META_KEY_CODES, $hashed_codes);
        update_user_meta($user_id, self::META_KEY_GENERATED_AT, time());

        // Fire action for audit logging
        $user = get_user_by('id', $user_id);
        if ($user) {
            /**
             * Fires when backup codes are generated for a user.
             *
             * @param int $user_id User ID.
             * @param int $count Number of codes generated.
             * @param \WP_User $user User object.
             */
            do_action('ultimate_security_backup_codes_generated', $user_id, $count, $user);
        }

        return $codes; // Return unhashed codes to show user once
    }

    /**
     * Verify a backup code.
     *
     * @param int $user_id User ID.
     * @param string $code Code to verify.
     * @return bool True if code is valid and was consumed.
     */
    public static function verify_code(int $user_id, string $code): bool
    {
        $stored_codes = get_user_meta($user_id, self::META_KEY_CODES, true);

        if (!is_array($stored_codes) || empty($stored_codes)) {
            return false;
        }

        // Normalize the code (remove dashes, spaces, make uppercase)
        $code = strtoupper(preg_replace('/[\s\-]/', '', $code));

        foreach ($stored_codes as $key => $hashed_code) {
            if (wp_check_password($code, $hashed_code)) {
                // Remove used code
                unset($stored_codes[$key]);
                update_user_meta($user_id, self::META_KEY_CODES, array_values($stored_codes));

                $remaining = count($stored_codes);

                // Fire action for audit logging
                $user = get_user_by('id', $user_id);
                if ($user) {
                    /**
                     * Fires when a backup code is used successfully.
                     *
                     * @param int $user_id User ID.
                     * @param int $remaining Number of codes remaining.
                     * @param \WP_User $user User object.
                     */
                    do_action('ultimate_security_backup_code_used', $user_id, $remaining, $user);
                }

                // Fire low codes warning if applicable
                if ($remaining > 0 && $remaining <= 3) {
                    /**
                     * Fires when backup codes are running low (3 or fewer remaining).
                     *
                     * @param int $user_id User ID.
                     * @param int $remaining Number of codes remaining.
                     */
                    do_action('ultimate_security_backup_codes_low', $user_id, $remaining);
                }

                return true;
            }
        }

        // Fire action for failed verification
        $user = get_user_by('id', $user_id);
        if ($user) {
            /**
             * Fires when a backup code verification fails.
             *
             * @param int $user_id User ID.
             * @param \WP_User $user User object.
             */
            do_action('ultimate_security_backup_code_failed', $user_id, $user);
        }

        return false;
    }

    /**
     * Get remaining backup codes count.
     *
     * @param int $user_id User ID.
     * @return int Number of remaining backup codes.
     */
    public static function get_remaining_count(int $user_id): int
    {
        $stored_codes = get_user_meta($user_id, self::META_KEY_CODES, true);
        return is_array($stored_codes) ? count($stored_codes) : 0;
    }

    /**
     * Check if user has backup codes.
     *
     * @param int $user_id User ID.
     * @return bool True if user has backup codes.
     */
    public static function has_codes(int $user_id): bool
    {
        return self::get_remaining_count($user_id) > 0;
    }

    /**
     * Revoke all backup codes for a user.
     *
     * @param int $user_id User ID.
     * @return bool True on success.
     */
    public static function revoke_codes(int $user_id): bool
    {
        delete_user_meta($user_id, self::META_KEY_CODES);
        delete_user_meta($user_id, self::META_KEY_GENERATED_AT);

        // Fire action for audit logging
        $user = get_user_by('id', $user_id);
        if ($user) {
            /**
             * Fires when backup codes are revoked for a user.
             *
             * @param int $user_id User ID.
             * @param int $revoked_by User ID of the user who revoked the codes.
             * @param \WP_User $user User object.
             */
            do_action('ultimate_security_backup_codes_revoked', $user_id, get_current_user_id(), $user);
        }

        return true;
    }

    /**
     * Get backup codes generation timestamp.
     *
     * @param int $user_id User ID.
     * @return int|null Timestamp or null if not generated.
     */
    public static function get_generated_at(int $user_id): ?int
    {
        $timestamp = get_user_meta($user_id, self::META_KEY_GENERATED_AT, true);
        return $timestamp ? (int) $timestamp : null;
    }

    /**
     * Check if backup codes need regeneration (older than 90 days).
     *
     * @param int $user_id User ID.
     * @return bool True if codes should be regenerated.
     */
    public static function needs_regeneration(int $user_id): bool
    {
        $generated_at = self::get_generated_at($user_id);

        if (!$generated_at) {
            return true;
        }

        // Recommend regeneration after 90 days
        $ninety_days_ago = strtotime('-90 days');

        return $generated_at < $ninety_days_ago;
    }

    /**
     * Get backup code status for a user.
     *
     * @param int $user_id User ID.
     * @return array{has_codes: bool, remaining: int, generated_at: int|null, needs_regeneration: bool}
     */
    public static function get_status(int $user_id): array
    {
        $remaining = self::get_remaining_count($user_id);
        $generated_at = self::get_generated_at($user_id);

        return [
            'has_codes' => $remaining > 0,
            'remaining' => $remaining,
            'generated_at' => $generated_at,
            'generated_at_formatted' => $generated_at ? date('Y-m-d H:i:s', $generated_at) : null,
            'needs_regeneration' => self::needs_regeneration($user_id),
            'is_low' => $remaining > 0 && $remaining <= 3,
        ];
    }

    /**
     * Format backup codes for display.
     *
     * @param array $codes Array of backup codes.
     * @return array Formatted codes with dashes (e.g., A1B2-C3D4).
     */
    public static function format_codes_for_display(array $codes): array
    {
        return array_map(function ($code) {
            // Add dash in the middle for readability: A1B2C3D4 -> A1B2-C3D4
            return substr($code, 0, 4) . '-' . substr($code, 4);
        }, $codes);
    }
}
