<?php

namespace Modules\Identity\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Modules\Identity\Database\Factories\RegistrationVerificationFactory;
use Modules\Identity\Mail\RegistrationCodeMail;
use Modules\Shared\Sms\SmsService;

class RegistrationVerification extends Model
{
    /** @use HasFactory<RegistrationVerificationFactory> */
    use HasFactory;

    /**
     * A code is only good for this long — kept short since it's delivered
     * over email/SMS and re-requesting a fresh one is cheap (throttled by
     * the api-otp rate limiter).
     */
    private const CODE_LIFETIME_MINUTES = 10;

    /**
     * Wrong-code guesses allowed before the code is locked out and a fresh
     * one must be requested.
     */
    private const MAX_ATTEMPTS = 5;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'identifier',
        'type',
        'code',
        'attempts',
        'verified_at',
        'verification_token',
        'consumed_at',
        'expires_at',
    ];

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'verified_at' => 'datetime',
            'consumed_at' => 'datetime',
            'expires_at' => 'datetime',
        ];
    }

    /**
     * Generates a fresh code for the identifier and delivers it over
     * email or SMS, replacing any previous pending verification for the
     * same identifier (resend just supersedes the old code).
     */
    public static function issueFor(string $identifier, string $type): self
    {
        $code = (string) random_int(100000, 999999);

        $verification = self::query()->updateOrCreate(
            ['identifier' => $identifier],
            [
                'type' => $type,
                'code' => Hash::make($code),
                'attempts' => 0,
                'verified_at' => null,
                'verification_token' => null,
                'consumed_at' => null,
                'expires_at' => now()->addMinutes(self::CODE_LIFETIME_MINUTES),
            ],
        );

        $verification->deliver($code);

        return $verification;
    }

    /**
     * Checks the given code against this pending verification. On success,
     * marks it verified and issues the one-time token step 3 (registration)
     * will need to complete the flow.
     */
    public function attemptVerify(string $code): bool
    {
        if ($this->isExpired() || $this->attempts >= self::MAX_ATTEMPTS || ! Hash::check($code, $this->code)) {
            $this->increment('attempts');

            return false;
        }

        $this->forceFill([
            'verified_at' => now(),
            'verification_token' => Str::random(64),
        ])->save();

        return true;
    }

    public function isExpired(): bool
    {
        return $this->expires_at->isPast();
    }

    public function isVerified(): bool
    {
        return $this->verified_at !== null;
    }

    public function isConsumed(): bool
    {
        return $this->consumed_at !== null;
    }

    private function deliver(string $code): void
    {
        if ($this->type === 'email') {
            Mail::to($this->identifier)->send(new RegistrationCodeMail($code));

            return;
        }

        $appName = config('app.name');
        app(SmsService::class)->send(
            $this->identifier,
            "{$appName}: Your verification code is {$code}. It expires in ".self::CODE_LIFETIME_MINUTES.' minutes.',
        );
    }
}
