<?php

namespace App\Services;

use App\Enums\AppointmentStatus;
use App\Enums\QueueStatus;
use App\Exceptions\DomainException;
use App\Models\Appointment;
use App\Models\AppointmentStatusHistory;
use App\Models\QueueCounter;
use App\Models\QueueEntry;
use App\Models\QueueStatusHistory;
use App\Models\User;
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\DB;

class QueueService
{
    public function __construct(
        private readonly AccessService $access,
        private readonly TokenService $tokens,
        private readonly AuditService $audit,
        private readonly PatientMetricsService $patientMetrics,
    ) {}

    public function checkIn(User $actor, Appointment $appointment, string $tokenMode = 'auto', ?int $manualToken = null): QueueEntry
    {
        $this->access->assertOrganization($actor, $appointment);

        if (! $this->access->canCheckIn($actor, $appointment)) {
            throw new DomainException('FORBIDDEN', 'You cannot check in this appointment.', 403);
        }

        $timezone = $actor->organization?->timezone ?? config('clinic.default_timezone');
        $queueDate = $this->tokens->queueDate($actor, $appointment);
        $today = CarbonImmutable::now($timezone)->toDateString();

        if ($queueDate !== $today && ! $this->access->isAdmin($actor)) {
            throw new DomainException(
                'CHECK_IN_DATE_MISMATCH',
                'Appointments may only be checked in on their scheduled clinic date.',
                409
            );
        }

        return DB::transaction(function () use ($actor, $appointment, $tokenMode, $manualToken, $queueDate) {
            $appointment = Appointment::query()->lockForUpdate()->findOrFail($appointment->id);

            $existing = QueueEntry::query()
                ->where('appointment_id', $appointment->id)
                ->whereDate('queue_date', $queueDate)
                ->lockForUpdate()
                ->first();

            if ($existing) {
                return $existing->load(['patient', 'doctor.user', 'location', 'appointment']);
            }

            if (! in_array($appointment->status, [
                AppointmentStatus::CONFIRMED,
                AppointmentStatus::CHECKED_IN,
                AppointmentStatus::WAITING,
            ], true)) {
                throw new DomainException('INVALID_STATUS_TRANSITION', 'This appointment cannot be checked in.', 409);
            }

            [$token, $scopeKey] = $this->tokens->reserve(
                $actor,
                $appointment,
                $queueDate,
                $tokenMode === 'manual' ? $manualToken : null
            );

            $entry = QueueEntry::create([
                'organization_id' => $actor->organization_id,
                'appointment_id' => $appointment->id,
                'patient_id' => $appointment->patient_id,
                'doctor_id' => $appointment->doctor_id,
                'location_id' => $appointment->location_id,
                'queue_scope_key' => $scopeKey,
                'queue_date' => $queueDate,
                'token_number' => $token,
                'status' => QueueStatus::WAITING,
                'arrival_at' => now(),
                'created_by_user_id' => $actor->id,
            ]);

            QueueStatusHistory::create([
                'queue_entry_id' => $entry->id,
                'event_type' => 'token.assigned',
                'from_status' => null,
                'to_status' => QueueStatus::WAITING->value,
                'new_token_number' => $token,
                'changed_by_user_id' => $actor->id,
                'created_at' => now(),
            ]);

            $this->forceAppointmentPathToWaiting($actor, $appointment);

            $this->audit->log('token.assigned', $actor, $entry, null, [
                'queue_date' => $queueDate,
                'scope' => $scopeKey,
                'token_number' => $token,
                'appointment_id' => $appointment->id,
            ]);

            return $entry->fresh(['patient', 'doctor.user', 'location', 'appointment']);
        }, 3);
    }

