<?php

namespace App\Services\Care;

use App\Models\Branch;
use App\Models\CareServicePoint;
use App\Models\CareServiceQueue;
use App\Models\Member;
use App\Models\Organization;
use App\Models\Practitioner;
use Illuminate\Support\Facades\Log;

/**
 * Idempotently provisions Care Queue Engine service queues and service points
 * from Care staff / branch configuration.
 */
class CareQueueProvisioner
{
    public function __construct(
        protected PlanService $plans,
    ) {}

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

    /**
     * @return array<string, mixed>
     */
    public function provisionOrganization(Organization $organization): array
    {
        if (! $this->shouldProvision($organization)) {
            return (array) data_get($organization->settings, 'department_queue_provisioning', []);
        }

        $owner = (string) $organization->owner_ref;
        $branches = Branch::owned($owner)
            ->where('organization_id', $organization->id)
            ->where('is_active', true)
            ->orderBy('name')
            ->get();

        $provisioning = (array) data_get($organization->settings, 'department_queue_provisioning', []);

        foreach (CareQueueContexts::departments() as $context => $meta) {
            $queues = is_array($provisioning[$context]['queues'] ?? null)
                ? $provisioning[$context]['queues']
                : [];
            $byBranch = collect($queues)->keyBy(fn ($q) => (string) ($q['branch_id'] ?? ''));

            foreach ($branches as $branch) {
                $prior = $byBranch->get((string) $branch->id, []);
                $stub = $this->provisionBranchDepartment(
                    $organization,
                    $owner,
                    $context,
                    $meta,
                    $branch,
                    is_array($prior) ? $prior : [],
                );
                $byBranch->put((string) $branch->id, $stub);
            }

            $provisioning[$context] = [
                'queues' => $byBranch->values()->all(),
            ];
        }

        $settings = $organization->settings ?? [];
        $settings['department_queue_provisioning'] = $provisioning;
        $organization->update(['settings' => $settings]);

        return $provisioning;
    }

    /**
     * @param  array<string, mixed>  $meta
     * @param  array<string, mixed>  $prior
     * @return array<string, mixed>
     */
    protected function provisionBranchDepartment(
        Organization $organization,
        string $owner,
        string $context,
        array $meta,
        Branch $branch,
        array $prior,
    ): array {
        $stub = [
            'name' => $meta['name'],
            'prefix' => $meta['prefix'],
            'module' => $context,
            'branch_id' => $branch->id,
            'branch_name' => $branch->name,
            'active' => true,
            'routing_mode' => $meta['routing_mode'],
            'queue_external_key' => CareQueueContexts::queueExternalKey($context, $branch->id),
            'department_external_key' => CareQueueContexts::departmentExternalKey($context, $branch->id),
            'queue_uuid' => $prior['queue_uuid'] ?? null,
            'department_id' => $prior['department_id'] ?? null,
            'counter_uuid' => $prior['counter_uuid'] ?? null,
            'counter_external_key' => $prior['counter_external_key']
                ?? CareQueueContexts::counterExternalKey($context, $branch->id),
            'points' => is_array($prior['points'] ?? null) ? $prior['points'] : [],
            'synced' => false,
        ];

        try {
            $queue = CareServiceQueue::query()->updateOrCreate(
                [
                    'organization_id' => $organization->id,
                    'branch_id' => $branch->id,
                    'context' => $context,
                ],
                [
                    'owner_ref' => $owner,
                    'name' => $meta['name'],
                    'prefix' => $meta['prefix'],
                    'routing_mode' => $meta['routing_mode'],
                    'external_key' => $stub['queue_external_key'],
                    'is_active' => true,
                ],
            );

            $stub['queue_uuid'] = $queue->uuid;
            $stub['department_id'] = null;

            $points = $this->buildPointsForContext($organization, $context, $branch, $stub);
            $syncedPoints = [];
            $seenExternalKeys = [];

            foreach ($points as $point) {
                $externalKey = (string) ($point['external_key'] ?? '');
                if ($externalKey === '') {
                    continue;
                }
                $seenExternalKeys[] = $externalKey;

                $row = CareServicePoint::query()->updateOrCreate(
                    [
                        'service_queue_id' => $queue->id,
                        'external_key' => $externalKey,
                    ],
                    [
                        'owner_ref' => $owner,
                        'organization_id' => $organization->id,
                        'name' => (string) ($point['name'] ?? $point['destination'] ?? 'Desk'),
                        'destination' => $point['destination'] ?? null,
                        'kind' => (string) ($point['kind'] ?? 'default'),
                        'ref_id' => isset($point['ref_id']) ? (int) $point['ref_id'] : null,
                        'staff_ref' => $point['staff_ref'] ?? null,
                        'staff_display_name' => $point['staff_display_name'] ?? null,
                        'is_active' => true,
                    ],
                );

                $point['counter_uuid'] = $row->uuid;
                $point['synced'] = true;
                $syncedPoints[] = $point;
            }

            CareServicePoint::query()
                ->where('service_queue_id', $queue->id)
                ->when(
                    $seenExternalKeys !== [],
                    fn ($q) => $q->whereNotIn('external_key', $seenExternalKeys),
                    fn ($q) => $q->whereRaw('1 = 1'),
                )
                ->update(['is_active' => false]);

            $stub['points'] = $syncedPoints;
            $first = collect($syncedPoints)->first(fn ($p) => ! empty($p['counter_uuid']));
            if ($first) {
                $stub['counter_uuid'] = $first['counter_uuid'];
                $stub['counter_external_key'] = $first['external_key'];
            }
            $stub['synced'] = ! empty($stub['queue_uuid']) && collect($syncedPoints)->isNotEmpty();
        } catch (\Throwable $e) {
            Log::warning('care.native_queue_department_provision_failed', [
                'context' => $context,
                'branch_id' => $branch->id,
                'message' => $e->getMessage(),
            ]);
        }

        return $stub;
    }

