<?php

namespace App\Livewire\Concerns;

use App\Models\AuditEvent;
use App\Models\User;
use App\Services\BruteforceGuard;
use App\Services\WebauthnService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Validation\ValidationException;
use Livewire\Component;

/**
 * Shared 2FA-challenge plumbing for the TOTP/key view and the backup-only view: the
 * pending-user resolver, the brute-force buckets (ONE shared counter set across both
 * views), and the login success tail. Centralising it guarantees both views hit the
 * same rate limit and complete login identically.
 *
 * @mixin Component
 */
trait CompletesTwoFactorChallenge
{
    private ?User $pendingUser = null;

    private bool $pendingResolved = false;

    /** The pending 2FA user, resolved once per request from the session. */
    protected function pendingUser(): ?User
    {
        if (! $this->pendingResolved) {
            $this->pendingUser = User::find(session('2fa.user'));
            $this->pendingResolved = true;
        }

        return $this->pendingUser;
    }

    /** Whether the PENDING user has TOTP — the authenticator code field renders only then. */
    public function getPendingHasTotpProperty(): bool
    {
        return (bool) $this->pendingUser()?->hasTotp();
    }

    /** Whether to offer the security-key option for the pending user (domain+HTTPS + has a key). */
    public function webauthnAvailable(): bool
    {
        $user = $this->pendingUser();

        return $user !== null
            && app(WebauthnService::class)->available()
            && $user->hasWebauthnCredentials();
    }

    /**
     * Two auto-expiring 2FA brute-force buckets, keyed on the SERVER-side pending user id
     * (not client-controlled) — a tight per-(user+IP) one plus an IP-independent per-user
     * backstop, so a distributed (multi-IP) brute-force of the code is still capped. Both
     * decay in minutes (never a permanent lockout; clusev:reset-admin stays open).
     *
     * @return array<string, array{0:int,1:int}> key => [maxAttempts, decaySeconds]
     */
    protected function rateLimitBuckets(): array
    {
        $uid = (string) session('2fa.user');

        return [
            'two-factor:'.md5($uid.'|'.request()->ip()) => [5, 60],
            'two-factor-acct:'.md5($uid) => [20, 900],
        ];
    }

    protected function assertNotRateLimited(): void
    {
        foreach ($this->rateLimitBuckets() as $key => [$max]) {
            if (RateLimiter::tooManyAttempts($key, $max)) {
                throw ValidationException::withMessages([
                    'code' => __('auth.too_many_attempts', ['seconds' => RateLimiter::availableIn($key)]),
                ]);
            }
        }
    }

    protected function hitRateLimit(): void
    {
        foreach ($this->rateLimitBuckets() as $key => [, $decay]) {
            RateLimiter::hit($key, $decay);
        }
    }

    protected function clearRateLimit(): void
    {
        foreach (array_keys($this->rateLimitBuckets()) as $key) {
            RateLimiter::clear($key);
        }
    }

    /** A failed 2FA attempt: throttle (Layer 1) + feed the persistent ban (Layer 2) + audit. */
    protected function recordFailedAttempt(string $reason): void
    {
        $this->hitRateLimit();
        $ip = request()->ip();
        app(BruteforceGuard::class)->record($ip, $reason);
        AuditEvent::create([
            'user_id' => null,
            'actor' => 'system',
            'action' => 'auth.2fa_failed',
            'target' => (string) session('2fa.user'),
            'ip' => $ip,
        ]);
    }

    /** Log the verified pending user in and finish the challenge (shared success tail). */
    protected function completeLogin(User $user): mixed
    {
        $remember = (bool) session('2fa.remember');
        session()->forget(['2fa.user', '2fa.remember']);

        Auth::login($user, $remember);
        session()->regenerate();

        return $this->redirectIntended(route('dashboard'), navigate: true);
    }
}