    public function updateToken(User $actor, QueueEntry $entry, int $newToken): QueueEntry
    {
        if (! $this->access->scopeQueue(QueueEntry::query()->whereKey($entry->id), $actor)->exists()) {
            throw new DomainException('FORBIDDEN', 'You cannot edit this queue entry.', 403);
        }

        if (! $this->access->isAdmin($actor)
            && ! $this->access->canUseDoctorLocation($actor, $entry->doctor_id, $entry->location_id, 'can_check_in')) {
            throw new DomainException('FORBIDDEN', 'You cannot edit tokens for this queue.', 403);
        }

        return DB::transaction(function () use ($actor, $entry, $newToken) {
            $entry = QueueEntry::query()->lockForUpdate()->findOrFail($entry->id);
            $oldToken = $entry->token_number;

            if ($oldToken === $newToken) {
                return $entry;
            }

            $this->tokens->assertTokenInRange($actor->organization_id, $newToken);
            $this->tokens->assertTokenFree(
                $actor->organization_id,
                $entry->queue_scope_key,
                $entry->queue_date->toDateString(),
                $newToken,
                $entry->id
            );

            $entry->update(['token_number' => $newToken]);

            $counter = QueueCounter::query()
                ->where('organization_id', $actor->organization_id)
                ->where('queue_scope_key', $entry->queue_scope_key)
                ->whereDate('queue_date', $entry->queue_date)
                ->lockForUpdate()
                ->first();

            if ($counter) {
                $updates = ['last_allocated_token' => max($counter->last_allocated_token, $newToken)];
                if ($counter->current_served_token === $oldToken) {
                    $updates['current_served_token'] = $newToken;
                }
                $counter->update($updates);
            }

            QueueStatusHistory::create([
                'queue_entry_id' => $entry->id,
                'event_type' => 'token.changed',
                'from_status' => $entry->status->value,
                'to_status' => $entry->status->value,
                'old_token_number' => $oldToken,
                'new_token_number' => $newToken,
                'changed_by_user_id' => $actor->id,
                'created_at' => now(),
            ]);

            $this->audit->log('token.changed', $actor, $entry, [
                'token_number' => $oldToken,
            ], [
                'token_number' => $newToken,
            ], [
                'queue_date' => $entry->queue_date->toDateString(),
                'scope' => $entry->queue_scope_key,
            ]);

            return $entry->fresh(['patient', 'doctor.user', 'location', 'appointment']);
        }, 3);
    }

    public function changeStatus(User $actor, QueueEntry $entry, QueueStatus $to, ?string $reason = null): QueueEntry
    {
        if (! $this->access->scopeQueue(QueueEntry::query()->whereKey($entry->id), $actor)->exists()) {
            throw new DomainException('FORBIDDEN', 'You cannot update this queue entry.', 403);
        }

        if (! $this->access->isAdmin($actor)
            && ! $this->access->canUseDoctorLocation($actor, $entry->doctor_id, $entry->location_id, 'can_check_in')) {
            throw new DomainException('FORBIDDEN', 'You cannot change this queue status.', 403);
        }

        return DB::transaction(function () use ($actor, $entry, $to, $reason) {
            $entry = QueueEntry::query()->lockForUpdate()->findOrFail($entry->id);
            $from = $entry->status;

            if ($from === $to) {
                return $entry;
            }

            if (! in_array($to, $from->allowedNext(), true) && ! $this->access->isAdmin($actor)) {
                throw new DomainException(
                    'INVALID_STATUS_TRANSITION',
                    "Cannot move queue entry from {$from->value} to {$to->value}.",
                    409
                );
            }

            $updates = ['status' => $to];

            if ($to === QueueStatus::WITH_DOCTOR) {
                $updates['sent_to_doctor_at'] = now();
            }
            if ($to === QueueStatus::COMPLETED) {
                $updates['completed_at'] = now();
            }
            if ($to === QueueStatus::NO_SHOW) {
                $updates['no_show_at'] = now();
            }

            $entry->update($updates);

            QueueStatusHistory::create([
                'queue_entry_id' => $entry->id,
                'event_type' => 'queue.status_changed',
                'from_status' => $from->value,
                'to_status' => $to->value,
                'reason' => $reason,
                'changed_by_user_id' => $actor->id,
                'created_at' => now(),
            ]);

            $this->syncAppointmentStatus($actor, $entry->appointment()->lockForUpdate()->firstOrFail(), $to, $reason);

            if ($to === QueueStatus::COMPLETED || $from === QueueStatus::COMPLETED) {
                $this->patientMetrics->refresh($entry->patient_id);
            }

            $this->audit->log('queue.status_changed', $actor, $entry, [
                'status' => $from->value,
            ], [
                'status' => $to->value,
            ], ['reason' => $reason]);

            return $entry->fresh(['patient', 'doctor.user', 'location', 'appointment']);
        }, 3);
    }