    /**
     * @param  array<string, mixed>  $stub
     * @return list<array<string, mixed>>
     */
    protected function buildPointsForContext(
        Organization $organization,
        string $context,
        Branch $branch,
        array $stub,
    ): array {
        $priorByKey = collect($stub['points'] ?? [])->keyBy(fn ($p) => (string) ($p['external_key'] ?? ''));

        $points = match (true) {
            $context === CareQueueContexts::CONSULTATION => $this->practitionerPoints(
                $organization,
                $branch,
                CareQueueContexts::CONSULTATION,
                $priorByKey,
            ),
            CareQueueContexts::isSpecialty($context) => array_values(array_merge(
                $this->practitionerPoints(
                    $organization,
                    $branch,
                    $context,
                    $priorByKey,
                    data_get($organization->settings, "specialty_module_provisioning.{$context}.department_ids", []),
                ),
                $this->specialtyStagePoints($organization, $branch, $context, $priorByKey),
            )),
            $context === CareQueueContexts::TRIAGE => $this->memberRolePoints(
                $organization,
                $branch,
                ['nurse'],
                $context,
                'Nurse station',
                $priorByKey,
            ),
            $context === CareQueueContexts::PHARMACY => $this->memberRolePoints(
                $organization,
                $branch,
                ['pharmacist'],
                $context,
                'Pharmacy counter',
                $priorByKey,
            ),
            in_array($context, [CareQueueContexts::LABORATORY, CareQueueContexts::IMAGING], true) => $this->memberRolePoints(
                $organization,
                $branch,
                ['lab_technician'],
                $context,
                $context === CareQueueContexts::IMAGING ? 'Imaging room' : 'Lab desk',
                $priorByKey,
            ),
            $context === CareQueueContexts::BILLING => $this->memberRolePoints(
                $organization,
                $branch,
                ['cashier'],
                $context,
                'Cashier desk',
                $priorByKey,
            ),
            $context === CareQueueContexts::RECEPTION => $this->memberRolePoints(
                $organization,
                $branch,
                ['receptionist'],
                $context,
                'Reception',
                $priorByKey,
            ),
            default => [],
        };

        if ($points === []) {
            $key = CareQueueContexts::pointExternalKey($context, $branch->id, 'default', 0);
            $prior = $priorByKey->get($key, []);
            $points[] = [
                'kind' => 'default',
                'ref_id' => 0,
                'name' => ($stub['name'] ?? ucfirst($context)).' desk',
                'destination' => ($stub['name'] ?? ucfirst($context)).' desk',
                'staff_ref' => null,
                'staff_display_name' => null,
                'external_key' => $key,
                'counter_uuid' => $prior['counter_uuid'] ?? null,
                'synced' => false,
            ];
        }

        return $points;
    }

