<?php

namespace App\Services\Care;

use App\Models\Appointment;
use App\Models\Organization;
use App\Models\Patient;
use App\Models\Practitioner;
use App\Models\Visit;
use App\Services\Care\Workflow\WorkflowQueueGate;
use App\Services\Care\Workflow\WorkflowStageCompletionService;
use App\Services\Meet\MeetClient;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
use InvalidArgumentException;

class AppointmentService
{
    public function __construct(
        protected VisitService $visits,
        protected AppointmentPatientNotifier $notifier,
        protected CareQueueBridge $queueBridge,
        protected WorkflowQueueGate $workflowGate,
        protected WorkflowStageCompletionService $workflowCompletion,
    ) {}

    public function queueContextFor(Organization $organization, Appointment $appointment): string
    {
        return $this->queueBridge->contextForAppointment($organization, $appointment);
    }

    public function queryForOrganization(string $ownerRef, int $organizationId): Builder
    {
        return Appointment::owned($ownerRef)->where('organization_id', $organizationId);
    }

    /**
     * @param  array<string, mixed>  $filters
     * @param  list<int>|null  $practitionerIds  When set, only appointments assigned to these desks.
     */
    public function list(
        string $ownerRef,
        int $organizationId,
        array $filters = [],
        ?int $branchId = null,
        ?array $practitionerIds = null,
    ): LengthAwarePaginator {
        $query = $this->queryForOrganization($ownerRef, $organizationId)
            ->with(['patient', 'practitioner', 'branch', 'department'])
            ->orderByDesc('scheduled_at');

        if ($branchId !== null) {
            $query->where('branch_id', $branchId);
        }

        if ($practitionerIds !== null) {
            if ($practitionerIds === []) {
                $query->whereRaw('1 = 0');
            } else {
                $query->whereIn('practitioner_id', $practitionerIds);
            }
        }

        if ($status = $filters['status'] ?? null) {
            $query->where('status', $status);
        }

        if ($practitionerId = $filters['practitioner_id'] ?? null) {
            $query->where('practitioner_id', $practitionerId);
        }

        if ($date = $filters['date'] ?? null) {
            $query->whereDate('scheduled_at', Carbon::parse($date)->toDateString());
        }

        if ($patientId = $filters['patient_id'] ?? null) {
            $query->where('patient_id', $patientId);
        }

        return $query->paginate((int) ($filters['per_page'] ?? 20))->withQueryString();
    }

    /** Max waiters / in-care rows rendered on the patient-flow board. */
    public const QUEUE_BOARD_LIMIT = 50;

