<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\PasswordResetOtp;
use App\Models\User;
use App\Services\Auth\PasswordResetOtpMailer;
use App\Services\Settings\SettingsService;
use Illuminate\Contracts\View\View;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\ValidationException;
use Throwable;

class PasswordResetOtpController extends Controller
{
    public function __construct(
        private readonly PasswordResetOtpMailer $mailer,
        private readonly SettingsService $settings,
    ) {}

    public function requestForm(): View
    {
        return view('auth.forgot-password');
    }

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

        $email = Str::lower((string) $validated['email']);
        $this->ensureOtpRequestIsNotRateLimited($request, $email);

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

        if (! $user) {
            RateLimiter::hit($this->otpThrottleKey($email, $request), 60);

            if ($request->expectsJson()) {
                return response()->json([
                    'message' => 'No account was found for this email address.',
                    'errors' => [
                        'email' => ['No account was found for this email address.'],
                    ],
                ], 422);
            }

            return back()
                ->withInput($request->only('email'))
                ->withErrors(['email' => 'No account was found for this email address.']);
        }

        $otp = (string) random_int(100000, 999999);

        PasswordResetOtp::query()
            ->where('email', $email)
            ->whereNull('consumed_at')
            ->update(['consumed_at' => now()]);

        PasswordResetOtp::query()->create([
            'email' => $email,
            'otp_hash' => Hash::make($otp),
            'expires_at' => now()->addMinutes((int) $this->settings->get('email.password_reset_otp_expires_minutes', 10)),
            'ip_address' => $request->ip(),
            'user_agent' => (string) $request->userAgent(),
        ]);

        try {
            $this->mailer->send($user, $otp);
        } catch (Throwable $exception) {
            report($exception);

            if ($request->expectsJson()) {
                return response()->json([
                    'message' => 'We could not send the OTP right now. Please verify Brevo or mail settings.',
                    'errors' => [
                        'email' => ['We could not send the OTP right now. Please verify Brevo or mail settings.'],
                    ],
                ], 422);
            }

            return back()
                ->withInput($request->only('email'))
                ->withErrors(['email' => 'We could not send the OTP right now. Please verify Brevo or mail settings.']);
        }

        RateLimiter::hit($this->otpThrottleKey($email, $request), 60);
        $request->session()->put('password_reset_email', $email);

        if ($request->expectsJson()) {
            return response()->json([
                'message' => 'If this email exists, a password reset OTP has been sent.',
                'email' => $email,
            ]);
        }

        return redirect()
            ->route('password.otp.verify')
            ->with('status', 'If this email exists, a password reset OTP has been sent.');
    }

    public function resetForm(Request $request): View
    {
        return view('auth.reset-password', [
            'email' => old('email', $request->session()->get('password_reset_email')),
        ]);
    }

    public function reset(Request $request): RedirectResponse|JsonResponse
    {
        $validated = $request->validate([
            'email' => ['required', 'email', 'max:255'],
            'otp' => ['required', 'digits:6'],
            'password' => ['required', 'confirmed', Password::min(8)->mixedCase()->numbers()],
        ]);

        $email = Str::lower((string) $validated['email']);
        $user = User::query()->where('email', $email)->first();
        $otpRecord = PasswordResetOtp::query()
            ->where('email', $email)
            ->whereNull('consumed_at')
            ->latest()
            ->first();

        if (! $user || ! $otpRecord || $otpRecord->isExpired()) {
            throw ValidationException::withMessages([
                'otp' => 'The OTP is invalid or expired.',
            ]);
        }

        if ($otpRecord->attempts >= 5) {
            $otpRecord->forceFill(['consumed_at' => now()])->save();

            throw ValidationException::withMessages([
                'otp' => 'Too many incorrect OTP attempts. Please request a new OTP.',
            ]);
        }

        if (! Hash::check((string) $validated['otp'], $otpRecord->otp_hash)) {
            $otpRecord->increment('attempts');

            throw ValidationException::withMessages([
                'otp' => 'The OTP is invalid or expired.',
            ]);
        }

        $user->forceFill([
            'password' => Hash::make((string) $validated['password']),
            'password_set_at' => now(),
        ])->save();

        PasswordResetOtp::query()
            ->where('email', $email)
            ->whereNull('consumed_at')
            ->update(['consumed_at' => now()]);

        $request->session()->forget('password_reset_email');

        if ($request->expectsJson()) {
            return response()->json([
                'message' => 'Your password has been reset. You can now login.',
                'login_url' => route('login'),
            ]);
        }

        return redirect()
            ->route('login')
            ->with('status', 'Your password has been reset. You can now login.');
    }

    private function ensureOtpRequestIsNotRateLimited(Request $request, string $email): void
    {
        if (! RateLimiter::tooManyAttempts($this->otpThrottleKey($email, $request), 3)) {
            return;
        }

        $seconds = RateLimiter::availableIn($this->otpThrottleKey($email, $request));

        throw ValidationException::withMessages([
            'email' => "Please wait {$seconds} seconds before requesting another OTP.",
        ]);
    }

    private function otpThrottleKey(string $email, Request $request): string
    {
        return 'password-reset-otp|'.$email.'|'.$request->ip();
    }
}
