<?php

namespace App\Http\Controllers\Auth;

use App\Jobs\SendVerificationEmailJob;
use App\Http\Controllers\Controller;
use App\Models\EmailSendEvent;
use App\Models\User;
use App\Services\Auth\AuthAuditLogger;
use App\Services\Auth\DisposableEmailService;
use App\Services\Auth\RegistrationVerificationTokenService;
use App\Services\Security\CaptchaVerifier;
use App\Services\Security\TurnstileVerifier;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Illuminate\Support\Str;
use Illuminate\View\View;

class RegisteredUserController extends Controller
{
    public function __construct(
        private readonly CaptchaVerifier $captchaVerifier,
        private readonly TurnstileVerifier $turnstileVerifier,
        private readonly DisposableEmailService $disposableEmailService,
        private readonly RegistrationVerificationTokenService $verificationTokenService,
        private readonly AuthAuditLogger $authAuditLogger,
    )
    {
    }

    /**
     * Display the registration view.
     */
    public function create(Request $request): View
    {
        $cspNonce = $this->resolveCspNonce($request);

        return view('auth.register', [
            'prefillEmail' => (string) $request->query('email', ''),
            'page_canonical' => route('register'),
            'turnstile' => [
                'enabled' => $this->turnstileVerifier->isEnabled(),
                'siteKey' => $this->turnstileVerifier->siteKey(),
                'scriptUrl' => $this->turnstileVerifier->scriptUrl(),
                'cspNonce' => $cspNonce,
            ],
        ]);
    }

    public function notice(Request $request): View
    {
        $email = (string) session('registration_email', '');
        $remaining = $email === '' ? 0 : $this->resendRemainingSeconds($email);

        return view('auth.register-notice', [
            'email' => $email,
            'resendSeconds' => $remaining,
        ]);
    }

