<?php

namespace App\Filament\Pages\Auth;

use App\Models\User;
use App\Services\AuditLog;
use App\Services\CurrentContext;
use App\Services\LoginThrottle;
use Filament\Auth\Http\Responses\Contracts\LoginResponse;
use Filament\Auth\MultiFactor\Contracts\HasBeforeChallengeHook;
use Filament\Auth\Pages\Login as BaseLogin;
use Filament\Facades\Filament;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\RenderHook;
use Filament\Schemas\Components\View;
use Filament\Schemas\Schema;
use Filament\View\PanelsRenderHook;
use Illuminate\Auth\SessionGuard;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;

/**
 * Ported from leolanka-inertia's AuthenticatedSessionController@store
 * (docs/migration-plan.md §5): dummy-hash timing defense, tenant-segmented
 * burst + 24h lockout rate limiting, the 20-attempt persistent lockout, and
 * the exact original per-case error messages. Multi-factor challenge
 * handling (TOTP via Filament's native AppAuthentication, email-OTP via
 * App\Filament\Auth\MultiFactor\EmailOtpAuthentication) reuses Filament's own
 * inline challenge-form machinery from the parent class — see
 * $userUndertakingMultiFactorAuthentication, multiFactorChallengeForm().
 */
class Login extends BaseLogin
{
    /**
     * A dedicated view so the login screen can present the tenant's logo at
     * hero size (see resources/views/filament/pages/auth/login.blade.php)
     * instead of the sidebar's 1.75rem brand mark.
     */
    protected string $view = 'filament.pages.auth.login';

    public function authenticate(): ?LoginResponse
    {
        $data = $this->form->getState();

        $email = Str::lower(trim((string) $data['email']));
        $password = (string) $data['password'];
        $remember = (bool) ($data['remember'] ?? false);
        $ip = (string) request()->ip();

        $throttle = app(LoginThrottle::class);
        $burstKey = $throttle->burstKey($email, $ip);
        $lockoutKey = $throttle->lockoutKey($email);

        if ($throttle->tooManyAttempts($burstKey)) {
            throw ValidationException::withMessages([
                'data.email' => __('Too many login attempts. Please try again in :seconds seconds.', [
                    'seconds' => $throttle->availableInSeconds($burstKey),
                ]),
            ]);
        }

        $user = User::query()->where('email', $email)->first();

        if (! $throttle->passwordMatches($user, $password)) {
            $throttle->recordFailure($burstKey, $lockoutKey, $user);

            AuditLog::record('user.login_failed', $user, [
                'email' => $email,
                'ip' => $ip,
                'user_agent' => substr((string) request()->userAgent(), 0, 255),
            ]);

            throw ValidationException::withMessages([
                'data.email' => __('These credentials do not match our records.'),
            ]);
        }

        if ($user->isLocked()) {
            AuditLog::record('user.login_failed', $user, ['reason' => 'locked', 'ip' => $ip]);

            throw ValidationException::withMessages([
                'data.email' => __('This account is temporarily locked. Contact an administrator.'),
            ]);
        }

        if (! $user->isActive()) {
            AuditLog::record('user.login_failed', $user, ['reason' => $user->status, 'ip' => $ip]);

            throw ValidationException::withMessages([
                'data.email' => $user->isSuspended()
                    ? __('Your account has been suspended. Contact an administrator.')
                    : __('Your account is not active. Contact an administrator.'),
            ]);
        }

        if (! $this->isUserAllowedToAccessPanel($user)) {
            throw ValidationException::withMessages([
                'data.email' => __('filament-panels::auth/pages/login.messages.failed'),
            ]);
        }

        $throttle->clear($burstKey, $lockoutKey, $user);

        // Reuses Filament's own inline multi-factor challenge state machine
        // (registered providers: TOTP via AppAuthentication, email-OTP via
        // EmailOtpAuthentication) — same mechanism as the base Login page,
        // just entered from our own credential-check step instead of theirs.
        if ($this->needsMultiFactorChallenge($user)) {
            if (! empty($this->userUndertakingMultiFactorAuthentication)) {
                AuditLog::record('user.two_factor_challenged', $user, ['ip' => $ip]);
            }

            return null;
        }

        /** @var SessionGuard $authGuard */
        $authGuard = Filament::auth();
        $authGuard->login($user, $remember);

        session()->regenerate();

        return app(LoginResponse::class);
    }

