<?php

namespace App\Domains\Auth\Services;

use App\Domains\Auth\Models\ResetPasswordAttempt;
use App\Domains\Auth\Models\User;
use App\Domains\Authorization\Enums\RoleCode;
use App\Domains\Notification\Events\PasswordResetRequested;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Throwable;

class ResetPasswordAttemptService
{
    public const CODE_VALID = 'valid';

    public const CODE_INVALID = 'invalid';

    public const CODE_EXPIRED = 'expired';

    public function createForEmail(
        string $email,
        string $tenantCode,
        string $reason = ResetPasswordAttempt::REASON_MANUAL,
    ): void {
        $emailFingerprint = $this->emailFingerprint($email);

        try {
            $attemptId = DB::transaction(function () use ($email, $emailFingerprint, $reason): ?int {
                $user = User::query()
                    ->where('email', $email)
                    ->lockForUpdate()
                    ->first();

                return $this->createAttemptForUser(
                    $user,
                    $reason,
                    $emailFingerprint,
                    'Password reset attempt was not created because the user was not found.',
                );
            });
        } catch (Throwable $exception) {
            Log::error('Failed to create password reset attempt.', [
                'email_fingerprint' => $emailFingerprint,
                'exception' => $exception,
            ]);

            throw $exception;
        }

        $this->dispatchPasswordResetRequested(
            $attemptId,
            $tenantCode,
            PasswordResetRequested::CHANNEL_STOREFRONT,
            $emailFingerprint,
        );
    }

    public function createForAdminAppEmail(
        string $email,
        string $reason = ResetPasswordAttempt::REASON_MANUAL,
    ): void {
        $emailFingerprint = $this->emailFingerprint($email);

        try {
            $result = DB::transaction(function () use ($email, $emailFingerprint, $reason): ?array {
                $user = User::query()
                    ->where('email', $email)
                    ->where('rol_codigo', RoleCode::AdminApp->value)
                    ->whereNotNull('tenant_codigo')
                    ->lockForUpdate()
                    ->first();

                $attemptId = $this->createAttemptForUser(
                    $user,
                    $reason,
                    $emailFingerprint,
                    'AdminApp password reset attempt was not created because the user was not found.',
                );

                if ($user === null || $attemptId === null) {
                    return null;
                }

                return [
                    'attempt_id' => $attemptId,
                    'tenant_code' => $user->tenant_codigo,
                ];
            });
        } catch (Throwable $exception) {
            Log::error('Failed to create AdminApp password reset attempt.', [
                'email_fingerprint' => $emailFingerprint,
                'exception' => $exception,
            ]);

            throw $exception;
        }

        $this->dispatchPasswordResetRequested(
            $result['attempt_id'] ?? null,
            $result['tenant_code'] ?? null,
            PasswordResetRequested::CHANNEL_ADMINAPP,
            $emailFingerprint,
        );
    }

    public function createForScannerEmail(
        string $email,
        string $reason = ResetPasswordAttempt::REASON_MANUAL,
    ): void {
        $emailFingerprint = $this->emailFingerprint($email);

        try {
            $result = DB::transaction(function () use ($email, $emailFingerprint, $reason): ?array {
                $user = User::query()
                    ->where('email', $email)
                    ->where('rol_codigo', RoleCode::Scanner->value)
                    ->whereNotNull('tenant_codigo')
                    ->lockForUpdate()
                    ->first();

                $attemptId = $this->createAttemptForUser(
                    $user,
                    $reason,
                    $emailFingerprint,
                    'Scanner password reset attempt was not created because the user was not found.',
                );

                if ($user === null || $attemptId === null) {
                    return null;
                }

                return [
                    'attempt_id' => $attemptId,
                    'tenant_code' => $user->tenant_codigo,
                ];
            });
        } catch (Throwable $exception) {
            Log::error('Failed to create Scanner password reset attempt.', [
                'email_fingerprint' => $emailFingerprint,
                'exception' => $exception,
            ]);

            throw $exception;
        }

        $this->dispatchPasswordResetRequested(
            $result['attempt_id'] ?? null,
            $result['tenant_code'] ?? null,
            PasswordResetRequested::CHANNEL_SCANNER,
            $emailFingerprint,
        );
    }

