<?php

namespace App\Services\Care;

use App\Models\CareQueueTicket;
use App\Models\CareServicePoint;
use App\Models\CareServiceQueue;
use App\Models\Organization;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use InvalidArgumentException;
use Throwable;

/**
 * In-app Care Queue Engine — issue / call-next / serve / complete / recall.
 * Returns ticket arrays shaped like the former Ladill Queue API for CareQueueBridge.
 */
class CareQueueEngine
{
    /**
     * @param  array{
     *     service_queue_id: string,
     *     customer_name?: ?string,
     *     customer_phone?: ?string,
     *     priority?: string,
     *     source?: string,
     *     metadata?: array<string, mixed>,
     *     assigned_counter_id?: ?string
     * }  $payload
     * @return array<string, mixed>
     */
    public function issueTicket(Organization $organization, array $payload): array
    {
        $queueUuid = (string) ($payload['service_queue_id'] ?? '');
        if ($queueUuid === '') {
            throw new InvalidArgumentException('service_queue_id is required.');
        }

        return DB::transaction(function () use ($organization, $payload, $queueUuid) {
            /** @var CareServiceQueue|null $queue */
            $queue = CareServiceQueue::query()
                ->where('organization_id', $organization->id)
                ->where('uuid', $queueUuid)
                ->lockForUpdate()
                ->first();

            if (! $queue || ! $queue->is_active) {
                throw new InvalidArgumentException('Service queue not found.');
            }

            $point = null;
            $counterUuid = (string) ($payload['assigned_counter_id'] ?? '');
            if ($counterUuid !== '') {
                $point = CareServicePoint::query()
                    ->where('organization_id', $organization->id)
                    ->where('service_queue_id', $queue->id)
                    ->where('uuid', $counterUuid)
                    ->where('is_active', true)
                    ->first();
                if (! $point) {
                    throw new InvalidArgumentException('Service point not found.');
                }
            }

            $seq = (int) $queue->next_number;
            $queue->forceFill(['next_number' => $seq + 1])->save();
            $ticketNumber = $queue->prefix.str_pad((string) $seq, 3, '0', STR_PAD_LEFT);

            $metadata = is_array($payload['metadata'] ?? null) ? $payload['metadata'] : [];

            $ticket = CareQueueTicket::create([
                'owner_ref' => $organization->owner_ref,
                'organization_id' => $organization->id,
                'service_queue_id' => $queue->id,
                'service_point_id' => $point?->id,
                'ticket_number' => $ticketNumber,
                'status' => CareQueueTicket::STATUS_WAITING,
                'priority' => (string) ($payload['priority'] ?? 'walk_in'),
                'source' => (string) ($payload['source'] ?? 'api'),
                'customer_name' => $payload['customer_name'] ?? null,
                'customer_phone' => $payload['customer_phone'] ?? null,
                'care_entity' => $metadata['care_entity'] ?? null,
                'care_entity_uuid' => $metadata['care_entity_uuid'] ?? null,
                'care_entity_id' => isset($metadata['care_entity_id']) ? (int) $metadata['care_entity_id'] : null,
                'visit_id' => isset($metadata['visit_id']) ? (int) $metadata['visit_id'] : null,
                'metadata' => $metadata,
            ]);

            $ticket->setRelation('servicePoint', $point);
            $ticket->setRelation('serviceQueue', $queue);

            return $this->toApiArray($ticket);
        });
    }

    /**
     * @return array<string, mixed>|null
     */
    public function callNext(Organization $organization, string $queueUuid, string $counterUuid): ?array
    {
        return DB::transaction(function () use ($organization, $queueUuid, $counterUuid) {
            /** @var CareServiceQueue|null $queue */
            $queue = CareServiceQueue::query()
                ->where('organization_id', $organization->id)
                ->where('uuid', $queueUuid)
                ->lockForUpdate()
                ->first();

            if (! $queue || ! $queue->is_active) {
                return null;
            }

            /** @var CareServicePoint|null $point */
            $point = CareServicePoint::query()
                ->where('organization_id', $organization->id)
                ->where('service_queue_id', $queue->id)
                ->where('uuid', $counterUuid)
                ->where('is_active', true)
                ->first();

            if (! $point) {
                return null;
            }

            $waiting = CareQueueTicket::query()
                ->where('service_queue_id', $queue->id)
                ->where('status', CareQueueTicket::STATUS_WAITING)
                ->when(
                    $queue->routing_mode === 'assigned_only',
                    // Claim desk tickets and unassigned waiters (null point) — never other desks.
                    fn ($q) => $q->where(function ($inner) use ($point) {
                        $inner->where('service_point_id', $point->id)
                            ->orWhereNull('service_point_id');
                    }),
                    fn ($q) => $q->where(function ($inner) use ($point) {
                        $inner->whereNull('service_point_id')
                            ->orWhere('service_point_id', $point->id);
                    }),
                )
                ->orderByRaw("CASE priority WHEN 'appointment' THEN 0 WHEN 'walk_in' THEN 1 ELSE 2 END")
                ->orderBy('id')
                ->lockForUpdate()
                ->first();

            if (! $waiting) {
                return null;
            }

            return $this->markTicketCalled($waiting, $point, $queue);
        });
    }

