<?php

namespace App\Livewire\Admin;

use App\Livewire\Concerns\ConfirmsPassword;
use App\Livewire\Concerns\ResolvesOperator;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Actions\ConfirmTwoFactorAuthentication;
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
use Laravel\Fortify\Actions\GenerateNewRecoveryCodes;
use Livewire\Attributes\Layout;
use Livewire\Attributes\On;
use Livewire\Component;

/**
 * Where an operator sets their own two-factor login up.
 *
 * Its own page, not a card on Admin\Settings. RequireOperatorTwoFactor must
 * be able to exempt ENROLMENT alone while compulsory two-factor is on — the
 * earlier shape exempted the whole of Admin\Settings on the theory that it
 * was "the page where two-factor is set up", but that component also carries
 * staff management, site visibility, network restrictions, account changes,
 * and the two-factor policy switch itself. An unenrolled operator got
 * unrestricted access to all of that, not just enrolment (Codex R15, P1b).
 * One route to exempt is one decision; eight actions on a shared page each
 * remembering to check is eight chances to forget one.
 *
 * Reachable by EVERY operator, not gated behind any capability — the
 * compulsory switch (Admin\Settings::saveTwoFactorPolicy()) applies to every
 * member of staff, so enrolment has to be reachable by every operator too.
 *
 * Every action is re-checked, server-side, against a recent password
 * confirmation on every single call — not merely by hiding the buttons: a
 * Livewire action is reachable by anyone who can post to /livewire/update,
 * and "the form was not on screen" has never stopped anybody.
 */
#[Layout('layouts.admin')]
class TwoFactorSetup extends Component
{
    /**
     * Rendered inside the settings page rather than as a page of its own.
     *
     * Only the heading differs: as a tab it sits under the settings title, and
     * a second <h1> saying "Zwei-Faktor" directly beneath "Einstellungen" is
     * two page titles on one page. Nothing about what the component DOES
     * changes — the password gate, the enrolment and every server-side check
     * are the same code on both routes in.
     */
    public bool $embedded = false;

    use ConfirmsPassword;
    use ResolvesOperator;

    /** Two-factor is confirmed against the operator's own record, not a portal one. */
    protected function confirmationGuard(): string
    {
        return 'operator';
    }

    /** Shown once, right after setting two-factor up. */
    public ?array $recoveryCodes = null;

    public string $twoFactorCode = '';

    /**
     * Start two-factor setup: generate the secret, then show the QR code.
     *
     * Not confirmed yet — Fortify keeps `two_factor_confirmed_at` null until a
     * code from the app has been accepted, so someone who scans nothing and
     * closes the tab is not left locked out of their own account.
     */
    public function enableTwoFactor(): void
    {
        $this->requireConfirmedPassword();

        if (! $operator = $this->currentOperator()) {
            return;
        }

        app(EnableTwoFactorAuthentication::class)($operator);
    }

    /**
     * Step back out of a half-finished setup.
     *
     * Without this the only way out of the QR step was to navigate away, which
     * left an unconfirmed secret sitting on the account — invisible, because
     * nothing shows a pending enrolment anywhere else. Clearing it costs
     * nothing: it was never confirmed, so it never protected anything.
     */
    public function cancelSetup(): void
    {
        $this->requireConfirmedPassword();

        if (! $operator = $this->currentOperator()) {
            return;
        }

        app(DisableTwoFactorAuthentication::class)($operator);

        $this->twoFactorCode = '';
        $this->resetErrorBag('twoFactorCode');
        $this->dispatch('notify', message: __('two_factor_setup.cancelled'));
    }

    /** Accept a code from the app, which is what actually turns it on. */
    public function confirmTwoFactor(): void
    {
        $this->requireConfirmedPassword();

        if (! $operator = $this->currentOperator()) {
            return;
        }

        try {
            app(ConfirmTwoFactorAuthentication::class)(
                $operator,
                $this->twoFactorCode,
            );
        } catch (ValidationException $e) {
            $this->addError('twoFactorCode', $e->errors()['code'][0] ?? __('two_factor_setup.code_wrong'));

            return;
        }

        $this->twoFactorCode = '';
        // Shown once, here, because this is the only moment they exist in a
        // form anyone will read. They stay retrievable afterwards, but nobody
        // writes down what they were not shown.
        $this->recoveryCodes = json_decode(decrypt($operator->two_factor_recovery_codes), true);
        $this->dispatch('notify', message: __('two_factor_setup.on'));
    }

    public function regenerateRecoveryCodes(): void
    {
        $this->requireConfirmedPassword();

        if (! $operator = $this->currentOperator()) {
            return;
        }

        app(GenerateNewRecoveryCodes::class)($operator);
        $this->recoveryCodes = json_decode(decrypt($operator->refresh()->two_factor_recovery_codes), true);
    }

    public function disableTwoFactor(): void
    {
        $this->requireConfirmedPassword();

        if (! $operator = $this->currentOperator()) {
            return;
        }

        app(DisableTwoFactorAuthentication::class)($operator);
        $this->recoveryCodes = null;
        $this->dispatch('notify', message: __('two_factor_setup.off'));
    }

    /**
     * The disable button opens ConfirmDisableTwoFactor instead of calling
     * disableTwoFactor() directly (R23); its confirm button dispatches back
     * here, and disableTwoFactor() still runs its own password check.
     */
    #[On('two-factor-disable-confirmed')]
    public function onDisableConfirmed(): void
    {
        $this->disableTwoFactor();
    }

    /**
     * Every two-factor action goes through this.
     *
     * Server-side, on each call, and not merely by hiding the buttons: a
     * Livewire action is reachable by anyone who can post to /livewire/update,
     * and "the form was not on screen" has never stopped anybody.
     */
    private function requireConfirmedPassword(): void
    {
        abort_unless($this->passwordRecentlyConfirmed(), 403);
    }

    public function render()
    {
        // Unlike the actions above, render() has no requireConfirmedPassword()
        // gate in front of it — Livewire calls it on every update, including a
        // bare property sync, so it is its own reachable path for a session
        // that ended after the page loaded.
        if (! $operator = $this->currentOperator()) {
            return view('livewire.admin.two-factor-setup', [
                'twoFactorPending' => false,
                'twoFactorOn' => false,
                'twoFactorQr' => null,
                'passwordConfirmed' => false,
            ]);
        }

        return view('livewire.admin.two-factor-setup', [
            // Never the secret itself — only whether it exists, and the SVG
            // Fortify renders from it. The secret in a Livewire property would
            // travel to the browser and back in the component snapshot.
            'twoFactorPending' => $operator->two_factor_secret !== null && $operator->two_factor_confirmed_at === null,
            'twoFactorOn' => $operator->two_factor_confirmed_at !== null,
            'twoFactorQr' => $operator->two_factor_secret !== null && $operator->two_factor_confirmed_at === null
                ? $operator->twoFactorQrCodeSvg()
                : null,
            'passwordConfirmed' => $this->passwordRecentlyConfirmed(),
        ]);
    }
}