    /**
     * Fixed stage points for specialty modules that define distinct queue_point values
     * (e.g. dentistry waiting / chair / recovery).
     *
     * @param  \Illuminate\Support\Collection<string, array<string, mixed>>  $priorByKey
     * @return list<array<string, mixed>>
     */
    protected function specialtyStagePoints(
        Organization $organization,
        Branch $branch,
        string $context,
        $priorByKey,
    ): array {
        $stages = app(SpecialtyShellService::class)->stages($context);
        $hasQueuePoints = collect($stages)->contains(fn ($s) => is_string($s['queue_point'] ?? null) && $s['queue_point'] !== '');
        if (! $hasQueuePoints) {
            return [];
        }

        $seen = [];
        $points = [];
        foreach ($stages as $stage) {
            $queuePoint = $stage['queue_point'] ?? null;
            if (! is_string($queuePoint) || $queuePoint === '' || isset($seen[$queuePoint])) {
                continue;
            }
            $seen[$queuePoint] = true;
            $key = CareQueueContexts::pointExternalKey($context, $branch->id, 'stage', $queuePoint);
            $prior = $priorByKey->get($key, []);
            $label = (string) ($stage['label'] ?? ucfirst($queuePoint));
            $points[] = [
                'kind' => 'stage',
                'stage' => $queuePoint,
                'ref_id' => 0,
                'name' => $label,
                'destination' => $label,
                'staff_ref' => null,
                'staff_display_name' => null,
                'external_key' => $key,
                'counter_uuid' => $prior['counter_uuid'] ?? null,
                'synced' => false,
            ];
        }

        return $points;
    }

    /**
     * @param  list<int|string>|null  $departmentIds
     * @param  \Illuminate\Support\Collection<string, array<string, mixed>>  $priorByKey
     * @return list<array<string, mixed>>
     */
    protected function practitionerPoints(
        Organization $organization,
        Branch $branch,
        string $context,
        $priorByKey,
        ?array $departmentIds = null,
    ): array {
        $practitioners = Practitioner::owned($organization->owner_ref)
            ->where('organization_id', $organization->id)
            ->where('branch_id', $branch->id)
            ->where('is_active', true)
            ->when(
                is_array($departmentIds) && $departmentIds !== [],
                fn ($q) => $q->whereIn('department_id', array_map('intval', $departmentIds)),
            )
            ->orderBy('name')
            ->get();

        $points = [];
        $roomIndex = 1;
        $roomPrefix = CareQueueContexts::isSpecialty($context)
            ? 'Bay'
            : 'Consultation Room';

        foreach ($practitioners as $practitioner) {
            $key = CareQueueContexts::pointExternalKey(
                $context,
                $branch->id,
                'practitioner',
                $practitioner->id,
            );
            $prior = $priorByKey->get($key, []);
            $room = trim((string) ($practitioner->room ?? ''));
            if ($room === '') {
                $room = $roomPrefix.' '.$roomIndex;
            }
            $roomIndex++;

            $points[] = [
                'kind' => 'practitioner',
                'ref_id' => $practitioner->id,
                'name' => $practitioner->name,
                'destination' => $room,
                'staff_ref' => $practitioner->user_ref ?: ('practitioner:'.$practitioner->id),
                'staff_display_name' => $practitioner->name,
                'specialty' => $practitioner->specialty,
                'external_key' => $key,
                'counter_uuid' => $prior['counter_uuid'] ?? null,
                'synced' => false,
            ];
        }

        return $points;
    }