    /**
     * Handle an incoming registration request.
     *
     * @throws \Illuminate\Validation\ValidationException
     */
    public function store(Request $request): RedirectResponse
    {
        $turnstileResponse = (string) ($request->input('turnstile_token') ?: $request->input('cf-turnstile-response', ''));

        $rules = [
            'email' => ['required', 'string', 'lowercase', 'email', 'max:255'],
            'website' => ['nullable', 'max:0'],
            'turnstile_token' => ['nullable', 'string'],
            'cf-turnstile-response' => [$this->turnstileVerifier->isEnabled() ? 'required_without:turnstile_token' : 'nullable', 'string'],
        ];

        $validator = Validator::make($request->all(), $rules);

        if ($validator->fails()) {
            $errors = $validator->errors()->toArray();

            if (array_key_exists('cf-turnstile-response', $errors) && ! array_key_exists('turnstile_token', $errors)) {
                $errors['turnstile_token'] = $errors['cf-turnstile-response'];
                unset($errors['cf-turnstile-response']);
            }

            $this->authAuditLogger->log(
                eventType: 'register',
                request: $request,
                status: 'failed',
                reason: 'validation_failed',
                identifier: (string) $request->input('email'),
                metadata: ['fields' => array_keys($errors)],
            );

            throw ValidationException::withMessages($errors);
        }

        $validated = $validator->validated();

        $email = strtolower(trim((string) $validated['email']));
        $ip = $request->ip();

        $this->trackRegisterAttempt($ip);

        if ($this->turnstileVerifier->isEnabled() && ! $this->turnstileVerifier->verify($turnstileResponse, $ip)) {
            $this->authAuditLogger->log(
                eventType: 'register',
                request: $request,
                status: 'failed',
                reason: 'captcha_failed',
                identifier: $email,
            );

            return back()
                ->withInput($request->except('website', 'turnstile_token', 'cf-turnstile-response'))
                ->withErrors(['turnstile_token' => 'Security verification failed. Please try again.']);
        }

        if ($this->disposableEmailService->isDisposableEmail($email)) {
            $this->logEmailEvent($email, $ip, null, 'blocked', 'disposable');
            $this->authAuditLogger->log(
                eventType: 'register',
                request: $request,
                status: 'failed',
                reason: 'disposable_email',
                identifier: $email,
            );

            return back()
                ->withInput($request->except('website'))
                ->withErrors(['email' => 'Please use a real email provider.']);
        }

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

        if ($user && $user->hasCompletedOnboarding()) {
            $this->authAuditLogger->log(
                eventType: 'register',
                request: $request,
                status: 'failed',
                reason: 'email_exists',
                identifier: $email,
                user: $user,
            );

            return back()
                ->withInput($request->except('website'))
                ->withErrors(['email' => 'An account with this email already exists.']);
        }

        if (! $user) {
            $user = new User();
            $user->forceFill([
                'username' => null,
                'name' => Str::before($email, '@'),
                'email' => $email,
                'password' => Hash::make(Str::random(64)),
                'is_active' => true,
                'email_verified_at' => null,
                'onboarding_step' => 'verified',
                'needs_password_reset' => true,
                'username_changed_at' => now(),
                'last_username_change_at' => now(),
            ]);
            $user->save();
        } else {
            $user->forceFill([
                'email_verified_at' => $user->email_verified_at,
                'is_active' => true,
                'onboarding_step' => strtolower((string) ($user->onboarding_step ?? '')) === 'password' ? 'password' : 'verified',
                'needs_password_reset' => strtolower((string) ($user->onboarding_step ?? '')) === 'password'
                    ? (bool) $user->needs_password_reset
                    : true,
            ])->save();
        }

        Auth::login($user);

        $this->authAuditLogger->log(
            eventType: 'register',
            request: $request,
            status: 'success',
            reason: $user->wasRecentlyCreated ? 'user_created' : 'resume_onboarding',
            identifier: $email,
            user: $user,
        );

        $needsPasswordSetup = strtolower((string) ($user->onboarding_step ?? '')) !== 'password'
            || (bool) $user->needs_password_reset;

        return redirect(route($needsPasswordSetup ? 'setup.password.create' : 'setup.username.create', absolute: false))
            ->with('status', $needsPasswordSetup
                ? 'Continue with password setup.'
                : 'Continue with username setup.');
    }

    public function resendVerification(Request $request): RedirectResponse
    {
        $validated = $request->validate([
            'email' => ['required', 'string', 'lowercase', 'email', 'max:255'],
        ]);

        $email = strtolower(trim((string) $validated['email']));
        $ip = $request->ip();

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

        if (! $user) {
            $this->logEmailEvent($email, $ip, null, 'blocked', 'missing');

            return $this->redirectToRegisterNotice($email);
        }

        if ($this->isWithinEmailCooldown($user)) {
            $this->logEmailEvent($email, $ip, (int) $user->id, 'blocked', 'cooldown');

            return $this->redirectToRegisterNotice($email);
        }

        $token = $this->verificationTokenService->createForUser((int) $user->id);
        $event = $this->logEmailEvent($email, $ip, (int) $user->id, 'queued', null);

        SendVerificationEmailJob::dispatch(
            emailEventId: (int) $event->id,
            email: $email,
            token: $token,
            userId: (int) $user->id,
            ip: $ip
        );

        $this->markVerificationEmailSent($user);

        return $this->redirectToRegisterNotice($email);
    }

    private function redirectToRegisterNotice(string $email): RedirectResponse
    {
        return redirect(route('register.notice', absolute: false))
            ->with('status', $this->genericSuccessMessage())
            ->with('registration_email', $email);
    }

    private function genericSuccessMessage(): string
    {
        return (string) config('registration.generic_success_message', 'If that email is valid, we sent a verification link.');
    }

    private function logEmailEvent(string $email, ?string $ip, ?int $userId, string $status, ?string $reason): EmailSendEvent
    {
        return EmailSendEvent::query()->create([
            'type' => 'verify_email',
            'email' => $email,
            'ip' => $ip,
            'user_id' => $userId,
            'status' => $status,
            'reason' => $reason,
            'created_at' => now(),
        ]);
    }

