<?php

namespace App\Services\Care;

use App\Models\Appointment;
use App\Models\Bill;
use App\Models\CareQueueTicket;
use App\Models\InvestigationRequest;
use App\Models\Member;
use App\Models\Organization;
use App\Models\Patient;
use App\Models\Practitioner;
use App\Models\Prescription;
use App\Services\Care\Workflow\WorkflowQueueGate;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;

/**
 * Bridges Care workflow entities to Care Queue Engine tickets.
 */
class CareQueueBridge
{
    /** @var array<int, array<int, string>> organization_id => [department_id => specialty_key] */
    protected array $specialtyDepartmentMaps = [];

    public function __construct(
        protected CareQueueEngine $engine,
        protected CareQueueProvisioner $provisioner,
        protected PlanService $plans,
        protected SpecialtyModuleService $specialties,
        protected WorkflowQueueGate $workflowGate,
    ) {}

    public function isEnabled(Organization $organization): bool
    {
        return $this->plans->canUseQueueIntegration($organization)
            && (bool) data_get($organization->settings, 'queue_integration_enabled');
    }

    public function contextForAppointment(Organization $organization, Appointment $appointment): string
    {
        $departmentId = $appointment->department_id;
        if (! $departmentId) {
            return CareQueueContexts::CONSULTATION;
        }

        return $this->contextForDepartment($organization, (int) $departmentId);
    }

    public function contextForDepartment(Organization $organization, int $departmentId): string
    {
        if ($departmentId <= 0) {
            return CareQueueContexts::CONSULTATION;
        }

        return $this->specialtyDepartmentMap($organization)[$departmentId]
            ?? CareQueueContexts::CONSULTATION;
    }

    /**
     * Department id → specialty module key for this org (request-scoped cache).
     *
     * @return array<int, string>
     */
    public function specialtyDepartmentMap(Organization $organization): array
    {
        $orgId = (int) $organization->id;
        if (isset($this->specialtyDepartmentMaps[$orgId])) {
            return $this->specialtyDepartmentMaps[$orgId];
        }

        $map = [];
        foreach ($this->specialties->enabledKeys($organization) as $key) {
            $deptIds = data_get($organization->settings, "specialty_module_provisioning.{$key}.department_ids", []);
            if (! is_array($deptIds)) {
                continue;
            }
            foreach ($deptIds as $deptId) {
                $map[(int) $deptId] = $key;
            }
        }

        return $this->specialtyDepartmentMaps[$orgId] = $map;
    }

    public function contextForPractitioner(Organization $organization, ?Practitioner $practitioner): string
    {
        if (! $practitioner?->department_id) {
            return CareQueueContexts::CONSULTATION;
        }

        return $this->contextForDepartment($organization, (int) $practitioner->department_id);
    }

    public function issueForAppointment(Organization $organization, Appointment $appointment): ?Appointment
    {
        if (! $this->isEnabled($organization) || filled($appointment->queue_ticket_uuid)) {
            return $appointment;
        }

        $appointment->loadMissing('visit');
        $context = $this->contextForAppointment($organization, $appointment);
        if (! $this->workflowGate->canRelease($organization, $appointment->visit, $context)) {
            return $appointment;
        }

        $point = $this->resolveAppointmentPoint($organization, $appointment);

        if (CareQueueContexts::usesAssignedRouting($context) && empty($point['counter_uuid'] ?? null)) {
            $appointment->forceFill([
                'queue_routing_status' => CareQueueContexts::ROUTING_UNRESOLVED,
            ])->save();
            Log::info('care.queue_issue_unresolved_assignment', [
                'appointment_id' => $appointment->id,
                'practitioner_id' => $appointment->practitioner_id,
                'context' => $context,
            ]);

            return $appointment->fresh();
        }

        $ticket = $this->issue(
            $organization,
            $context,
            (int) $appointment->branch_id,
            $appointment->patient,
            [
                'care_entity' => 'appointment',
                'care_entity_uuid' => $appointment->uuid,
                'care_entity_id' => $appointment->id,
                'practitioner_id' => $appointment->practitioner_id,
            ],
            $appointment->type === Appointment::TYPE_WALK_IN ? 'walk_in' : 'appointment',
            'appointment',
            $point,
        );

        if (! $ticket) {
            return $appointment;
        }

        $this->applyTicket($appointment, $ticket, $point);

        return $appointment->fresh();
    }

