<?php

namespace App\Filament\Auth\MultiFactor;

use App\Models\User;
use App\Services\AuditLog;
use App\Services\LoginOtpService;
use Closure;
use Filament\Actions\Action;
use Filament\Auth\MultiFactor\App\AppAuthentication;
use Filament\Auth\MultiFactor\Contracts\HasBeforeChallengeHook;
use Filament\Auth\MultiFactor\Contracts\MultiFactorAuthenticationProvider;
use Filament\Facades\Filament;
use Filament\Forms\Components\OneTimeCodeInput;
use Filament\Notifications\Notification;
use Filament\Schemas\Components\Actions;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Text;
use Illuminate\Contracts\Auth\Authenticatable;

/**
 * Wraps the source app's bespoke email-OTP second factor (App\Services\LoginOtpService
 * — bcrypt-hashed 6-digit code, 10-minute expiry, 5 attempts, 60s resend
 * cooldown) as a Filament MultiFactorAuthenticationProvider, so Filament's own
 * Login page challenge flow drives it identically to the native TOTP
 * provider. Deliberately NOT Filament's built-in EmailAuthentication provider
 * — that has its own generation/verification/rate-limiting that would not
 * match LoginOtpService's exact numbers (docs/migration-plan.md §5).
 *
 * Enabled per-user via the existing `two_factor_email_enabled` column
 * (User::hasEmailAuthentication(), unchanged from the source app).
 */
class EmailOtpAuthentication implements HasBeforeChallengeHook, MultiFactorAuthenticationProvider
{
    public static function make(): static
    {
        return app(static::class);
    }

    public function getId(): string
    {
        return 'email_otp';
    }

    public function getLoginFormLabel(): string
    {
        return 'Email code';
    }

    public function isEnabled(Authenticatable $user): bool
    {
        return $user instanceof User && $user->hasEmailAuthentication();
    }

    /**
     * Fired once, right before the challenge form is shown — sends the code.
     */
    public function beforeChallenge(Authenticatable $user): void
    {
        if (! $user instanceof User) {
            return;
        }

        $otp = app(LoginOtpService::class);

        if (! $otp->send($user)) {
            return;
        }

        Notification::make()
            ->title('Verification code sent')
            ->body("We emailed a 6-digit code to {$user->email}.")
            ->success()
            ->send();
    }

    /**
     * Self-service enable/disable/regenerate, ported from
     * leolanka-inertia's Profile\TwoFactorController (enableEmail/
     * disableEmail/regenerateRecoveryCodes): same audit events, same
     * "only mint recovery codes if none exist yet" rule. Recovery codes are
     * generated and hashed through the already-registered AppAuthentication
     * provider (not reimplemented here) so a code minted from either factor
     * verifies through the same shared `two_factor_recovery_codes` column at
     * the login challenge.
     *
     * @return array<Component|Action>
     */
    public function getManagementSchemaComponents(): array
    {
        $user = Filament::auth()->user();

        return [
            Actions::make($this->getActions())
                ->label($this->getLoginFormLabel())
                ->belowContent('Get a 6-digit code by email each time you sign in.')
                ->afterLabel(fn (): Text => $this->isEnabled($user)
                    ? Text::make('Enabled')->badge()->color('success')
                    : Text::make('Disabled')->badge()),
        ];
    }

    /**
     * @return array<Action>
     */
    public function getActions(): array
    {
        $user = Filament::auth()->user();

        return [
            Action::make('enableEmailOtp')
                ->label('Enable')
                ->color('primary')
                ->hidden(fn (): bool => $this->isEnabled($user))
                ->action(fn () => $this->enable()),
            Action::make('regenerateEmailOtpRecoveryCodes')
                ->label('Regenerate recovery codes')
                ->requiresConfirmation()
                ->modalDescription('Your old recovery codes will no longer work.')
                ->visible(fn (): bool => $this->isEnabled($user))
                ->action(fn () => $this->regenerateRecoveryCodes()),
            Action::make('disableEmailOtp')
                ->label('Disable')
                ->color('danger')
                ->requiresConfirmation()
                ->visible(fn (): bool => $this->isEnabled($user))
                ->action(fn () => $this->disable()),
        ];
    }

    protected function enable(): void
    {
        /** @var User $user */
        $user = Filament::auth()->user();

        $user->toggleEmailAuthentication(true);

        $freshCodes = null;
        if (blank($user->two_factor_recovery_codes)) {
            $freshCodes = $this->appAuthentication()->generateRecoveryCodes();
            $this->appAuthentication()->saveRecoveryCodes($user, $freshCodes);
        }

        AuditLog::record('user.two_factor_email_enabled', $user);

        Notification::make()
            ->title('Email verification is now required when you log in.')
            ->body($freshCodes !== null
                ? "Save these recovery codes somewhere safe — shown once:\n".implode("\n", $freshCodes)
                : null)
            ->success()
            ->persistent()
            ->send();
    }

    protected function disable(): void
    {
        /** @var User $user */
        $user = Filament::auth()->user();

        $user->toggleEmailAuthentication(false);

        AuditLog::record('user.two_factor_email_disabled', $user);

        Notification::make()->title('Email verification at login has been disabled.')->success()->send();
    }

    protected function regenerateRecoveryCodes(): void
    {
        /** @var User $user */
        $user = Filament::auth()->user();

        $hasAnySecondFactor = $user->two_factor_confirmed_at !== null || $user->hasEmailAuthentication();

        if (! $hasAnySecondFactor) {
            Notification::make()->title('Enable two-factor authentication first.')->danger()->send();

            return;
        }

        $codes = $this->appAuthentication()->generateRecoveryCodes();
        $this->appAuthentication()->saveRecoveryCodes($user, $codes);

        AuditLog::record('user.recovery_codes_regenerated', $user);

        Notification::make()
            ->title('New recovery codes generated.')
            ->body("Your old codes no longer work — save these somewhere safe:\n".implode("\n", $codes))
            ->success()
            ->persistent()
            ->send();
    }

    /**
     * The panel's registered TOTP provider — reused purely for its recovery
     * -code generation/hashing utility methods (generateRecoveryCodes(),
     * saveRecoveryCodes()), which operate on the same shared
     * `two_factor_recovery_codes` column regardless of which factor calls
     * them, so codes minted here work at the TOTP challenge's recovery-code
     * field too.
     */
    protected function appAuthentication(): AppAuthentication
    {
        return collect(Filament::getMultiFactorAuthenticationProviders())
            ->first(fn (MultiFactorAuthenticationProvider $provider): bool => $provider instanceof AppAuthentication);
    }

    /**
     * @return array<Component|Action>
     */
    public function getChallengeFormComponents(Authenticatable $user): array
    {
        return [
            OneTimeCodeInput::make('code')
                ->label('Verification code')
                ->required()
                ->rule(function () use ($user): Closure {
                    return function (string $attribute, mixed $value, Closure $fail) use ($user): void {
                        if (! $user instanceof User) {
                            $fail('Invalid code.');

                            return;
                        }

                        if (! app(LoginOtpService::class)->verify($user, (string) $value)) {
                            $remaining = app(LoginOtpService::class)->attemptsRemaining($user);
                            $fail($remaining > 0
                                ? "That code is invalid or has expired. {$remaining} attempt(s) remaining."
                                : 'Too many failed attempts. Request a new code.');

                            return;
                        }
                    };
                }),
        ];
    }
}
