<?php

namespace Modules\Auth\Services;

use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Modules\Auth\Contracts\SmsSender;
use Modules\Auth\Entities\PhoneVerificationOtp;
use Modules\Auth\Entities\User;

class PhoneVerificationOtpService
{
    public function __construct(
        protected SmsSender $smsSender,
    ) {}

    public function send(string $phoneNumber): void
    {
        $phoneNumber = $this->normalizePhoneNumber($phoneNumber);

        if ($this->phoneNumberBelongsToAnotherUser($phoneNumber)) {
            throw ValidationException::withMessages([
                'phone_number' => ['Nombor telefon ini telah didaftarkan.'],
            ]);
        }

        $this->dispatchOtp($phoneNumber);
    }

    public function sendForUser(User $user, string $phoneNumber): void
    {
        $phoneNumber = $this->normalizePhoneNumber($phoneNumber);

        $this->assertPhoneNumberAvailableForUser($user, $phoneNumber);

        if ($user->phone_verified_at && $user->phone_number === $phoneNumber) {
            throw ValidationException::withMessages([
                'phone_number' => ['Nombor telefon ini sama dengan nombor sedia ada.'],
            ]);
        }

        $this->dispatchOtp($phoneNumber);
    }

    public function verifyForUser(User $user, string $phoneNumber, string $otp): User
    {
        $phoneNumber = $this->normalizePhoneNumber($phoneNumber);

        $this->assertPhoneNumberAvailableForUser($user, $phoneNumber);

        $record = $this->validateOtpRecord($phoneNumber, $otp);
        $record->delete();

        $user->forceFill([
            'phone_number' => $phoneNumber,
            'phone_verified_at' => now(),
        ])->save();

        return $user->fresh();
    }

    /**
     * @return array{verification_token: string, phone_number: string}
     */
    public function verify(string $phoneNumber, string $otp): array
    {
        $phoneNumber = $this->normalizePhoneNumber($phoneNumber);

        $record = $this->validateOtpRecord($phoneNumber, $otp);

        $verificationToken = Str::random(64);

        $record->forceFill([
            'verified_at' => now(),
            'verification_token' => Hash::make($verificationToken),
            'verification_token_expires_at' => now()->addMinutes($this->tokenExpiryMinutes()),
        ])->save();

        return [
            'verification_token' => $verificationToken,
            'phone_number' => $phoneNumber,
        ];
    }

    public function consumeRegistrationToken(string $phoneNumber, string $verificationToken): void
    {
        $phoneNumber = $this->normalizePhoneNumber($phoneNumber);

        $record = PhoneVerificationOtp::query()
            ->where('phone_number', $phoneNumber)
            ->whereNotNull('verified_at')
            ->latest()
            ->first();

        if (! $record || $record->isVerificationTokenExpired()) {
            throw ValidationException::withMessages([
                'phone_verification_token' => ['Pengesahan nombor telefon tidak sah atau telah tamat tempoh. Sila sahkan semula.'],
            ]);
        }

        if (! Hash::check($verificationToken, (string) $record->verification_token)) {
            throw ValidationException::withMessages([
                'phone_verification_token' => ['Pengesahan nombor telefon tidak sah atau telah tamat tempoh. Sila sahkan semula.'],
            ]);
        }

        $record->delete();
    }

    protected function dispatchOtp(string $phoneNumber): void
    {
        $otp = $this->generateOtp();

        PhoneVerificationOtp::query()
            ->where('phone_number', $phoneNumber)
            ->delete();

        PhoneVerificationOtp::create([
            'phone_number' => $phoneNumber,
            'code' => Hash::make($otp),
            'expires_at' => now()->addMinutes($this->otpExpiryMinutes()),
            'attempts' => 0,
        ]);

        $this->smsSender->send(
            $phoneNumber,
            $this->buildOtpMessage($otp),
        );
    }

    protected function validateOtpRecord(string $phoneNumber, string $otp): PhoneVerificationOtp
    {
        $record = PhoneVerificationOtp::query()
            ->where('phone_number', $phoneNumber)
            ->latest()
            ->first();

        if (! $record) {
            throw ValidationException::withMessages([
                'otp' => ['Kod OTP tidak dijumpai. Sila minta kod baharu.'],
            ]);
        }

        if ($record->isExpired()) {
            $record->delete();

            throw ValidationException::withMessages([
                'otp' => ['Kod OTP telah tamat tempoh. Sila minta kod baharu.'],
            ]);
        }

        if ($record->hasExceededMaxAttempts()) {
            $record->delete();

            throw ValidationException::withMessages([
                'otp' => ['Terlalu banyak percubaan. Sila minta kod baharu.'],
            ]);
        }

        if (! Hash::check($otp, $record->code)) {
            $record->increment('attempts');

            if ($record->fresh()->hasExceededMaxAttempts()) {
                $record->delete();
            }

            throw ValidationException::withMessages([
                'otp' => ['Kod OTP tidak sah.'],
            ]);
        }

        return $record;
    }

    protected function assertPhoneNumberAvailableForUser(User $user, string $phoneNumber): void
    {
        if ($this->phoneNumberBelongsToAnotherUser($phoneNumber, $user->id)) {
            throw ValidationException::withMessages([
                'phone_number' => ['Nombor telefon ini telah digunakan oleh akaun lain.'],
            ]);
        }
    }

    protected function phoneNumberBelongsToAnotherUser(string $phoneNumber, ?string $exceptUserId = null): bool
    {
        return User::query()
            ->where('phone_number', $phoneNumber)
            ->when($exceptUserId, fn ($query) => $query->where('id', '!=', $exceptUserId))
            ->exists();
    }

    public function normalizePhoneNumber(string $phoneNumber): string
    {
        $phoneNumber = preg_replace('/[\s\-]/', '', trim($phoneNumber)) ?? '';

        if (str_starts_with($phoneNumber, '+60')) {
            $phoneNumber = '0'.substr($phoneNumber, 3);
        } elseif (str_starts_with($phoneNumber, '60') && strlen($phoneNumber) > 10) {
            $phoneNumber = '0'.substr($phoneNumber, 2);
        }

        return $phoneNumber;
    }

    public function phoneNumberRules(): array
    {
        return [
            'required',
            'string',
            'max:20',
            'regex:/^(\+?60|0)1[0-9]{8,9}$/',
        ];
    }

    protected function generateOtp(): string
    {
        return str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
    }

    protected function buildOtpMessage(string $otp): string
    {
        $template = (string) config('auth.phone_verification.message');

        return str_replace(
            [':otp', ':minutes'],
            [$otp, (string) $this->otpExpiryMinutes()],
            $template,
        );
    }

    protected function otpExpiryMinutes(): int
    {
        return (int) config('auth.phone_verification.otp_expiry_minutes', 10);
    }

    protected function tokenExpiryMinutes(): int
    {
        return (int) config('auth.phone_verification.token_expiry_minutes', 30);
    }
}
