<?php

namespace App\Services;

use App\Exceptions\DomainException;
use App\Models\Appointment;
use App\Models\QueueCounter;
use App\Models\QueueEntry;
use App\Models\Setting;
use App\Models\User;
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\DB;

class TokenService
{
    public function queueDate(User $actor, Appointment $appointment): string
    {
        $timezone = $actor->organization?->timezone ?? config('clinic.default_timezone');

        return CarbonImmutable::instance($appointment->scheduled_start_at)
            ->setTimezone($timezone)
            ->toDateString();
    }

    public function scopeKey(int $organizationId, int $doctorId, int $locationId): string
    {
        $scope = $this->setting($organizationId, 'token_scope', 'doctor_location');

        return $scope === 'location'
            ? "location:{$locationId}"
            : "doctor:{$doctorId}:location:{$locationId}";
    }

    public function reserve(
        User $actor,
        Appointment $appointment,
        string $queueDate,
        ?int $manualToken = null
    ): array {
        $scopeKey = $this->scopeKey(
            $actor->organization_id,
            $appointment->doctor_id,
            $appointment->location_id
        );

        DB::table('queue_counters')->insertOrIgnore([
            'organization_id' => $actor->organization_id,
            'queue_scope_key' => $scopeKey,
            'queue_date' => $queueDate,
            'last_allocated_token' => max(0, (int) $this->setting($actor->organization_id, 'token_start', 1) - 1),
            'current_served_token' => null,
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        /** @var QueueCounter $counter */
        $counter = QueueCounter::query()
            ->where('organization_id', $actor->organization_id)
            ->where('queue_scope_key', $scopeKey)
            ->whereDate('queue_date', $queueDate)
            ->lockForUpdate()
            ->firstOrFail();

        if ($manualToken !== null) {
            $this->assertTokenInRange($actor->organization_id, $manualToken);
            $this->assertTokenFree($actor->organization_id, $scopeKey, $queueDate, $manualToken);

            if ($manualToken > $counter->last_allocated_token) {
                $counter->last_allocated_token = $manualToken;
                $counter->save();
            }

            return [$manualToken, $scopeKey, $counter];
        }

        $existingMax = (int) QueueEntry::query()
            ->where('organization_id', $actor->organization_id)
            ->where('queue_scope_key', $scopeKey)
            ->whereDate('queue_date', $queueDate)
            ->max('token_number');

        $start = (int) $this->setting($actor->organization_id, 'token_start', 1);
        $candidate = max($counter->last_allocated_token, $existingMax, $start - 1) + 1;

        $max = (int) $this->setting($actor->organization_id, 'token_max', 9999);
        if ($candidate > $max) {
            throw new DomainException('TOKEN_RANGE_EXHAUSTED', 'No more token numbers are available for this queue.', 409);
        }

        $this->assertTokenFree($actor->organization_id, $scopeKey, $queueDate, $candidate);
        $counter->update(['last_allocated_token' => $candidate]);

        return [$candidate, $scopeKey, $counter->fresh()];
    }

    public function assertTokenFree(
        int $organizationId,
        string $scopeKey,
        string $queueDate,
        int $token,
        ?int $ignoreQueueEntryId = null
    ): void {
        $query = QueueEntry::query()
            ->where('organization_id', $organizationId)
            ->where('queue_scope_key', $scopeKey)
            ->whereDate('queue_date', $queueDate)
            ->where('token_number', $token);

        if ($ignoreQueueEntryId) {
            $query->whereKeyNot($ignoreQueueEntryId);
        }

        if ($query->exists()) {
            throw new DomainException(
                'TOKEN_ALREADY_IN_USE',
                "Token {$token} is already assigned in this queue.",
                409,
                ['token_number' => ['Choose another token.']]
            );
        }
    }

    public function assertTokenInRange(int $organizationId, int $token): void
    {
        $min = (int) $this->setting($organizationId, 'token_min', 1);
        $max = (int) $this->setting($organizationId, 'token_max', 9999);

        if ($token < $min || $token > $max) {
            throw new DomainException(
                'INVALID_TOKEN',
                "Token must be between {$min} and {$max}.",
                422,
                ['token_number' => ["Token must be between {$min} and {$max}."]]
            );
        }
    }

    public function setting(int $organizationId, string $key, mixed $default = null): mixed
    {
        $setting = Setting::query()
            ->where('organization_id', $organizationId)
            ->where('key', $key)
            ->first();

        if (! $setting) {
            return $default;
        }

        $value = $setting->value;

        if (is_array($value) && array_is_list($value) && count($value) === 1) {
            return $value[0];
        }

        return $value ?? $default;
    }
}
