<?php

namespace App\Livewire\Auth;

use App\Models\Operator;
use App\Support\AdminArea;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider;
use Livewire\Attributes\Layout;
use Livewire\Component;

/**
 * The operator's two-factor step.
 *
 * Fortify's TOTP provider does the arithmetic — there is no second
 * implementation of the algorithm here, only a second flow, because Fortify's
 * own flow is bound to the web guard.
 */
#[Layout('layouts.portal')]
class OperatorTwoFactorChallenge extends Component
{
    public string $code = '';

    public string $recoveryCode = '';

    public bool $usingRecovery = false;

    public function mount(): void
    {
        // Reachable only between password and code. Landing here directly is a
        // dead end, not a way in.
        abort_if(! session()->has('operator.2fa.id'), 403);
    }

    public function verify(): void
    {
        $operator = Operator::find(session('operator.2fa.id'));
        abort_if($operator === null, 403);

        $key = 'operator-2fa:'.$operator->id.'|'.request()->ip();

        if (RateLimiter::tooManyAttempts($key, 5)) {
            throw ValidationException::withMessages([
                'code' => __('auth.throttle', ['seconds' => RateLimiter::availableIn($key)]),
            ]);
        }

        if (! $this->passes($operator)) {
            RateLimiter::hit($key, 60);
            $this->reset('code', 'recoveryCode');

            throw ValidationException::withMessages(['code' => __('auth.twofa_invalid')]);
        }

        RateLimiter::clear($key);

        $remember = (bool) session('operator.2fa.remember', false);
        session()->forget(['operator.2fa.id', 'operator.2fa.remember']);

        if (! OperatorLogin::completeLogin($operator, $remember)) {
            // Disabled, or lost every console role, in the window between
            // the password step and this one. The code just entered may be
            // perfectly correct — the account behind it no longer is, so
            // reusing "wrong code" would mislead. The pending challenge is
            // spent either way (already forgotten above): no retry loop
            // against a session that can never complete now.
            throw ValidationException::withMessages(['code' => __('auth.not_an_operator')]);
        }

        $this->redirect(AdminArea::home(), navigate: false);
    }

    private function passes(Operator $operator): bool
    {
        if ($this->usingRecovery) {
            $codes = json_decode(decrypt($operator->two_factor_recovery_codes), true) ?: [];

            if (! in_array($this->recoveryCode, $codes, true)) {
                return false;
            }

            // A recovery code is single use — leaving it valid turns a one-time
            // escape hatch into a second password.
            $operator->forceFill([
                'two_factor_recovery_codes' => encrypt(json_encode(array_values(
                    array_diff($codes, [$this->recoveryCode])
                ))),
            ])->save();

            return true;
        }

        return app(TwoFactorAuthenticationProvider::class)
            ->verify(decrypt($operator->two_factor_secret), $this->code);
    }

    public function render()
    {
        return view('livewire.auth.operator-two-factor-challenge');
    }
}