    /**
     * @param  list<string>  $roles
     * @param  \Illuminate\Support\Collection<string, array<string, mixed>>  $priorByKey
     * @return list<array<string, mixed>>
     */
    protected function memberRolePoints(
        Organization $organization,
        Branch $branch,
        array $roles,
        string $context,
        string $destinationPrefix,
        $priorByKey,
    ): array {
        $members = Member::owned($organization->owner_ref)
            ->where('organization_id', $organization->id)
            ->whereIn('role', $roles)
            ->where(function ($q) use ($branch) {
                $q->whereNull('branch_id')->orWhere('branch_id', $branch->id);
            })
            ->orderBy('id')
            ->get();

        $points = [];
        $index = 1;
        foreach ($members as $member) {
            $key = CareQueueContexts::pointExternalKey($context, $branch->id, 'member', $member->id);
            $prior = $priorByKey->get($key, []);
            $label = $destinationPrefix.' '.$index;
            $points[] = [
                'kind' => 'member',
                'ref_id' => $member->id,
                'name' => $label,
                'destination' => $label,
                'staff_ref' => $member->user_ref,
                'staff_display_name' => null,
                'external_key' => $key,
                'counter_uuid' => $prior['counter_uuid'] ?? null,
                'synced' => false,
            ];
            $index++;
        }

        return $points;
    }

    /**
     * @return array{queue_uuid: ?string, counter_uuid: ?string, queue_external_key: string, counter_external_key: string, synced: bool, points: list<array<string, mixed>>, routing_mode: string}|null
     */
    public function resourcesFor(
        Organization $organization,
        string $context,
        int $branchId,
    ): ?array {
        if (CareQueueContexts::isSpecialty($context)) {
            $queues = data_get($organization->settings, "specialty_module_provisioning.{$context}.queues", []);
        } else {
            $queues = data_get($organization->settings, "department_queue_provisioning.{$context}.queues", []);
        }

        if (! is_array($queues)) {
            return null;
        }

        $match = collect($queues)->first(
            fn ($q) => (int) ($q['branch_id'] ?? 0) === $branchId && ($q['active'] ?? true)
        );

        if (! $match) {
            return null;
        }

        $meta = CareQueueContexts::departments()[$context] ?? null;

        return [
            'queue_uuid' => $match['queue_uuid'] ?? null,
            'counter_uuid' => $match['counter_uuid'] ?? null,
            'queue_external_key' => (string) ($match['queue_external_key']
                ?? CareQueueContexts::queueExternalKey($context, $branchId)),
            'counter_external_key' => (string) ($match['counter_external_key']
                ?? CareQueueContexts::counterExternalKey($context, $branchId)),
            'synced' => (bool) ($match['synced'] ?? false),
            'points' => is_array($match['points'] ?? null) ? $match['points'] : [],
            'routing_mode' => (string) ($match['routing_mode'] ?? $meta['routing_mode'] ?? 'shared_pool'),
        ];
    }

    /**
     * Resolve a service point for a practitioner on a queue context.
     *
     * When a doctor is linked after queues were first provisioned, ensure() may
     * early-return with a stale points list — refresh provisioning on miss so
     * Call next can still reach board waiters assigned to that desk.
     *
     * @return array<string, mixed>|null
     */
    public function pointForPractitioner(
        Organization $organization,
        int $branchId,
        int $practitionerId,
        string $context = CareQueueContexts::CONSULTATION,
    ): ?array {
        $point = $this->findPractitionerPoint($organization, $context, $branchId, $practitionerId);
        if ($point && ! empty($point['counter_uuid'])) {
            return $point;
        }

        if (! $this->shouldProvision($organization)) {
            return $this->practitionerPointFromDb($organization, $context, $branchId, $practitionerId);
        }

        if (CareQueueContexts::isSpecialty($context)) {
            $this->provisionSpecialtyContextBranch($organization->fresh() ?? $organization, $context, $branchId);
        } elseif (CareQueueContexts::isDepartment($context)) {
            $this->provisionContextBranch($organization->fresh() ?? $organization, $context, $branchId);
        } else {
            return $this->practitionerPointFromDb($organization, $context, $branchId, $practitionerId);
        }

        $organization = $organization->fresh() ?? $organization;
        $point = $this->findPractitionerPoint($organization, $context, $branchId, $practitionerId);
        if ($point && ! empty($point['counter_uuid'])) {
            return $point;
        }

        return $this->practitionerPointFromDb($organization, $context, $branchId, $practitionerId);
    }