    public function validateCode(string $email, string $code): string
    {
        $emailFingerprint = $this->emailFingerprint($email);

        try {
            return DB::transaction(function () use ($email, $code, $emailFingerprint): string {
                $user = User::query()
                    ->where('email', $email)
                    ->lockForUpdate()
                    ->first();

                $attempt = $user?->resetPasswordAttempts()
                    ->where('codigo', $code)
                    ->where('status', ResetPasswordAttempt::STATUS_PENDING)
                    ->latest('id')
                    ->lockForUpdate()
                    ->first();

                if ($attempt === null) {
                    Log::warning('Password reset code validation failed: no matching pending attempt.', [
                        'email_fingerprint' => $emailFingerprint,
                    ]);

                    return self::CODE_INVALID;
                }

                if ($attempt->expires_at?->isPast()) {
                    $attempt->update([
                        'status' => ResetPasswordAttempt::STATUS_EXPIRED,
                    ]);

                    Log::info('Password reset code validation failed: attempt expired.', [
                        'email_fingerprint' => $emailFingerprint,
                        'attempt_id' => $attempt->getKey(),
                    ]);

                    return self::CODE_EXPIRED;
                }

                $attempt->update([
                    'status' => ResetPasswordAttempt::STATUS_VALIDATED,
                ]);

                return self::CODE_VALID;
            });
        } catch (Throwable $exception) {
            Log::error('Failed to validate password reset code.', [
                'email_fingerprint' => $emailFingerprint,
                'exception' => $exception,
            ]);

            throw $exception;
        }
    }

    public function resetPassword(string $email, string $code, string $password): bool
    {
        $emailFingerprint = $this->emailFingerprint($email);

        try {
            return DB::transaction(function () use ($email, $code, $password, $emailFingerprint): bool {
                $user = User::query()
                    ->where('email', $email)
                    ->lockForUpdate()
                    ->first();

                $attempt = $user?->resetPasswordAttempts()
                    ->where('codigo', $code)
                    ->where('status', ResetPasswordAttempt::STATUS_VALIDATED)
                    ->latest('id')
                    ->lockForUpdate()
                    ->first();

                if ($user === null || $attempt === null) {
                    Log::warning('Password reset failed: no matching validated attempt.', [
                        'email_fingerprint' => $emailFingerprint,
                    ]);

                    return false;
                }

                if ($attempt->expires_at?->isPast()) {
                    $attempt->update([
                        'status' => ResetPasswordAttempt::STATUS_EXPIRED,
                    ]);

                    Log::info('Password reset failed: attempt expired.', [
                        'email_fingerprint' => $emailFingerprint,
                        'attempt_id' => $attempt->getKey(),
                    ]);

                    return false;
                }

                $user->password = $password;
                $user->failed_login_attempts = 0;
                $user->last_failed_login_at = null;
                $user->locked_until = null;
                $user->save();
                $user->tokens()->delete();

                $user->resetPasswordAttempts()
                    ->whereKeyNot($attempt->getKey())
                    ->whereIn('status', [
                        ResetPasswordAttempt::STATUS_PENDING,
                        ResetPasswordAttempt::STATUS_VALIDATED,
                    ])
                    ->update(['status' => ResetPasswordAttempt::STATUS_EXPIRED]);

                $attempt->update([
                    'status' => ResetPasswordAttempt::STATUS_USED,
                ]);

                return true;
            });
        } catch (Throwable $exception) {
            Log::error('Failed to reset user password.', [
                'email_fingerprint' => $emailFingerprint,
                'exception' => $exception,
            ]);

            throw $exception;
        }
    }

    private function createAttemptForUser(
        ?User $user,
        string $reason,
        string $emailFingerprint,
        string $userNotFoundMessage,
    ): ?int {
        if ($user === null) {
            Log::warning($userNotFoundMessage, [
                'email_fingerprint' => $emailFingerprint,
            ]);

            return null;
        }

        $user->resetPasswordAttempts()
            ->whereIn('status', [
                ResetPasswordAttempt::STATUS_PENDING,
                ResetPasswordAttempt::STATUS_VALIDATED,
            ])
            ->update(['status' => ResetPasswordAttempt::STATUS_EXPIRED]);

        $attempt = $user->resetPasswordAttempts()->create([
            'codigo' => $this->generateCode(),
            'reason' => $reason,
            'status' => ResetPasswordAttempt::STATUS_PENDING,
            'expires_at' => now()->addMinutes((int) config('auth.passwords.users.expire')),
        ]);

        return $attempt->getKey();
    }

    private function dispatchPasswordResetRequested(
        ?int $attemptId,
        ?string $tenantCode,
        string $channel,
        string $emailFingerprint,
    ): void {
        if ($attemptId === null || $tenantCode === null) {
            return;
        }

        try {
            PasswordResetRequested::dispatch($attemptId, $tenantCode, $channel);
        } catch (Throwable $exception) {
            Log::error('Failed to dispatch password reset email.', [
                'attempt_id' => $attemptId,
                'tenant_code' => $tenantCode,
                'channel' => $channel,
                'email_fingerprint' => $emailFingerprint,
                'exception' => $exception,
            ]);

            throw $exception;
        }
    }

    private function generateCode(): string
    {
        return str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT);
    }

    private function emailFingerprint(string $email): string
    {
        return substr(hash('sha256', strtolower(trim($email))), 0, 12);
    }
}