    private function shouldRequireCaptchaForIp(?string $ip): bool
    {
        if (! $this->captchaVerifier->isEnabled() && ! $this->turnstileVerifier->isEnabled()) {
            return false;
        }

        if ($ip === null || $ip === '') {
            return false;
        }

        $threshold = max(1, (int) config('registration.turnstile_suspicious_attempts', 2));
        $attempts = (int) cache()->get($this->registerAttemptCacheKey($ip), 0);

        if ($attempts >= $threshold) {
            return true;
        }

        $minuteLimit = max(1, (int) config('registration.ip_per_minute_limit', 3));
        $dailyLimit = max(1, (int) config('registration.ip_per_day_limit', 20));

        if (RateLimiter::tooManyAttempts($this->registerIpRateKey($ip), $minuteLimit)) {
            return true;
        }

        return RateLimiter::tooManyAttempts($this->registerIpDailyRateKey($ip), $dailyLimit);
    }

    private function trackRegisterAttempt(?string $ip): void
    {
        if ($ip === null || $ip === '') {
            return;
        }

        $key = $this->registerAttemptCacheKey($ip);
        $windowMinutes = max(1, (int) config('registration.turnstile_attempt_window_minutes', 30));
        $seconds = $windowMinutes * 60;

        $attempts = (int) cache()->get($key, 0);
        cache()->put($key, $attempts + 1, $seconds);
    }

    private function registerAttemptCacheKey(string $ip): string
    {
        return 'register:attempts:' . sha1($ip);
    }

    private function registerIpRateKey(string $ip): string
    {
        return 'register:ip:' . $ip;
    }

    private function registerIpDailyRateKey(string $ip): string
    {
        return 'register:ip:daily:' . $ip;
    }

    private function isWithinEmailCooldown(User $user): bool
    {
        if ($user->last_verification_sent_at === null) {
            return false;
        }

        $cooldownMinutes = max(1, (int) config('registration.email_cooldown_minutes', 30));

        return $user->last_verification_sent_at->gt(now()->subMinutes($cooldownMinutes));
    }

    private function markVerificationEmailSent(User $user): void
    {
        $now = now();

        $windowStartedAt = $user->verification_send_window_started_at;
        if (! $windowStartedAt || $windowStartedAt->lt($now->copy()->subDay())) {
            $user->verification_send_window_started_at = $now;
            $user->verification_send_count_24h = 1;
        } else {
            $user->verification_send_count_24h = ((int) $user->verification_send_count_24h) + 1;
        }

        $user->last_verification_sent_at = $now;
        $user->save();
    }

    private function resendCooldownSeconds(): int
    {
        return max(60, ((int) config('registration.email_cooldown_minutes', 30)) * 60);
    }

    private function resendRemainingSeconds(string $email): int
    {
        $user = User::query()
            ->where('email', strtolower(trim($email)))
            ->whereNull('email_verified_at')
            ->first();

        if (! $user || $user->last_verification_sent_at === null) {
            return 0;
        }

        $remaining = $user->last_verification_sent_at
            ->copy()
            ->addSeconds($this->resendCooldownSeconds())
            ->diffInSeconds(now(), false);

        return $remaining >= 0 ? 0 : abs((int) $remaining);
    }

    private function resolveCspNonce(Request $request): ?string
    {
        $candidates = [
            $request->attributes->get('csp_nonce'),
            $request->attributes->get('cspNonce'),
            $request->headers->get('X-CSP-Nonce'),
            $request->server('HTTP_X_CSP_NONCE'),
        ];

        foreach ($candidates as $candidate) {
            if (! is_string($candidate)) {
                continue;
            }

            $nonce = trim($candidate);

            if ($nonce !== '') {
                return $nonce;
            }
        }

        return null;
    }
}
