<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api\V1\Auth;

use App\Enums\AuthEventType;
use App\Models\User;
use App\Services\Audit\AuditEvent;
use App\Services\Audit\AuditRecorder;
use App\Services\Auth\AuthEventRecorder;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Password as PasswordRule;
use Illuminate\Validation\ValidationException;

final class PasswordResetController extends Controller
{
    public function __construct(
        private readonly AuditRecorder $audit,
        private readonly AuthEventRecorder $authEvents,
    ) {}

    /**
     * Begin a reset.
     *
     * Always answers the same way whether or not the address is registered:
     * a differentiated response would turn this endpoint into an account
     * enumeration oracle.
     */
    public function sendResetLink(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'email' => ['required', 'string', 'email', 'max:255'],
        ]);

        $email = mb_strtolower(trim($validated['email']));

        Password::sendResetLink(['email' => $email]);

        $this->authEvents->record(
            AuthEventType::PasswordResetRequested,
            user: User::query()->where('email', $email)->first(),
            emailAttempted: $email,
        );

        return response()->json([
            'message' => 'If an account exists for that address, a reset link is on its way.',
        ]);
    }

    /** Complete a reset with the emailed token. */
    public function reset(Request $request): JsonResponse
    {
        $request->validate([
            'token' => ['required', 'string'],
            'email' => ['required', 'string', 'email'],
            'password' => [
                'required',
                'string',
                'confirmed',
                // Resolved from the application-wide default; see
                // AppServiceProvider::configurePasswords().
                PasswordRule::defaults(),
            ],
        ], [
            'password.uncompromised' => 'This password has appeared in a known data breach. Please choose a different one.',
        ]);

        $status = Password::reset(
            $request->only('email', 'password', 'password_confirmation', 'token'),
            function (User $user, string $password): void {
                $user->forceFill([
                    'password' => $password,
                    'remember_token' => Str::random(60),
                    'password_changed_at' => now(),
                ])->save();

                /*
                 * Every existing token is revoked. A reset is the standard
                 * response to a suspected compromise, so any session an
                 * attacker already holds has to die with the old password.
                 */
                $user->tokens()->delete();

                $this->audit->record(AuditEvent::USER_PASSWORD_CHANGED, $user, context: ['via' => 'reset_link'], actor: $user);
                $this->authEvents->record(AuthEventType::PasswordChanged, $user, metadata: ['via' => 'reset_link']);

                event(new PasswordReset($user));
            }
        );

        if ($status !== Password::PasswordReset) {
            throw ValidationException::withMessages([
                'email' => [__($status)],
            ]);
        }

        return response()->json([
            'message' => 'Password updated. You can now sign in, and any other devices have been signed out.',
        ]);
    }
}