    /**
     * Call a specific waiting ticket to a service point (Care board → ticket handoff).
     * Reassigns the ticket when it was waiting on another desk or unassigned.
     *
     * @return array<string, mixed>|null
     */
    public function callWaitingTicket(Organization $organization, string $ticketUuid, string $counterUuid): ?array
    {
        return DB::transaction(function () use ($organization, $ticketUuid, $counterUuid) {
            /** @var CareQueueTicket|null $ticket */
            $ticket = CareQueueTicket::query()
                ->where('organization_id', $organization->id)
                ->where('uuid', $ticketUuid)
                ->lockForUpdate()
                ->first();

            if (! $ticket || $ticket->status !== CareQueueTicket::STATUS_WAITING) {
                return null;
            }

            /** @var CareServiceQueue|null $queue */
            $queue = CareServiceQueue::query()
                ->where('organization_id', $organization->id)
                ->whereKey($ticket->service_queue_id)
                ->lockForUpdate()
                ->first();

            if (! $queue || ! $queue->is_active) {
                return null;
            }

            /** @var CareServicePoint|null $point */
            $point = CareServicePoint::query()
                ->where('organization_id', $organization->id)
                ->where('service_queue_id', $queue->id)
                ->where('uuid', $counterUuid)
                ->where('is_active', true)
                ->first();

            if (! $point) {
                return null;
            }

            return $this->markTicketCalled($ticket, $point, $queue);
        });
    }

    /**
     * @return array<string, mixed>
     */
    protected function markTicketCalled(CareQueueTicket $ticket, CareServicePoint $point, CareServiceQueue $queue): array
    {
        $ticket->forceFill([
            'service_point_id' => $point->id,
            'status' => CareQueueTicket::STATUS_CALLED,
            'called_at' => now(),
        ])->save();

        $ticket->setRelation('servicePoint', $point);
        $ticket->setRelation('serviceQueue', $queue);

        try {
            app(CareVoiceAnnouncementService::class)->announceCalled($ticket, $point);
        } catch (Throwable $e) {
            Log::warning('Care voice announcement failed on call-next', [
                'ticket_id' => $ticket->id,
                'error' => $e->getMessage(),
            ]);
        }

        return $this->toApiArray($ticket);
    }

    /**
     * @param  array{action: string}  $payload
     * @return array<string, mixed>
     */
    public function ticketAction(Organization $organization, string $ticketUuid, array $payload): array
    {
        $action = (string) ($payload['action'] ?? '');

        return DB::transaction(function () use ($organization, $ticketUuid, $action) {
            /** @var CareQueueTicket|null $ticket */
            $ticket = CareQueueTicket::query()
                ->where('organization_id', $organization->id)
                ->where('uuid', $ticketUuid)
                ->lockForUpdate()
                ->first();

            if (! $ticket) {
                throw new InvalidArgumentException('Ticket not found.');
            }

            $ticket->loadMissing('servicePoint', 'serviceQueue');

            match ($action) {
                'start' => $this->markServing($ticket),
                'complete' => $this->markCompleted($ticket),
                'recall' => $this->markCalled($ticket),
                'skip' => $this->markSkipped($ticket),
                'cancel' => $this->markCancelled($ticket),
                default => throw new InvalidArgumentException("Unknown ticket action [{$action}]."),
            };

            return $this->toApiArray($ticket->fresh(['servicePoint', 'serviceQueue']));
        });
    }

    /**
     * @return array<string, mixed>
     */
    public function toApiArray(CareQueueTicket $ticket): array
    {
        $point = $ticket->servicePoint;

        return [
            'uuid' => $ticket->uuid,
            'ticket_number' => $ticket->ticket_number,
            'status' => $ticket->status,
            'priority' => $ticket->priority,
            'source' => $ticket->source,
            'customer_name' => $ticket->customer_name,
            'customer_phone' => $ticket->customer_phone,
            'metadata' => $ticket->metadata ?? [],
            'assigned_counter' => $point ? [
                'uuid' => $point->uuid,
                'name' => $point->name,
                'staff_display_name' => $point->staff_display_name,
                'destination' => $point->destination,
            ] : null,
            'counter' => $point ? [
                'uuid' => $point->uuid,
                'name' => $point->name,
            ] : null,
            'destination' => $point?->destination,
            'staff_display_name' => $point?->staff_display_name,
            'called_at' => $ticket->called_at?->toIso8601String(),
            'serving_at' => $ticket->serving_at?->toIso8601String(),
            'completed_at' => $ticket->completed_at?->toIso8601String(),
        ];
    }

    protected function markServing(CareQueueTicket $ticket): void
    {
        $ticket->forceFill([
            'status' => CareQueueTicket::STATUS_SERVING,
            'serving_at' => $ticket->serving_at ?? now(),
            'called_at' => $ticket->called_at ?? now(),
        ])->save();
    }

    protected function markCompleted(CareQueueTicket $ticket): void
    {
        $ticket->forceFill([
            'status' => CareQueueTicket::STATUS_COMPLETED,
            'completed_at' => now(),
            'serving_at' => $ticket->serving_at ?? now(),
        ])->save();
    }

    protected function markCalled(CareQueueTicket $ticket): void
    {
        $ticket->forceFill([
            'status' => CareQueueTicket::STATUS_CALLED,
            'called_at' => now(),
        ])->save();

        $point = $ticket->servicePoint;
        if (! $point && $ticket->service_point_id) {
            $ticket->loadMissing('servicePoint');
            $point = $ticket->servicePoint;
        }

        if ($point) {
            try {
                app(CareVoiceAnnouncementService::class)->announceCalled($ticket, $point);
            } catch (Throwable $e) {
                Log::warning('Care voice announcement failed on recall', [
                    'ticket_id' => $ticket->id,
                    'error' => $e->getMessage(),
                ]);
            }
        }
    }

    protected function markSkipped(CareQueueTicket $ticket): void
    {
        $ticket->forceFill([
            'status' => CareQueueTicket::STATUS_SKIPPED,
            'completed_at' => now(),
        ])->save();
    }

    protected function markCancelled(CareQueueTicket $ticket): void
    {
        $ticket->forceFill([
            'status' => CareQueueTicket::STATUS_CANCELLED,
            'completed_at' => now(),
        ])->save();
    }
}
