<?php

namespace App\Models;

use App\Enums\QrCodeType;
use Database\Factories\QrCodeFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\URL;

/**
 * A dynamic or static QR code managed by the admin.
 *
 * NOTE: This class name collides with `Endroid\QrCode\QrCode`, the image
 * generation library used to render these codes. When a file needs both,
 * alias the library import, e.g. `use Endroid\QrCode\QrCode as EndroidQrCode;`
 * — do not alias this model, since it is the conventional name referenced
 * throughout the rest of the app.
 *
 * @property int $id
 * @property string $short_code
 * @property string $name
 * @property QrCodeType $type
 * @property array<string, mixed>|null $payload
 * @property string|null $target_url
 * @property bool $is_dynamic
 * @property bool $is_active
 * @property int $scan_count
 * @property Carbon|null $last_scanned_at
 * @property Carbon|null $created_at
 * @property Carbon|null $updated_at
 * @property Carbon|null $deleted_at
 * @property-read Collection<int, QrCodeScan> $scans
 * @property-read Collection<int, QrCodeRender> $renders
 */
#[Fillable([
    'short_code', 'name', 'type', 'payload', 'target_url', 'is_dynamic',
    'is_active', 'scan_count', 'last_scanned_at',
])]
class QrCode extends Model
{
    /** @use HasFactory<QrCodeFactory> */
    use HasFactory, SoftDeletes;

    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 'qr_codes';

    /**
     * Length of a generated short code.
     */
    private const SHORT_CODE_LENGTH = 8;

    /**
     * Characters used to generate short codes, excluding visually ambiguous
     * ones (0/O, 1/l/I) since codes are sometimes read aloud or hand-typed.
     */
    private const SHORT_CODE_ALPHABET = '23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';

    /**
     * Get the attributes that should be cast.
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'type' => QrCodeType::class,
            'payload' => 'array',
            'is_dynamic' => 'boolean',
            'is_active' => 'boolean',
            'last_scanned_at' => 'datetime',
        ];
    }

    /**
     * Get the route key for the model.
     */
    public function getRouteKeyName(): string
    {
        return 'short_code';
    }

    /**
     * Get all scans recorded for this QR code.
     *
     * @return HasMany<QrCodeScan, $this>
     */
    public function scans(): HasMany
    {
        return $this->hasMany(QrCodeScan::class);
    }

    /**
     * Get all image render/download events for this QR code.
     *
     * @return HasMany<QrCodeRender, $this>
     */
    public function renders(): HasMany
    {
        return $this->hasMany(QrCodeRender::class);
    }

    /**
     * Scope a query to only include active QR codes.
     *
     * @param  Builder<QrCode>  $query
     * @return Builder<QrCode>
     */
    public function scopeActive(Builder $query): Builder
    {
        return $query->where('is_active', true);
    }

    /**
     * Get the public URL that this QR code encodes / redirects through.
     *
     * Built directly from the `/q/{short_code}` path rather than a named
     * `route()` call, since the redirect route is added by another part of
     * this feature and may not exist yet when this model is used.
     */
    public function publicUrl(): string
    {
        return URL::to('/q/'.$this->short_code);
    }

    /**
     * Generate a unique, URL-safe short code for a new QR code.
     *
     * Excludes visually ambiguous characters (0/O, 1/l/I) since codes are
     * sometimes read aloud or hand-typed from a printed piece.
     */
    public static function generateShortCode(): string
    {
        do {
            $code = self::randomCode();
        } while (static::query()->withTrashed()->where('short_code', $code)->exists());

        return $code;
    }

    /**
     * Build a random code from the unambiguous short-code alphabet.
     */
    private static function randomCode(): string
    {
        $alphabetLength = strlen(self::SHORT_CODE_ALPHABET);

        $code = '';

        for ($i = 0; $i < self::SHORT_CODE_LENGTH; $i++) {
            $code .= self::SHORT_CODE_ALPHABET[random_int(0, $alphabetLength - 1)];
        }

        return $code;
    }
}