    /**
     * Mirrors the parent class's private MFA-challenge computation (not
     * reusable directly since it's `private` there) — same provider loop,
     * same $userUndertakingMultiFactorAuthentication state property.
     */
    protected function needsMultiFactorChallenge(User $user): bool
    {
        if (
            filled($this->userUndertakingMultiFactorAuthentication) &&
            (decrypt($this->userUndertakingMultiFactorAuthentication) === $user->getAuthIdentifier())
        ) {
            if ($this->isMultiFactorChallengeRateLimited($user)) {
                return true;
            }

            $this->multiFactorChallengeForm->validate();

            return false;
        }

        foreach (Filament::getMultiFactorAuthenticationProviders() as $provider) {
            if (! $provider->isEnabled($user)) {
                continue;
            }

            $this->userUndertakingMultiFactorAuthentication = encrypt($user->getAuthIdentifier());

            if ($provider instanceof HasBeforeChallengeHook) {
                $provider->beforeChallenge($user);
            }

            break;
        }

        if (filled($this->userUndertakingMultiFactorAuthentication)) {
            $this->multiFactorChallengeForm->fill();

            return true;
        }

        return false;
    }

    /**
     * Ported from UserLanding::canAccess() / AuthenticatedSessionController's
     * intended-URL honouring — Filament's own LoginResponse redirects to
     * intended() automatically, this just needs UserLanding wired as the
     * fallback, which happens via RedirectToAccessibleHome + UserLanding
     * already (see docs/migration-plan.md).
     */
    protected function getCredentialsFromFormData(array $data): array
    {
        return [
            'email' => $data['email'],
            'password' => $data['password'],
        ];
    }

    /**
     * Adds the Quick PIN pad, ported from leolanka-inertia's login.tsx +
     * use-device-token.ts: a trusted-device token minted by
     * device-token-script.blade.php (rendered on authenticated pages) is
     * read from localStorage entirely client-side (Alpine, shared `x-data`
     * scope across the pad and the real form) and toggles which one is
     * visible — no server round-trip needed to decide, so there's no flash
     * of the wrong form on first paint. The PIN pad posts directly to the
     * already-ported `/pin-login` route.
     *
     * The Alpine scope is registered by name in pin-pad-script.blade.php
     * rather than written inline here: Filament emits extra attributes with
     * `escape: false`, so a `"` anywhere in an inline expression truncates the
     * `x-data` attribute and costs Alpine the entire scope — which hides the
     * password form along with the pad. See that view for the full note.
     *
     * The password form is gated on `! $data.pinPad`, not `! pinPad`, so that
     * it fails *open*. A bare `pinPad` against a missing scope is a
     * ReferenceError; Alpine logs it, resolves the expression to undefined and
     * `x-show` hides the element — so any breakage in the optional PIN pad
     * takes the primary login form down with it, which is exactly how the
     * `x-data` truncation became a lockout instead of a cosmetic glitch. The
     * `$data` magic reads the same reactive scope (so the toggle still works)
     * but resolves to undefined instead of throwing when the scope is absent,
     * leaving the form visible. The pad and toggle link keep bare references
     * on purpose: an optional affordance should fail closed.
     */
    /**
     * The landlord's `branding.login_heading`, in place of the stock "Sign in".
     *
     * Master Control has always collected this (and the subtitle below) and the
     * Theme tab has always previewed it, but nothing on the real page read
     * either — every tenant's login said "Sign in" no matter what was saved.
     *
     * Deliberately deferring to the parent while a multi-factor challenge is in
     * flight: at that point the heading is an instruction ("Two-factor
     * authentication"), not branding, and replacing it would leave the user
     * looking at a code field under a welcome message.
     */
    public function getHeading(): string|Htmlable|null
    {
        if (filled($this->userUndertakingMultiFactorAuthentication)) {
            return parent::getHeading();
        }

        return static::branding('login_heading') ?? parent::getHeading();
    }

    public function getSubheading(): string|Htmlable|null
    {
        if (filled($this->userUndertakingMultiFactorAuthentication)) {
            return parent::getSubheading();
        }

        return static::branding('login_subtitle') ?? parent::getSubheading();
    }

    /**
     * A branding string for the current tenant, or null when unset — including
     * on the central domain and in CLI contexts, where there is no tenant and
     * Filament's own copy is the right answer. On a universal host the tenant
     * may come from the `slug`/`tenant` query parameter (see
     * CurrentContext::brandingTenant()), so the page brands itself with the
     * workspace the visitor is signing into even before they authenticate.
     */
    protected static function branding(string $key): ?string
    {
        $value = app(CurrentContext::class)->brandingTenant()?->setting("branding.{$key}");

        return is_string($value) && trim($value) !== '' ? trim($value) : null;
    }

    public function content(Schema $schema): Schema
    {
        return $schema->components([
            RenderHook::make(PanelsRenderHook::AUTH_LOGIN_FORM_BEFORE),
            Group::make([
                View::make('filament.pages.auth.pin-pad-script'),
                View::make('filament.pages.auth.pin-pad'),
                $this->getFormContentComponent()->extraAttributes(['x-show' => '! $data.pinPad']),
                $this->getMultiFactorChallengeFormContentComponent(),
                View::make('filament.pages.auth.pin-toggle-link'),
            ])->extraAttributes([
                'x-data' => 'quickPinLogin',
            ]),
            RenderHook::make(PanelsRenderHook::AUTH_LOGIN_FORM_AFTER),
        ]);
    }
}