    /**
     * @return array<string, mixed>|null
     */
    protected function findPractitionerPoint(
        Organization $organization,
        string $context,
        int $branchId,
        int $practitionerId,
    ): ?array {
        $resources = $this->resourcesFor($organization, $context, $branchId);
        if (! $resources) {
            return null;
        }

        return collect($resources['points'] ?? [])->first(
            fn ($p) => ($p['kind'] ?? '') === 'practitioner' && (int) ($p['ref_id'] ?? 0) === $practitionerId
        );
    }

    /**
     * @return array<string, mixed>|null
     */
    protected function practitionerPointFromDb(
        Organization $organization,
        string $context,
        int $branchId,
        int $practitionerId,
    ): ?array {
        $row = CareServicePoint::query()
            ->where('organization_id', $organization->id)
            ->where('kind', 'practitioner')
            ->where('ref_id', $practitionerId)
            ->where('is_active', true)
            ->whereHas(
                'serviceQueue',
                fn ($q) => $q
                    ->where('organization_id', $organization->id)
                    ->where('context', $context)
                    ->where('branch_id', $branchId)
                    ->where('is_active', true),
            )
            ->first();

        if (! $row) {
            return null;
        }

        return [
            'kind' => 'practitioner',
            'ref_id' => $practitionerId,
            'name' => $row->name,
            'destination' => $row->destination,
            'staff_ref' => $row->staff_ref,
            'staff_display_name' => $row->staff_display_name,
            'external_key' => $row->external_key,
            'counter_uuid' => $row->uuid,
            'synced' => true,
        ];
    }

    /**
     * @return array<string, mixed>|null
     */
    public function pointForMember(Organization $organization, string $context, int $branchId, Member $member): ?array
    {
        $resources = $this->resourcesFor($organization, $context, $branchId);
        if (! $resources) {
            return null;
        }

        $byMember = collect($resources['points'])->first(
            fn ($p) => ($p['kind'] ?? '') === 'member' && (int) ($p['ref_id'] ?? 0) === $member->id
        );
        if ($byMember) {
            return $byMember;
        }

        return collect($resources['points'])->first(
            fn ($p) => ($p['staff_ref'] ?? null) === $member->user_ref
        );
    }

    /**
     * Deterministic least-load / first active point when Care has no explicit assignment.
     *
     * @return array<string, mixed>|null
     */
    public function defaultPoint(Organization $organization, string $context, int $branchId): ?array
    {
        $resources = $this->resourcesFor($organization, $context, $branchId);
        if (! $resources) {
            return null;
        }

        $points = collect($resources['points'])->filter(fn ($p) => ! empty($p['counter_uuid']) && ($p['synced'] ?? true));
        if ($points->isEmpty()) {
            if (! empty($resources['counter_uuid'])) {
                return [
                    'kind' => 'legacy',
                    'counter_uuid' => $resources['counter_uuid'],
                    'external_key' => $resources['counter_external_key'],
                    'destination' => null,
                    'staff_display_name' => null,
                ];
            }

            return null;
        }

        return $points->sortBy('ref_id')->first();
    }

    /**
     * Ensure Queue resources exist for one context + branch only.
     * Never fans out across every department × branch (that hung demo queues).
     *
     * @return array{queue_uuid: ?string, counter_uuid: ?string, queue_external_key: string, counter_external_key: string, synced: bool, points: list<array<string, mixed>>, routing_mode: string}|null
     */
    public function ensure(
        Organization $organization,
        string $context,
        int $branchId,
    ): ?array {
        $existing = $this->resourcesFor($organization, $context, $branchId);
        $expectedMode = CareQueueContexts::departments()[$context]['routing_mode']
            ?? (CareQueueContexts::isSpecialty($context) ? 'assigned_only' : null);
        $modeMatches = ! $expectedMode
            || ($existing['routing_mode'] ?? null) === $expectedMode;

        if ($existing && ! empty($existing['queue_uuid']) && (
            ! empty($existing['points']) || ! empty($existing['counter_uuid'])
        ) && $modeMatches) {
            if (! CareQueueContexts::isSpecialty($context) || ! empty($existing['points'])) {
                return $existing;
            }
        }

        if (! $this->shouldProvision($organization)) {
            return $existing;
        }

        if (CareQueueContexts::isSpecialty($context)) {
            return $this->provisionSpecialtyContextBranch($organization->fresh(), $context, $branchId)
                ?? $existing;
        }

        if (! CareQueueContexts::isDepartment($context)) {
            return null;
        }

        $this->provisionContextBranch($organization->fresh(), $context, $branchId);

        return $this->resourcesFor($organization->fresh(), $context, $branchId);
    }