    public function issueForPrescription(Organization $organization, Prescription $prescription): ?Prescription
    {
        if (! $this->isEnabled($organization) || filled($prescription->queue_ticket_uuid)) {
            return $prescription;
        }

        $prescription->loadMissing(['patient', 'visit']);
        if (! $this->workflowGate->canRelease($organization, $prescription->visit, CareQueueContexts::PHARMACY)) {
            return $prescription;
        }

        $branchId = (int) ($prescription->visit?->branch_id ?? 0);
        if ($branchId < 1) {
            return $prescription;
        }

        $ticket = $this->issue(
            $organization,
            CareQueueContexts::PHARMACY,
            $branchId,
            $prescription->patient,
            [
                'care_entity' => 'prescription',
                'care_entity_uuid' => $prescription->uuid,
                'care_entity_id' => $prescription->id,
            ],
            'walk_in',
            'api',
            null,
        );

        if (! $ticket) {
            return $prescription;
        }

        $this->applyTicket($prescription, $ticket, null);

        return $prescription->fresh();
    }

    public function issueForInvestigation(Organization $organization, InvestigationRequest $request): ?InvestigationRequest
    {
        if (! $this->isEnabled($organization) || filled($request->queue_ticket_uuid)) {
            return $request;
        }

        $request->loadMissing(['patient', 'visit', 'investigationType']);
        $context = $this->contextForInvestigation($request);
        if (! $this->workflowGate->canRelease($organization, $request->visit, $context)) {
            return $request;
        }

        $ticket = $this->issue(
            $organization,
            $context,
            (int) $request->branch_id,
            $request->patient,
            [
                'care_entity' => 'investigation_request',
                'care_entity_uuid' => $request->uuid,
                'care_entity_id' => $request->id,
            ],
            'walk_in',
            'api',
            null,
        );

        if (! $ticket) {
            return $request;
        }

        $this->applyTicket($request, $ticket, null);

        return $request->fresh();
    }

    public function contextForInvestigation(InvestigationRequest $request): string
    {
        $request->loadMissing('investigationType');

        return in_array($request->investigationType?->category, ['xray', 'ultrasound', 'ct', 'mri'], true)
            ? CareQueueContexts::IMAGING
            : CareQueueContexts::LABORATORY;
    }

    public function issueForBill(Organization $organization, Bill $bill): ?Bill
    {
        if (! $this->isEnabled($organization) || filled($bill->queue_ticket_uuid)) {
            return $bill;
        }

        if (in_array($bill->status, [Bill::STATUS_PAID, Bill::STATUS_VOID], true)) {
            return $bill;
        }

        $bill->loadMissing(['patient', 'visit']);
        if (! $this->workflowGate->canRelease($organization, $bill->visit, CareQueueContexts::BILLING)) {
            return $bill;
        }

        $ticket = $this->issue(
            $organization,
            CareQueueContexts::BILLING,
            (int) $bill->branch_id,
            $bill->patient,
            [
                'care_entity' => 'bill',
                'care_entity_uuid' => $bill->uuid,
                'care_entity_id' => $bill->id,
            ],
            'walk_in',
            'api',
            null,
        );

        if (! $ticket) {
            return $bill;
        }

        $this->applyTicket($bill, $ticket, null);

        return $bill->fresh();
    }

    /**
     * After completing a stage, issue the next department ticket when applicable.
     */
    public function advanceAfterComplete(Organization $organization, Model $entity, string $fromContext): void
    {
        if (! $this->isEnabled($organization)) {
            return;
        }

        // Lab/imaging completion → return-to-doctor consultation ticket when practitioner known.
        if (in_array($fromContext, [CareQueueContexts::LABORATORY, CareQueueContexts::IMAGING], true)
            && $entity instanceof InvestigationRequest) {
            $entity->loadMissing(['patient', 'practitioner', 'visit']);
            $appointment = Appointment::owned($organization->owner_ref)
                ->where('organization_id', $organization->id)
                ->where('patient_id', $entity->patient_id)
                ->where('branch_id', $entity->branch_id)
                ->whereIn('status', [
                    Appointment::STATUS_IN_CONSULTATION,
                    Appointment::STATUS_WAITING,
                    Appointment::STATUS_CHECKED_IN,
                ])
                ->when($entity->practitioner_id, fn ($q) => $q->where('practitioner_id', $entity->practitioner_id))
                ->latest('id')
                ->first();

            if ($appointment && blank($appointment->queue_ticket_uuid)) {
                $this->issueForAppointment($organization, $appointment);
            }
        }
    }