    /**
     * @param  list<int>|null  $practitionerIds
     * @return Collection<int, Appointment>
     */
    public function queue(
        string $ownerRef,
        int $branchId,
        ?int $practitionerId = null,
        bool $includeUnassigned = true,
        ?array $practitionerIds = null,
        ?int $limit = null,
    ): Collection {
        if ($practitionerIds !== null) {
            return $this->queueForPractitioners(
                $ownerRef,
                $practitionerIds,
                $branchId > 0 ? $branchId : null,
                $limit,
            );
        }

        $this->repairDuplicateQueuePositions($ownerRef, $branchId);

        $limit = $limit ?? self::QUEUE_BOARD_LIMIT;

        $query = Appointment::owned($ownerRef)
            ->where('branch_id', $branchId)
            ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN])
            ->with($this->queueEagerLoads())
            ->orderBy('queue_position')
            ->orderBy('waiting_at')
            ->orderBy('checked_in_at')
            ->limit($limit);

        if ($practitionerId !== null) {
            $query->where(function (Builder $q) use ($practitionerId, $includeUnassigned) {
                $q->where('practitioner_id', $practitionerId);
                if ($includeUnassigned) {
                    $q->orWhereNull('practitioner_id');
                }
            });
        }

        return $query->get()
            ->filter(function (Appointment $appointment) {
                $organization = $appointment->organization;
                if (! $organization) {
                    return true;
                }

                return $this->workflowGate->isReleasedForDisplay(
                    $organization,
                    $appointment->visit,
                    $this->queueBridge->contextForAppointment($organization, $appointment),
                );
            })
            ->values();
    }

    /**
     * Doctor queue: only appointments assigned to their linked practitioner desk(s).
     *
     * @param  list<int>  $practitionerIds
     * @return Collection<int, Appointment>
     */
    public function queueForPractitioners(
        string $ownerRef,
        array $practitionerIds,
        ?int $branchId = null,
        ?int $limit = null,
    ): Collection {
        if ($practitionerIds === []) {
            return new Collection;
        }

        $limit = $limit ?? self::QUEUE_BOARD_LIMIT;

        $query = Appointment::owned($ownerRef)
            ->whereIn('practitioner_id', $practitionerIds)
            ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN])
            ->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
            ->with($this->queueEagerLoads())
            ->orderBy('queue_position')
            ->orderBy('waiting_at')
            ->orderBy('checked_in_at')
            ->limit($limit);

        $practitioners = Practitioner::owned($ownerRef)
            ->whereIn('id', $practitionerIds)
            ->get();

        $allowedSpecialtyContexts = [];
        $orgIds = $practitioners->pluck('organization_id')->filter()->unique()->all();
        $orgsById = \App\Models\Organization::query()
            ->whereIn('id', $orgIds ?: [0])
            ->get()
            ->keyBy('id');

        foreach ($practitioners as $practitioner) {
            $org = $orgsById->get((int) $practitioner->organization_id);
            if (! $org) {
                continue;
            }
            $deskContext = $this->queueBridge->contextForPractitioner($org, $practitioner);
            if ($deskContext !== CareQueueContexts::CONSULTATION) {
                $allowedSpecialtyContexts[$deskContext] = true;
            }
        }
        $allowedSpecialtyContexts = array_keys($allowedSpecialtyContexts);

        return new Collection(
            $query->get()
                ->filter(function (Appointment $appointment) use ($allowedSpecialtyContexts) {
                    $organization = $appointment->organization;
                    if (! $organization) {
                        return true;
                    }

                    $context = $this->queueBridge->contextForAppointment($organization, $appointment);
                    // GPs see consultation only. Specialty desks see their own specialty waiters on Queue.
                    if ($context !== CareQueueContexts::CONSULTATION
                        && ! in_array($context, $allowedSpecialtyContexts, true)) {
                        return false;
                    }

                    return $this->workflowGate->isReleasedForDisplay(
                        $organization,
                        $appointment->visit,
                        $context,
                    );
                })
                ->values()
                ->all()
        );
    }

    /**
     * @return list<string|array<string, mixed>>
     */
    protected function queueEagerLoads(): array
    {
        return [
            'patient',
            'practitioner',
            'organization',
            'visit.currentStageAdvance.stage',
        ];
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public function book(Organization $organization, string $ownerRef, array $data, ?string $actorRef = null): Appointment
    {
        $appointment = Appointment::create([
            'owner_ref' => $ownerRef,
            'organization_id' => $organization->id,
            'branch_id' => $data['branch_id'],
            'patient_id' => $data['patient_id'],
            'practitioner_id' => $data['practitioner_id'] ?? null,
            'department_id' => $data['department_id'] ?? null,
            'type' => Appointment::TYPE_SCHEDULED,
            'status' => Appointment::STATUS_SCHEDULED,
            'scheduled_at' => $data['scheduled_at'],
            'reason' => $data['reason'] ?? null,
            'notes' => $data['notes'] ?? null,
            'created_by' => $actorRef,
        ]);

        AuditLogger::record($ownerRef, 'appointment.created', $organization->id, $actorRef, Appointment::class, $appointment->id);

        $appointment = $appointment->load(['patient', 'practitioner', 'branch']);
        $this->notifier->notifyAppointmentBooked($organization, $appointment);

        return $appointment;
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public function walkIn(Organization $organization, string $ownerRef, array $data, ?string $actorRef = null): Appointment
    {
        $visit = $this->visits->checkIn(
            $organization,
            $ownerRef,
            Patient::findOrFail($data['patient_id']),
            (int) $data['branch_id'],
            $actorRef,
        );

        $appointment = Appointment::create([
            'owner_ref' => $ownerRef,
            'organization_id' => $organization->id,
            'branch_id' => $data['branch_id'],
            'patient_id' => $data['patient_id'],
            'practitioner_id' => $data['practitioner_id'] ?? null,
            'department_id' => $data['department_id'] ?? null,
            'visit_id' => $visit->id,
            'type' => Appointment::TYPE_WALK_IN,
            'status' => Appointment::STATUS_WAITING,
            'scheduled_at' => now(),
            'checked_in_at' => now(),
            'waiting_at' => now(),
            'queue_position' => $this->nextQueuePosition($ownerRef, (int) $data['branch_id']),
            'reason' => $data['reason'] ?? 'Walk-in',
            'notes' => $data['notes'] ?? null,
            'created_by' => $actorRef,
        ]);

        AuditLogger::record($ownerRef, 'appointment.walk_in', $organization->id, $actorRef, Appointment::class, $appointment->id);

        $appointment = $appointment->load(['patient', 'practitioner', 'branch', 'visit', 'department']);
        $this->queueBridge->issueForAppointment($organization, $appointment);

        if ($appointment->department?->type === 'ambulance' && $appointment->visit) {
            $appointment->visit->update([
                'specialty_stage' => \App\Services\Care\Ambulance\AmbulanceWorkflowService::STAGE_CHECK_IN,
            ]);
        }

        return $appointment->fresh(['patient', 'practitioner', 'branch', 'visit']);
    }

    public function checkIn(Appointment $appointment, string $ownerRef, ?string $actorRef = null): Appointment
    {
        $this->assertTransition($appointment, Appointment::STATUS_SCHEDULED);

        $visit = $this->visits->checkIn(
            $appointment->organization,
            $ownerRef,
            $appointment->patient,
            $appointment->branch_id,
            $actorRef,
        );

        $appointment->update([
            'visit_id' => $visit->id,
            'status' => Appointment::STATUS_WAITING,
            'checked_in_at' => now(),
            'waiting_at' => now(),
            'queue_position' => $this->nextQueuePosition($ownerRef, $appointment->branch_id),
        ]);

        AuditLogger::record($ownerRef, 'appointment.checked_in', $appointment->organization_id, $actorRef, Appointment::class, $appointment->id);

        $appointment = $appointment->fresh(['patient', 'practitioner', 'visit', 'organization']);
        $this->queueBridge->issueForAppointment($appointment->organization, $appointment);

        return $appointment->fresh(['patient', 'practitioner', 'visit']);
    }

    public function markWaiting(Appointment $appointment, string $ownerRef, ?string $actorRef = null): Appointment
    {
        if (! in_array($appointment->status, [Appointment::STATUS_CHECKED_IN, Appointment::STATUS_SCHEDULED], true)) {
            $this->assertTransition($appointment, Appointment::STATUS_CHECKED_IN);
        }

        $updates = [
            'status' => Appointment::STATUS_WAITING,
            'waiting_at' => $appointment->waiting_at ?? now(),
        ];

        if ($appointment->queue_position === null) {
            $updates['queue_position'] = $this->nextQueuePosition($ownerRef, $appointment->branch_id);
        }

        $appointment->update($updates);
        AuditLogger::record($ownerRef, 'appointment.waiting', $appointment->organization_id, $actorRef, Appointment::class, $appointment->id);

        $appointment = $appointment->fresh(['patient', 'practitioner', 'organization']);
        if ($appointment->organization) {
            $this->queueBridge->issueForAppointment($appointment->organization, $appointment);
        }

        return $appointment->fresh(['patient', 'practitioner']);
    }

    public function startConsultation(Appointment $appointment, string $ownerRef, ?int $practitionerId = null, ?string $actorRef = null): Appointment
    {
        $this->assertTransition($appointment, Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN);

        if ($appointment->visit_id === null) {
            $visit = $this->visits->checkIn(
                $appointment->organization,
                $ownerRef,
                $appointment->patient,
                $appointment->branch_id,
                $actorRef,
            );
            $appointment->update(['visit_id' => $visit->id, 'checked_in_at' => now()]);
        }

        $appointment->loadMissing(['organization', 'visit']);
        if ($appointment->organization && ! $this->workflowGate->canRelease(
            $appointment->organization,
            $appointment->visit,
            $this->queueBridge->contextForAppointment($appointment->organization, $appointment),
        )) {
            throw new InvalidArgumentException('This visit has not reached a financially cleared consultation stage.');
        }

        $appointment->visit->update(['status' => Visit::STATUS_IN_PROGRESS]);

        $appointment->update([
            'status' => Appointment::STATUS_IN_CONSULTATION,
            'started_at' => now(),
            'practitioner_id' => $practitionerId ?? $appointment->practitioner_id,
        ]);

        AuditLogger::record($ownerRef, 'appointment.in_consultation', $appointment->organization_id, $actorRef, Appointment::class, $appointment->id);

        $appointment = $appointment->fresh(['patient', 'practitioner', 'visit', 'organization']);
        if ($appointment->organization) {
            $this->queueBridge->serve($appointment->organization, $appointment);
        }

        return $appointment->fresh(['patient', 'practitioner', 'visit']);
    }

    public function complete(Appointment $appointment, string $ownerRef, ?string $actorRef = null): Appointment
    {
        $this->assertTransition($appointment, Appointment::STATUS_IN_CONSULTATION);

        $appointment->update([
            'status' => Appointment::STATUS_COMPLETED,
            'completed_at' => now(),
        ]);

        $visit = $appointment->visit;
        $organization = $appointment->organization;
        $workflowEnabled = $visit
            && $organization
            && $this->workflowGate->enabled($organization);

        AuditLogger::record($ownerRef, 'appointment.completed', $appointment->organization_id, $actorRef, Appointment::class, $appointment->id);

        $appointment = $appointment->fresh(['patient', 'practitioner', 'visit', 'organization']);
        if ($appointment->organization) {
            $this->queueBridge->complete($appointment->organization, $appointment);
        }

        if ($workflowEnabled && $visit && $organization) {
            $this->workflowCompletion->complete(
                $organization,
                $visit,
                $this->queueBridge->contextForAppointment($organization, $appointment),
                $ownerRef,
                $actorRef,
            );
        } elseif ($visit) {
            $this->visits->complete($visit, $ownerRef, $actorRef);
        }

        return $appointment->fresh(['patient', 'practitioner', 'visit']);
    }

    public function cancel(Appointment $appointment, string $ownerRef, ?string $actorRef = null): Appointment
    {
        if (in_array($appointment->status, [Appointment::STATUS_COMPLETED, Appointment::STATUS_CANCELLED], true)) {
            throw new InvalidArgumentException('Appointment cannot be cancelled.');
        }

        $meetRoomUuid = filled($appointment->meet_room_uuid)
            ? (string) $appointment->meet_room_uuid
            : null;

        $appointment->update([
            'status' => Appointment::STATUS_CANCELLED,
            'cancelled_at' => now(),
        ]);

        AuditLogger::record($ownerRef, 'appointment.cancelled', $appointment->organization_id, $actorRef, Appointment::class, $appointment->id);

        if ($meetRoomUuid !== null) {
            $this->cancelLinkedMeetRoom($appointment, $ownerRef, $meetRoomUuid, $actorRef);
        }

        return $appointment->fresh(['patient', 'practitioner']);
    }

    protected function cancelLinkedMeetRoom(
        Appointment $appointment,
        string $ownerRef,
        string $roomUuid,
        ?string $actorRef = null,
    ): void {
        try {
            MeetClient::for($ownerRef)->cancelRoom($roomUuid, $actorRef ?? $ownerRef);
            AuditLogger::record(
                $ownerRef,
                'appointment.meet_cancelled',
                $appointment->organization_id,
                $actorRef,
                Appointment::class,
                $appointment->id,
                ['meet_room_uuid' => $roomUuid],
            );
        } catch (\Throwable $e) {
            Log::warning('Care appointment cancel: could not cancel linked Meet room', [
                'appointment_id' => $appointment->id,
                'meet_room_uuid' => $roomUuid,
                'error' => $e->getMessage(),
            ]);
        }
    }

    public function markNoShow(Appointment $appointment, string $ownerRef, ?string $actorRef = null): Appointment
    {
        $this->assertTransition($appointment, Appointment::STATUS_SCHEDULED);

        $appointment->update(['status' => Appointment::STATUS_NO_SHOW]);
        AuditLogger::record($ownerRef, 'appointment.no_show', $appointment->organization_id, $actorRef, Appointment::class, $appointment->id);

        return $appointment->fresh(['patient', 'practitioner']);
    }

    public function callNext(string $ownerRef, int $branchId, ?int $practitionerId = null): ?Appointment
    {
        $next = $this->queue($ownerRef, $branchId, $practitionerId)->first();

        return $next;
    }

    protected function nextQueuePosition(string $ownerRef, int $branchId): int
    {
        $max = Appointment::owned($ownerRef)
            ->where('branch_id', $branchId)
            ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN])
            ->max('queue_position');

        return ($max ?? 0) + 1;
    }

    /**
     * Demo specialty seed used to set every module's waiter to position 1.
     * Repair duplicates in place so the patient queue shows 1..n.
     * Cheap existence check first so healthy queues skip the full rewrite.
     */
    protected function repairDuplicateQueuePositions(string $ownerRef, int $branchId): void
    {
        $base = Appointment::owned($ownerRef)
            ->where('branch_id', $branchId)
            ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]);

        $waiterCount = (clone $base)->count();
        if ($waiterCount < 2) {
            return;
        }

        $hasNulls = (clone $base)->whereNull('queue_position')->exists();
        $distinctPositions = (int) (clone $base)
            ->whereNotNull('queue_position')
            ->selectRaw('count(distinct queue_position) as aggregate')
            ->value('aggregate');
        if (! $hasNulls && $distinctPositions === $waiterCount) {
            return;
        }

        $waiters = (clone $base)
            ->orderByRaw('COALESCE(waiting_at, checked_in_at, scheduled_at) asc')
            ->orderBy('id')
            ->get(['id', 'queue_position']);

        foreach ($waiters as $index => $appointment) {
            $position = $index + 1;
            if ((int) $appointment->queue_position === $position) {
                continue;
            }
            Appointment::whereKey($appointment->id)->update(['queue_position' => $position]);
        }
    }

    protected function assertTransition(Appointment $appointment, string ...$allowedFrom): void
    {
        if (! in_array($appointment->status, $allowedFrom, true)) {
            throw new InvalidArgumentException("Cannot transition from {$appointment->status}.");
        }
    }
}