    /**
     * Provision a single specialty queue for one branch (idempotent).
     *
     * @return array<string, mixed>|null
     */
    public function provisionSpecialtyContextBranch(Organization $organization, string $context, int $branchId): ?array
    {
        if (! CareQueueContexts::isSpecialty($context) || ! $this->shouldProvision($organization)) {
            return $this->resourcesFor($organization, $context, $branchId);
        }

        $definition = config("care.specialty_modules.{$context}");
        if (! is_array($definition)) {
            return null;
        }

        $branch = Branch::owned((string) $organization->owner_ref)
            ->where('organization_id', $organization->id)
            ->whereKey($branchId)
            ->first();
        if (! $branch) {
            return null;
        }

        $meta = [
            'name' => (string) ($definition['queue_name'] ?? $definition['label'] ?? $context),
            'prefix' => (string) ($definition['queue_prefix'] ?? strtoupper(substr($context, 0, 3))),
            'routing_mode' => 'assigned_only',
            'type' => 'consultation',
        ];

        $provisioning = (array) data_get($organization->settings, 'specialty_module_provisioning', []);
        $record = is_array($provisioning[$context] ?? null) ? $provisioning[$context] : [];
        $queues = is_array($record['queues'] ?? null) ? $record['queues'] : [];
        $byBranch = collect($queues)->keyBy(fn ($q) => (string) ($q['branch_id'] ?? ''));
        $prior = $byBranch->get((string) $branch->id, []);
        if (! is_array($prior)) {
            $prior = [];
        }
        $prior['module'] = $context;

        $stub = $this->provisionBranchDepartment(
            $organization,
            (string) $organization->owner_ref,
            $context,
            $meta,
            $branch,
            $prior,
        );
        $byBranch->put((string) $branch->id, $stub);

        $provisioning[$context] = array_merge($record, [
            'active' => true,
            'queues' => $byBranch->values()->all(),
        ]);

        $settings = $organization->settings ?? [];
        $settings['specialty_module_provisioning'] = $provisioning;
        $organization->update(['settings' => $settings]);

        return $stub;
    }

    /**
     * Provision a single department queue for one branch (idempotent).
     */
    public function provisionContextBranch(Organization $organization, string $context, int $branchId): ?array
    {
        if (! $this->shouldProvision($organization)) {
            return $this->resourcesFor($organization, $context, $branchId);
        }

        $meta = CareQueueContexts::departments()[$context] ?? null;
        if ($meta === null) {
            return null;
        }

        $branch = Branch::owned((string) $organization->owner_ref)
            ->where('organization_id', $organization->id)
            ->whereKey($branchId)
            ->first();
        if (! $branch) {
            return null;
        }

        $provisioning = (array) data_get($organization->settings, 'department_queue_provisioning', []);
        $queues = is_array($provisioning[$context]['queues'] ?? null)
            ? $provisioning[$context]['queues']
            : [];
        $byBranch = collect($queues)->keyBy(fn ($q) => (string) ($q['branch_id'] ?? ''));
        $prior = $byBranch->get((string) $branch->id, []);

        $stub = $this->provisionBranchDepartment(
            $organization,
            (string) $organization->owner_ref,
            $context,
            $meta,
            $branch,
            is_array($prior) ? $prior : [],
        );
        $byBranch->put((string) $branch->id, $stub);

        $provisioning[$context] = [
            'queues' => $byBranch->values()->all(),
        ];

        $settings = $organization->settings ?? [];
        $settings['department_queue_provisioning'] = $provisioning;
        $organization->update(['settings' => $settings]);

        return $stub;
    }
}