    /**
     * @return int Number of tickets successfully issued
     */
    public function syncMissingTickets(Organization $organization, string $context, int $branchId, int $limit = 50): int
    {
        if (! $this->isEnabled($organization) || $branchId < 1 || $limit < 1) {
            return 0;
        }

        $this->provisioner->ensure($organization, $context, $branchId);

        return match (true) {
            $context === CareQueueContexts::PHARMACY => $this->syncMissingPrescriptions($organization, $branchId, $limit),
            in_array($context, [CareQueueContexts::LABORATORY, CareQueueContexts::IMAGING], true) => $this->syncMissingInvestigations($organization, $context, $branchId, $limit),
            $context === CareQueueContexts::BILLING => $this->syncMissingBills($organization, $branchId, $limit),
            default => $this->syncMissingAppointments($organization, $context, $branchId, $limit),
        };
    }

    /**
     * Issue Queue tickets for every waiting appointment at a branch (consultation + specialties).
     */
    public function syncMissingAppointmentTickets(Organization $organization, int $branchId, int $limit = 100): int
    {
        if (! $this->isEnabled($organization) || $branchId < 1 || $limit < 1) {
            return 0;
        }

        $owner = (string) $organization->owner_ref;
        $appointments = Appointment::owned($owner)
            ->where('organization_id', $organization->id)
            ->where('branch_id', $branchId)
            ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN])
            ->where(function ($q) {
                $q->whereNull('queue_ticket_uuid')->orWhere('queue_ticket_uuid', '');
            })
            ->with(['patient', 'organization', 'practitioner', 'visit'])
            ->orderBy('queue_position')
            ->orderBy('waiting_at')
            ->orderBy('checked_in_at')
            ->orderBy('id')
            ->limit($limit)
            ->get();

        $issued = 0;
        foreach ($appointments as $appointment) {
            $updated = $this->issueForAppointment($organization, $appointment->fresh([
                'patient', 'organization', 'practitioner', 'visit',
            ]));
            if ($updated && filled($updated->queue_ticket_uuid)) {
                $issued++;
            }
        }

        return $issued;
    }

    /**
     * @return array{ticket: ?array<string, mixed>, entity: ?Model, resources: ?array<string, mixed>}
     */
    public function callNext(
        Organization $organization,
        string $context,
        int $branchId,
        ?Member $member = null,
        ?int $practitionerId = null,
    ): array {
        $resources = $this->provisioner->ensure($organization, $context, $branchId);
        if (! $resources || empty($resources['queue_uuid'])) {
            return ['ticket' => null, 'entity' => null, 'resources' => $resources];
        }

        $point = $this->resolveCallNextPoint($organization, $context, $branchId, $member, $practitionerId, $resources);
        if (! $point || empty($point['counter_uuid'])) {
            return ['ticket' => null, 'entity' => null, 'resources' => $resources];
        }

        // GP / specialty: advance the next Care waiter (board order), even when their
        // ticket sits on another desk or was never issued (sync-on-GET was removed).
        if ($this->isAppointmentQueueContext($context)) {
            $fromCare = $this->callNextAppointment(
                $organization,
                $context,
                $branchId,
                $practitionerId,
                $resources,
                $point,
            );
            if ($fromCare['ticket']) {
                return $fromCare;
            }
        }

        $callResources = [
            'queue_uuid' => $resources['queue_uuid'],
            'counter_uuid' => $point['counter_uuid'],
        ];

        $ticket = $this->attemptCallNext($organization, $callResources);
        if (! $ticket) {
            $this->syncMissingTickets($organization, $context, $branchId, 25);
            $ticket = $this->attemptCallNext($organization, $callResources);
        }

        // Admins / reception without a desk: try every service point on this queue so
        // Call next matches the specialty waiting list instead of one empty desk.
        if (! $ticket && ! $practitionerId && (
            CareQueueContexts::isSpecialty($context)
            || $context === CareQueueContexts::CONSULTATION
        )) {
            $ticket = $this->attemptCallNextAcrossPoints($organization, $resources, $point);
        }

        if (! $ticket) {
            return ['ticket' => null, 'entity' => null, 'resources' => $resources];
        }

        $entity = $this->findEntityByTicket($organization, $context, (string) ($ticket['uuid'] ?? ''));
        if ($entity) {
            $matchedPoint = $point;
            $ticketPointUuid = (string) data_get($ticket, 'assigned_counter.uuid', data_get($ticket, 'counter.uuid', ''));
            if ($ticketPointUuid !== '') {
                $matchedPoint = collect($resources['points'] ?? [])->first(
                    fn ($p) => ($p['counter_uuid'] ?? null) === $ticketPointUuid
                ) ?? $point;
            }
            $this->applyTicket($entity, $ticket, $matchedPoint);
        }

        return ['ticket' => $ticket, 'entity' => $entity?->fresh(), 'resources' => $resources];
    }

    protected function isAppointmentQueueContext(string $context): bool
    {
        return $context === CareQueueContexts::CONSULTATION
            || CareQueueContexts::isSpecialty($context);
    }

    /**
     * Call the next waiting appointment for this context — matching board order.
     *
     * @param  array{queue_uuid?: ?string, counter_uuid?: ?string, points?: list<array<string, mixed>>}  $resources
     * @param  array<string, mixed>  $point
     * @return array{ticket: ?array<string, mixed>, entity: ?Model, resources: array<string, mixed>}
     */
    protected function callNextAppointment(
        Organization $organization,
        string $context,
        int $branchId,
        ?int $practitionerId,
        array $resources,
        array $point,
    ): array {
        $empty = ['ticket' => null, 'entity' => null, 'resources' => $resources];

        $candidates = $this->callableAppointments($organization, $context, $branchId, $practitionerId);
        if ($candidates === []) {
            $this->syncMissingTickets($organization, $context, $branchId, 25);
            $candidates = $this->callableAppointments($organization, $context, $branchId, $practitionerId);
        }

        foreach ($candidates as $appointment) {
            $called = $this->callSpecificAppointment($organization, $context, $branchId, $appointment, $resources, $point);
            if ($called['ticket']) {
                return $called;
            }
        }

        return $empty;
    }

    /**
     * @param  array{queue_uuid?: ?string, counter_uuid?: ?string, points?: list<array<string, mixed>>}  $resources
     * @param  array<string, mixed>  $point
     * @return array{ticket: ?array<string, mixed>, entity: ?Model, resources: array<string, mixed>}
     */
    protected function callSpecificAppointment(
        Organization $organization,
        string $context,
        int $branchId,
        Appointment $appointment,
        array $resources,
        array $point,
    ): array {
        $empty = ['ticket' => null, 'entity' => null, 'resources' => $resources];

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

        if (blank($appointment->queue_ticket_uuid)) {
            // Last resort: issue directly onto the calling desk.
            $ticket = $this->issue(
                $organization,
                $context,
                $branchId,
                $appointment->patient,
                [
                    'care_entity' => 'appointment',
                    'care_entity_uuid' => $appointment->uuid,
                    'care_entity_id' => $appointment->id,
                    'practitioner_id' => $appointment->practitioner_id,
                ],
                $appointment->type === Appointment::TYPE_WALK_IN ? 'walk_in' : 'appointment',
                'appointment',
                $point,
            );
            if ($ticket) {
                $this->applyTicket($appointment, $ticket, $point);
                $appointment = $appointment->fresh() ?? $appointment;
            }
        }

        if (blank($appointment->queue_ticket_uuid)) {
            return $empty;
        }

        $ticket = $this->attemptCallWaitingTicket(
            $organization,
            (string) $appointment->queue_ticket_uuid,
            (string) $point['counter_uuid'],
        );

        // Stale uuid / wrong queue / non-waiting row — clear and re-issue onto this desk.
        if (! $ticket) {
            $appointment->forceFill([
                'queue_ticket_uuid' => null,
                'queue_ticket_number' => null,
                'queue_ticket_status' => null,
                'queue_service_point_uuid' => null,
            ])->save();

            $ticket = $this->issue(
                $organization,
                $context,
                $branchId,
                $appointment->patient,
                [
                    'care_entity' => 'appointment',
                    'care_entity_uuid' => $appointment->uuid,
                    'care_entity_id' => $appointment->id,
                    'practitioner_id' => $appointment->practitioner_id,
                ],
                $appointment->type === Appointment::TYPE_WALK_IN ? 'walk_in' : 'appointment',
                'appointment',
                $point,
            );
            if (! $ticket) {
                return $empty;
            }
            $this->applyTicket($appointment, $ticket, $point);
            $appointment = $appointment->fresh() ?? $appointment;

            $ticket = $this->attemptCallWaitingTicket(
                $organization,
                (string) $appointment->queue_ticket_uuid,
                (string) $point['counter_uuid'],
            );
        }

        if (! $ticket) {
            return $empty;
        }

        $this->applyTicket($appointment, $ticket, $point);

        return [
            'ticket' => $ticket,
            'entity' => $appointment->fresh(),
            'resources' => $resources,
        ];
    }

    /**
     * @return list<Appointment>
     */
    protected function callableAppointments(
        Organization $organization,
        string $context,
        int $branchId,
        ?int $practitionerId,
    ): array {
        $owner = (string) $organization->owner_ref;

        $query = Appointment::owned($owner)
            ->where('organization_id', $organization->id)
            ->where('branch_id', $branchId)
            ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN])
            ->where(function ($q) {
                $q->whereNull('queue_ticket_status')
                    ->orWhere('queue_ticket_status', '')
                    ->orWhereNotIn('queue_ticket_status', ['called', 'serving']);
            })
            ->with(['patient', 'organization', 'practitioner', 'visit'])
            ->orderBy('queue_position')
            ->orderBy('waiting_at')
            ->orderBy('checked_in_at')
            ->orderBy('id')
            ->limit(50);

        if ($practitionerId) {
            $query->where('practitioner_id', $practitionerId);
        }

        $callable = [];
        foreach ($query->get() as $appointment) {
            if ($this->contextForAppointment($organization, $appointment) !== $context) {
                continue;
            }

            // Already ticketed waiters stay callable; gate only blocks new issuance.
            if (blank($appointment->queue_ticket_uuid)
                && ! $this->workflowGate->canRelease($organization, $appointment->visit, $context)) {
                continue;
            }

            $callable[] = $appointment;
        }

        return $callable;
    }

    /**
     * @return array<string, mixed>|null
     */
    protected function attemptCallWaitingTicket(
        Organization $organization,
        string $ticketUuid,
        string $counterUuid,
    ): ?array {
        try {
            return $this->engine->callWaitingTicket($organization, $ticketUuid, $counterUuid);
        } catch (\Throwable $e) {
            Log::warning('care.queue_call_waiting_ticket_failed', [
                'ticket_uuid' => $ticketUuid,
                'message' => $e->getMessage(),
            ]);

            return null;
        }
    }

    /**
     * @param  array{queue_uuid?: ?string, counter_uuid?: ?string, points?: list<array<string, mixed>>}  $resources
     * @param  array<string, mixed>  $preferredPoint
     * @return array<string, mixed>|null
     */
    protected function attemptCallNextAcrossPoints(
        Organization $organization,
        array $resources,
        array $preferredPoint,
    ): ?array {
        $queueUuid = (string) ($resources['queue_uuid'] ?? '');
        if ($queueUuid === '') {
            return null;
        }

        $preferredUuid = (string) ($preferredPoint['counter_uuid'] ?? '');
        $points = collect($resources['points'] ?? [])
            ->filter(fn ($p) => ! empty($p['counter_uuid']))
            ->sortBy(fn ($p) => ($p['counter_uuid'] ?? '') === $preferredUuid ? 0 : 1)
            ->values();

        foreach ($points as $candidate) {
            $uuid = (string) $candidate['counter_uuid'];
            if ($uuid === $preferredUuid) {
                continue;
            }
            $ticket = $this->attemptCallNext($organization, [
                'queue_uuid' => $queueUuid,
                'counter_uuid' => $uuid,
            ]);
            if ($ticket) {
                return $ticket;
            }
        }

        return null;
    }

    /**
     * @param  array{queue_uuid?: ?string, counter_uuid?: ?string}  $resources
     * @return array<string, mixed>|null
     */
    protected function attemptCallNext(Organization $organization, array $resources): ?array
    {
        try {
            return $this->engine->callNext(
                $organization,
                (string) $resources['queue_uuid'],
                (string) $resources['counter_uuid'],
            );
        } catch (\Throwable $e) {
            Log::warning('care.queue_call_next_failed', [
                'message' => $e->getMessage(),
            ]);

            return null;
        }
    }

    /**
     * @return array<string, mixed>|null
     */
    public function serve(Organization $organization, Model $entity): ?array
    {
        return $this->ticketAction($organization, $entity, 'start');
    }

    /**
     * @return array<string, mixed>|null
     */
    public function complete(Organization $organization, Model $entity, ?string $context = null): ?array
    {
        $ticket = $this->ticketAction($organization, $entity, 'complete');
        if ($ticket && $context) {
            $this->advanceAfterComplete($organization, $entity, $context);
        }

        return $ticket;
    }

    /**
     * @return array<string, mixed>|null
     */
    public function recall(Organization $organization, Model $entity): ?array
    {
        return $this->ticketAction($organization, $entity, 'recall');
    }

    /**
     * @return array{enabled: bool, context: string, queue_uuid: ?string, counter_uuid: ?string, points: list<array<string, mixed>>, routing_mode: ?string}
     */
    public function listMeta(Organization $organization, string $context, int $branchId): array
    {
        if (! $this->isEnabled($organization)) {
            return [
                'enabled' => false,
                'context' => $context,
                'queue_uuid' => null,
                'counter_uuid' => null,
                'points' => [],
                'routing_mode' => null,
            ];
        }

        $resources = $this->provisioner->ensure($organization, $context, $branchId);

        return [
            'enabled' => true,
            'context' => $context,
            'queue_uuid' => $resources['queue_uuid'] ?? null,
            'counter_uuid' => $resources['counter_uuid'] ?? null,
            'points' => $resources['points'] ?? [],
            'routing_mode' => $resources['routing_mode'] ?? null,
        ];
    }

    /**
     * @return array<string, mixed>|null
     */
    protected function resolveAppointmentPoint(Organization $organization, Appointment $appointment): ?array
    {
        $branchId = (int) $appointment->branch_id;
        $context = $this->contextForAppointment($organization, $appointment);

        $this->provisioner->ensure($organization, $context, $branchId);
        $organization = $organization->fresh() ?? $organization;

        if ($appointment->practitioner_id) {
            $point = $this->provisioner->pointForPractitioner(
                $organization,
                $branchId,
                (int) $appointment->practitioner_id,
                $context,
            );
            if ($point && ! empty($point['counter_uuid'])) {
                return $point;
            }

            // Refresh points for this context only, then retry the assigned desk.
            $this->provisioner->ensure($organization, $context, $branchId);
            $organization = $organization->fresh() ?? $organization;
            $point = $this->provisioner->pointForPractitioner(
                $organization,
                $branchId,
                (int) $appointment->practitioner_id,
                $context,
            );
            if ($point && ! empty($point['counter_uuid'])) {
                return $point;
            }
        }

        // Prefer a desk ticket over leaving waiters without a Queue number.
        return $this->provisioner->defaultPoint($organization, $context, $branchId);
    }

    /**
     * @param  array<string, mixed>  $resources
     * @return array<string, mixed>|null
     */
    protected function resolveCallNextPoint(
        Organization $organization,
        string $context,
        int $branchId,
        ?Member $member,
        ?int $practitionerId,
        array $resources,
    ): ?array {
        if ($context === CareQueueContexts::CONSULTATION || CareQueueContexts::isSpecialty($context)) {
            if ($practitionerId) {
                return $this->provisioner->pointForPractitioner($organization, $branchId, $practitionerId, $context)
                    ?? collect($resources['points'] ?? [])->first(
                        fn ($p) => ($p['kind'] ?? '') === 'practitioner' && (int) ($p['ref_id'] ?? 0) === $practitionerId
                    );
            }
            if ($member && app(CarePermissions::class)->usesPractitionerBranchScope($member)) {
                $prac = Practitioner::owned($organization->owner_ref)
                    ->where('organization_id', $organization->id)
                    ->where('branch_id', $branchId)
                    ->where(function ($q) use ($member) {
                        $q->where('member_id', $member->id)
                            ->orWhere('user_ref', $member->user_ref);
                    })
                    ->first();
                if ($prac) {
                    return $this->provisioner->pointForPractitioner($organization, $branchId, $prac->id, $context);
                }
            }
        }

        if ($member) {
            $byMember = $this->provisioner->pointForMember($organization, $context, $branchId, $member);
            if ($byMember) {
                return $byMember;
            }
        }

        return $this->provisioner->defaultPoint($organization, $context, $branchId);
    }

    /**
     * @param  array<string, mixed>  $metadata
     * @param  array<string, mixed>|null  $point
     * @return array<string, mixed>|null
     */
    protected function issue(
        Organization $organization,
        string $context,
        int $branchId,
        ?Patient $patient,
        array $metadata = [],
        string $priority = 'walk_in',
        string $source = 'api',
        ?array $point = null,
    ): ?array {
        $resources = $this->provisioner->ensure($organization, $context, $branchId);
        if (! $resources || empty($resources['queue_uuid'])) {
            Log::info('care.queue_issue_skipped_no_queue', [
                'context' => $context,
                'branch_id' => $branchId,
            ]);

            return null;
        }

        if (CareQueueContexts::usesAssignedRouting($context) && empty($point['counter_uuid'])) {
            Log::info('care.queue_issue_skipped_no_point', [
                'context' => $context,
                'branch_id' => $branchId,
            ]);

            return null;
        }

        $payload = [
            'service_queue_id' => $resources['queue_uuid'],
            'customer_name' => $patient?->fullName(),
            'customer_phone' => $patient?->phone,
            'priority' => $priority,
            'source' => $source,
            'metadata' => $metadata,
        ];
        if (! empty($point['counter_uuid'])) {
            $payload['assigned_counter_id'] = $point['counter_uuid'];
        }

        try {
            return $this->engine->issueTicket($organization, $payload);
        } catch (\Throwable $e) {
            Log::warning('care.queue_issue_failed', [
                'context' => $context,
                'branch_id' => $branchId,
                'message' => $e->getMessage(),
            ]);

            return null;
        }
    }

    /**
     * @param  array<string, mixed>  $ticket
     * @param  array<string, mixed>|null  $point
     */
    protected function applyTicket(Model $entity, array $ticket, ?array $point = null): void
    {
        $assigned = $ticket['assigned_counter'] ?? null;
        $entity->forceFill([
            'queue_ticket_uuid' => $ticket['uuid'] ?? $entity->getAttribute('queue_ticket_uuid'),
            'queue_ticket_number' => $ticket['ticket_number'] ?? $entity->getAttribute('queue_ticket_number'),
            'queue_ticket_status' => $ticket['status'] ?? $entity->getAttribute('queue_ticket_status'),
            'queue_service_point_uuid' => $ticket['assigned_counter']['uuid']
                ?? $ticket['counter']['uuid']
                ?? $point['counter_uuid']
                ?? $entity->getAttribute('queue_service_point_uuid'),
            'queue_destination' => $ticket['destination']
                ?? $point['destination']
                ?? $entity->getAttribute('queue_destination'),
            'queue_staff_display_name' => $ticket['staff_display_name']
                ?? $point['staff_display_name']
                ?? (is_array($assigned) ? ($assigned['staff_display_name'] ?? null) : null)
                ?? $entity->getAttribute('queue_staff_display_name'),
            'queue_routing_status' => CareQueueContexts::ROUTING_ROUTED,
        ])->save();
    }

    /**
     * @return array<string, mixed>|null
     */
    protected function ticketAction(Organization $organization, Model $entity, string $action): ?array
    {
        $uuid = (string) $entity->getAttribute('queue_ticket_uuid');
        if ($uuid === '' || ! $this->isEnabled($organization)) {
            return null;
        }

        try {
            $ticket = $this->engine->ticketAction($organization, $uuid, [
                'action' => $action,
            ]);
            $this->applyTicket($entity, $ticket);

            return $ticket;
        } catch (\Throwable $e) {
            Log::warning('care.queue_ticket_action_failed', [
                'action' => $action,
                'ticket_uuid' => $uuid,
                'message' => $e->getMessage(),
            ]);

            return null;
        }
    }

    protected function findEntityByTicket(Organization $organization, string $context, string $ticketUuid): ?Model
    {
        if ($ticketUuid === '') {
            return null;
        }

        $owner = (string) $organization->owner_ref;

        $entity = match (true) {
            $context === CareQueueContexts::PHARMACY => Prescription::owned($owner)
                ->where('organization_id', $organization->id)
                ->where('queue_ticket_uuid', $ticketUuid)
                ->first(),
            in_array($context, [CareQueueContexts::LABORATORY, CareQueueContexts::IMAGING], true) => InvestigationRequest::owned($owner)
                ->where('organization_id', $organization->id)
                ->where('queue_ticket_uuid', $ticketUuid)
                ->first(),
            $context === CareQueueContexts::BILLING => Bill::owned($owner)
                ->where('organization_id', $organization->id)
                ->where('queue_ticket_uuid', $ticketUuid)
                ->first(),
            default => Appointment::owned($owner)
                ->where('organization_id', $organization->id)
                ->where('queue_ticket_uuid', $ticketUuid)
                ->first(),
        };

        if ($entity) {
            return $entity;
        }

        // Recover when the Care row lost its uuid mirror but the ticket still points at it.
        $ticket = CareQueueTicket::query()
            ->where('organization_id', $organization->id)
            ->where('uuid', $ticketUuid)
            ->first();

        if (! $ticket || ! $ticket->care_entity_id) {
            return null;
        }

        return match ($ticket->care_entity) {
            'prescription' => Prescription::owned($owner)
                ->where('organization_id', $organization->id)
                ->whereKey($ticket->care_entity_id)
                ->first(),
            'investigation_request' => InvestigationRequest::owned($owner)
                ->where('organization_id', $organization->id)
                ->whereKey($ticket->care_entity_id)
                ->first(),
            'bill' => Bill::owned($owner)
                ->where('organization_id', $organization->id)
                ->whereKey($ticket->care_entity_id)
                ->first(),
            'appointment' => Appointment::owned($owner)
                ->where('organization_id', $organization->id)
                ->whereKey($ticket->care_entity_id)
                ->first(),
            default => null,
        };
    }

    protected function syncMissingAppointments(Organization $organization, string $context, int $branchId, int $limit): int
    {
        $owner = (string) $organization->owner_ref;
        $appointments = Appointment::owned($owner)
            ->where('organization_id', $organization->id)
            ->where('branch_id', $branchId)
            ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN])
            ->where(function ($q) {
                $q->whereNull('queue_ticket_uuid')->orWhere('queue_ticket_uuid', '');
            })
            ->with(['patient', 'organization', 'practitioner'])
            ->orderBy('queue_position')
            ->orderBy('waiting_at')
            ->orderBy('checked_in_at')
            ->orderBy('id')
            ->limit($limit)
            ->get();

        $issued = 0;
        foreach ($appointments as $appointment) {
            if ($this->contextForAppointment($organization, $appointment) !== $context) {
                continue;
            }

            $updated = $this->issueForAppointment($organization, $appointment->fresh(['patient', 'organization', 'practitioner']));
            if ($updated && filled($updated->queue_ticket_uuid)) {
                $issued++;
            }
        }

        return $issued;
    }

    protected function syncMissingPrescriptions(Organization $organization, int $branchId, int $limit): int
    {
        $owner = (string) $organization->owner_ref;
        $prescriptions = Prescription::owned($owner)
            ->where('organization_id', $organization->id)
            ->where('status', Prescription::STATUS_ACTIVE)
            ->where(function ($q) {
                $q->whereNull('queue_ticket_uuid')->orWhere('queue_ticket_uuid', '');
            })
            ->whereHas('visit', fn ($q) => $q->where('branch_id', $branchId))
            ->with(['patient', 'visit'])
            ->orderBy('created_at')
            ->orderBy('id')
            ->limit($limit)
            ->get();

        $issued = 0;
        foreach ($prescriptions as $prescription) {
            $updated = $this->issueForPrescription($organization, $prescription);
            if ($updated && filled($updated->queue_ticket_uuid)) {
                $issued++;
            }
        }

        return $issued;
    }

    protected function syncMissingInvestigations(Organization $organization, string $context, int $branchId, int $limit): int
    {
        $owner = (string) $organization->owner_ref;
        $requests = InvestigationRequest::owned($owner)
            ->where('organization_id', $organization->id)
            ->where('branch_id', $branchId)
            ->whereIn('status', [
                InvestigationRequest::STATUS_PENDING,
                InvestigationRequest::STATUS_SAMPLE_COLLECTED,
                InvestigationRequest::STATUS_IN_PROGRESS,
            ])
            ->where(function ($q) {
                $q->whereNull('queue_ticket_uuid')->orWhere('queue_ticket_uuid', '');
            })
            ->with(['patient', 'visit', 'investigationType'])
            ->orderBy('id')
            ->limit($limit)
            ->get();

        $issued = 0;
        foreach ($requests as $request) {
            if ($this->contextForInvestigation($request) !== $context) {
                continue;
            }

            $updated = $this->issueForInvestigation($organization, $request);
            if ($updated && filled($updated->queue_ticket_uuid)) {
                $issued++;
            }
        }

        return $issued;
    }

    protected function syncMissingBills(Organization $organization, int $branchId, int $limit): int
    {
        $owner = (string) $organization->owner_ref;
        $bills = Bill::owned($owner)
            ->where('organization_id', $organization->id)
            ->where('branch_id', $branchId)
            ->whereIn('status', [Bill::STATUS_OPEN, Bill::STATUS_PARTIAL, Bill::STATUS_DRAFT])
            ->where(function ($q) {
                $q->whereNull('queue_ticket_uuid')->orWhere('queue_ticket_uuid', '');
            })
            ->with('patient')
            ->orderBy('id')
            ->limit($limit)
            ->get();

        $issued = 0;
        foreach ($bills as $bill) {
            $updated = $this->issueForBill($organization, $bill);
            if ($updated && filled($updated->queue_ticket_uuid)) {
                $issued++;
            }
        }

        return $issued;
    }
}