    public function callNext(User $actor, int $doctorId, int $locationId, string $queueDate): ?QueueEntry
    {
        if (! $this->access->isAdmin($actor)
            && ! $this->access->canUseDoctorLocation($actor, $doctorId, $locationId, 'can_check_in')) {
            throw new DomainException('FORBIDDEN', 'You cannot advance this queue.', 403);
        }

        return DB::transaction(function () use ($actor, $doctorId, $locationId, $queueDate) {
            $scopeKey = $this->tokens->scopeKey($actor->organization_id, $doctorId, $locationId);

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

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

            $query = QueueEntry::query()
                ->where('organization_id', $actor->organization_id)
                ->where('queue_scope_key', $scopeKey)
                ->whereDate('queue_date', $queueDate)
                ->where('status', QueueStatus::WAITING->value);

            if ($counter->current_served_token !== null) {
                $query->where('token_number', '>', $counter->current_served_token);
            }

            /** @var QueueEntry|null $next */
            $next = $query->orderBy('token_number')->lockForUpdate()->first();

            if (! $next) {
                return null;
            }

            $from = $next->status;
            $next->update([
                'status' => QueueStatus::WITH_DOCTOR,
                'sent_to_doctor_at' => now(),
            ]);
            $counter->update(['current_served_token' => $next->token_number]);

            QueueStatusHistory::create([
                'queue_entry_id' => $next->id,
                'event_type' => 'queue.call_next',
                'from_status' => $from->value,
                'to_status' => QueueStatus::WITH_DOCTOR->value,
                'changed_by_user_id' => $actor->id,
                'created_at' => now(),
            ]);

            $appointment = $next->appointment()->lockForUpdate()->firstOrFail();
            $this->syncAppointmentStatus($actor, $appointment, QueueStatus::WITH_DOCTOR, 'Called as next token');

            $this->audit->log('queue.call_next', $actor, $next, [
                'current_served_token' => $counter->getOriginal('current_served_token'),
            ], [
                'current_served_token' => $next->token_number,
            ]);

            return $next->fresh(['patient', 'doctor.user', 'location', 'appointment']);
        }, 3);
    }

    private function forceAppointmentPathToWaiting(User $actor, Appointment $appointment): void
    {
        $appointment = Appointment::query()->lockForUpdate()->findOrFail($appointment->id);

        if ($appointment->status === AppointmentStatus::CONFIRMED) {
            $this->writeAppointmentTransition($appointment, AppointmentStatus::CHECKED_IN, $actor, 'Checked in');
        }

        if ($appointment->status === AppointmentStatus::CHECKED_IN) {
            $this->writeAppointmentTransition($appointment, AppointmentStatus::WAITING, $actor, 'Added to waiting queue');
        }
    }

    private function syncAppointmentStatus(User $actor, Appointment $appointment, QueueStatus $queueStatus, ?string $reason): void
    {
        $target = match ($queueStatus) {
            QueueStatus::CHECKED_IN => AppointmentStatus::CHECKED_IN,
            QueueStatus::WAITING => AppointmentStatus::WAITING,
            QueueStatus::WITH_DOCTOR => AppointmentStatus::WITH_DOCTOR,
            QueueStatus::COMPLETED => AppointmentStatus::COMPLETED,
            QueueStatus::NO_SHOW => AppointmentStatus::NO_SHOW,
            QueueStatus::CANCELLED => AppointmentStatus::CANCELLED,
        };

        if ($appointment->status === $target) {
            return;
        }

        if (! in_array($target, $appointment->status->allowedNext(), true) && ! $this->access->isAdmin($actor)) {
            throw new DomainException(
                'INVALID_STATUS_TRANSITION',
                "Appointment cannot move from {$appointment->status->value} to {$target->value}.",
                409
            );
        }

        $this->writeAppointmentTransition($appointment, $target, $actor, $reason);
    }

    private function writeAppointmentTransition(
        Appointment $appointment,
        AppointmentStatus $to,
        User $actor,
        ?string $reason
    ): void {
        $from = $appointment->status;
        $appointment->status = $to;
        $appointment->save();

        AppointmentStatusHistory::create([
            'appointment_id' => $appointment->id,
            'from_status' => $from->value,
            'to_status' => $to->value,
            'reason' => $reason,
            'changed_by_user_id' => $actor->id,
            'created_at' => now(),
        ]);

        $this->audit->log('appointment.status_changed', $actor, $appointment, [
            'status' => $from->value,
        ], [
            'status' => $to->value,
        ], ['reason' => $reason]);
    }
}
