<?php

namespace App\Services\Care;

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

/**
 * Activate/deactivate specialty practice modules (dentistry, eye care, …).
 *
 * Persistence: organization.settings.specialty_modules[key] = bool
 * Provisioning: departments per branch + Care Queue Engine stubs/points.
 * Deactivate hides UI and marks queues inactive; does not destroy clinical history.
 *
 * Plan gate: Pro/Enterprise via PlanService feature specialty_modules (same tier as queue_integration).
 */
class SpecialtyModuleService
{
    /** @var array<string, \Illuminate\Support\Collection<int, Practitioner>> */
    protected array $practitionersCache = [];

    /** @var array<string, bool> */
    protected array $deskSpecialistCache = [];

    /** @var array<string, list<array{key: string, definition: array<string, mixed>, access_level: string}>> */
    protected array $enabledModulesForMemberCache = [];

    /** @var array<string, string> */
    protected array $memberAccessLevelCache = [];

    public function __construct(
        protected PlanService $plans,
    ) {}

    /**
     * @return array<string, array<string, mixed>>
     */
    public function catalog(): array
    {
        return config('care.specialty_modules', []);
    }

    /**
     * Legacy maternity module key → womens_health.
     */
    public function normalizeKey(string $key): string
    {
        return $key === 'maternity' ? 'womens_health' : $key;
    }

    public function definition(string $key): ?array
    {
        $key = $this->normalizeKey($key);
        $catalog = $this->catalog();

        return $catalog[$key] ?? null;
    }

    /**
     * Migrate org settings from maternity → womens_health (idempotent).
     */
    public function migrateMaternityToWomensHealth(Organization $organization): Organization
    {
        $settings = is_array($organization->settings) ? $organization->settings : [];
        $modules = is_array($settings['specialty_modules'] ?? null) ? $settings['specialty_modules'] : [];
        $provisioning = is_array($settings['specialty_module_provisioning'] ?? null)
            ? $settings['specialty_module_provisioning']
            : [];

        $changed = false;
        if (! empty($modules['maternity']) && empty($modules['womens_health'])) {
            $modules['womens_health'] = true;
            $modules['maternity'] = false;
            $changed = true;
        }
        if (isset($provisioning['maternity']) && ! isset($provisioning['womens_health'])) {
            $provisioning['womens_health'] = $provisioning['maternity'];
            unset($provisioning['maternity']);
            $changed = true;
        } elseif (isset($provisioning['maternity']) && isset($provisioning['womens_health'])) {
            unset($provisioning['maternity']);
            $changed = true;
        }

        if (! $changed) {
            return $organization;
        }

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

        // Point legacy maternity departments at the new type when still labeled maternity.
        Department::query()
            ->where('organization_id', $organization->id)
            ->where('type', 'maternity')
            ->update(['type' => 'womens_health']);

        return $organization->fresh();
    }

    /**
     * @return array<string, array{label: string, default_on: bool}>
     */
    public function serviceLines(string $moduleKey): array
    {
        $definition = $this->definition($moduleKey);
        $lines = $definition['service_lines'] ?? [];
        if (! is_array($lines)) {
            return [];
        }

        $out = [];
        foreach ($lines as $key => $line) {
            if (! is_string($key) || ! is_array($line)) {
                continue;
            }
            $out[$key] = [
                'label' => (string) ($line['label'] ?? $key),
                'default_on' => (bool) ($line['default_on'] ?? true),
            ];
        }

        return $out;
    }

    /**
     * Enabled service lines for an org module (defaults when unset).
     *
     * @return array<string, bool>
     */
    public function enabledServiceLines(Organization $organization, string $moduleKey): array
    {
        $moduleKey = $this->normalizeKey($moduleKey);
        $catalog = $this->serviceLines($moduleKey);
        if ($catalog === []) {
            return [];
        }

        $stored = data_get(
            $organization->settings,
            "specialty_module_provisioning.{$moduleKey}.service_lines",
            [],
        );
        $stored = is_array($stored) ? $stored : [];

        $out = [];
        foreach ($catalog as $key => $line) {
            $out[$key] = array_key_exists($key, $stored)
                ? (bool) $stored[$key]
                : $line['default_on'];
        }

        return $out;
    }

    /**
     * @param  array<string, bool>  $desired
     */
    public function syncServiceLines(Organization $organization, string $moduleKey, array $desired): void
    {
        $moduleKey = $this->normalizeKey($moduleKey);
        $catalog = $this->serviceLines($moduleKey);
        abort_unless($catalog !== [], 404);

        $settings = is_array($organization->settings) ? $organization->settings : [];
        $provisioning = is_array($settings['specialty_module_provisioning'] ?? null)
            ? $settings['specialty_module_provisioning']
            : [];
        $record = is_array($provisioning[$moduleKey] ?? null) ? $provisioning[$moduleKey] : [];

        $lines = [];
        foreach (array_keys($catalog) as $key) {
            $lines[$key] = (bool) ($desired[$key] ?? false);
        }
        // Keep at least one line on so the module remains usable.
        if (! in_array(true, $lines, true)) {
            $first = array_key_first($catalog);
            if ($first !== null) {
                $lines[$first] = true;
            }
        }

        $record['service_lines'] = $lines;
        $provisioning[$moduleKey] = $record;
        $settings['specialty_module_provisioning'] = $provisioning;
        $organization->update(['settings' => $settings]);
    }

    public function isServiceLineEnabled(Organization $organization, string $moduleKey, string $lineKey): bool
    {
        $enabled = $this->enabledServiceLines($organization, $moduleKey);

        return (bool) ($enabled[$lineKey] ?? false);
    }

    /**
     * Heroicon-style icon identifier from the specialty catalog (e.g. bolt, heart).
     */
    public function iconName(string $key): string
    {
        $name = (string) ($this->definition($key)['icon'] ?? 'squares-2x2');

        return $name !== '' ? $name : 'squares-2x2';
    }

    /**
     * Inline SVG path markup for sidebar / card icons (24×24 outline).
     */
    public function iconSvgPath(string $key): string
    {
        $icons = config('care.specialty_icons', []);
        $name = $this->iconName($key);

        if (is_array($icons) && isset($icons[$name]) && is_string($icons[$name]) && $icons[$name] !== '') {
            return $icons[$name];
        }

        return is_array($icons) && isset($icons['squares-2x2']) && is_string($icons['squares-2x2'])
            ? $icons['squares-2x2']
            : '';
    }

    public function isDefaultOnPaidPlans(string $key): bool
    {
        return (bool) ($this->definition($key)['default_on_paid_plans'] ?? false);
    }

    public function isEnabled(Organization $organization, string $key): bool
    {
        $key = $this->normalizeKey($key);

        if (! $this->definition($key)) {
            return false;
        }

        // One-time maternity → womens_health settings migration.
        if ($key === 'womens_health'
            && data_get($organization->settings, 'specialty_modules.maternity')) {
            $organization = $this->migrateMaternityToWomensHealth($organization);
        }

        if ($this->isDefaultOnPaidPlans($key) && $this->plans->hasPaidPlan($organization)) {
            return true;
        }

        return (bool) data_get($organization->settings, "specialty_modules.{$key}", false);
    }

    /**
     * @return list<string>
     */
    public function defaultKeysForPaidPlans(): array
    {
        $keys = [];
        foreach (array_keys($this->catalog()) as $key) {
            if ($this->isDefaultOnPaidPlans($key)) {
                $keys[] = $key;
            }
        }

        return $keys;
    }

    /**
     * Provision Emergency / Blood Bank (and any other default_on_paid_plans modules).
     */
    public function ensureDefaultModulesProvisioned(Organization $organization, string $ownerRef): void
    {
        if (! $this->plans->hasPaidPlan($organization)) {
            return;
        }

        foreach ($this->defaultKeysForPaidPlans() as $key) {
            $provisioned = data_get(
                $organization->settings,
                "specialty_module_provisioning.{$key}.active",
            );
            if ($provisioned) {
                continue;
            }

            try {
                $this->activate($organization->fresh(), $ownerRef, $key);
                $organization->refresh();
            } catch (\Throwable $e) {
                Log::warning('specialty_module.default_provision_failed', [
                    'key' => $key,
                    'message' => $e->getMessage(),
                ]);
            }
        }
    }

    /**
     * @return list<string>
     */
    public function enabledKeys(Organization $organization): array
    {
        $enabled = [];
        foreach (array_keys($this->catalog()) as $key) {
            if ($this->isEnabled($organization, $key)) {
                $enabled[] = $key;
            }
        }

        return $enabled;
    }

    /**
     * @return list<array{key: string, definition: array<string, mixed>}>
     */
    public function enabledModules(Organization $organization): array
    {
        $out = [];
        foreach ($this->enabledKeys($organization) as $key) {
            $definition = $this->definition($key);
            if ($definition) {
                $out[] = ['key' => $key, 'definition' => $definition];
            }
        }

        return $out;
    }

    /**
     * Enabled modules the member may open (manage, view, or refer).
     *
     * @return list<array{key: string, definition: array<string, mixed>, access_level: string}>
     */
    public function enabledModulesForMember(Organization $organization, ?Member $member): array
    {
        $cacheKey = $organization->id.'|'.($member?->id ?? 'guest');
        if (isset($this->enabledModulesForMemberCache[$cacheKey])) {
            return $this->enabledModulesForMemberCache[$cacheKey];
        }

        $out = [];
        foreach ($this->enabledModules($organization) as $item) {
            $level = $this->memberAccessLevel($organization, $member, $item['key']);
            if ($level === 'none') {
                continue;
            }
            $out[] = array_merge($item, ['access_level' => $level]);
        }

        return $this->enabledModulesForMemberCache[$cacheKey] = $out;
    }

    /**
     * Whether the sidebar should show the Specialty nav group.
     *
     * Single-app specialists (dentist, radiologist, …) and legacy desk
     * specialists only receive their own module(s), so a Specialty group is
     * redundant — they use dashboard cards / direct routes instead.
     * Multi-app clinical roles keep the group.
     */
    public function shouldShowSpecialtyNav(Organization $organization, ?Member $member): bool
    {
        if (! $member) {
            return false;
        }

        $modules = $this->enabledModulesForMember($organization, $member);
        if ($modules === []) {
            return false;
        }

        $permissions = app(CarePermissions::class);
        if ($permissions->isSingleAppSpecialist($member)) {
            return false;
        }

        if ($this->isDeskSpecialist($organization, $member)) {
            return false;
        }

        return true;
    }

    /**
     * @return 'general'|'limited'|'restricted'
     */
    public function accessTier(string $key): string
    {
        $tier = (string) ($this->definition($key)['access'] ?? 'general');

        return in_array($tier, ['general', 'limited', 'restricted'], true) ? $tier : 'general';
    }

    /**
     * Highest access for this member on the module.
     *
     * @return 'none'|'view'|'refer'|'manage'
     */
    public function memberAccessLevel(Organization $organization, ?Member $member, string $key): string
    {
        $key = $this->normalizeKey($key);
        $cacheKey = $organization->id.'|'.($member?->id ?? 'guest').'|'.$key;
        if (isset($this->memberAccessLevelCache[$cacheKey])) {
            return $this->memberAccessLevelCache[$cacheKey];
        }

        if (! $this->isEnabled($organization, $key)) {
            return $this->memberAccessLevelCache[$cacheKey] = 'none';
        }

        $definition = $this->definition($key);
        if (! $definition || ! $member) {
            return $this->memberAccessLevelCache[$cacheKey] = 'none';
        }

        if (app(CarePermissions::class)->isAdmin($member)) {
            return $this->memberAccessLevelCache[$cacheKey] = 'manage';
        }

        if ($this->memberCanManage($organization, $member, $key)) {
            return $this->memberAccessLevelCache[$cacheKey] = 'manage';
        }

        // Use gated helpers so desk specialists do not inherit GP view/refer on other modules.
        if ($this->memberCanRefer($organization, $member, $key)) {
            return $this->memberAccessLevelCache[$cacheKey] = 'refer';
        }
        if ($this->memberCanView($organization, $member, $key)) {
            return $this->memberAccessLevelCache[$cacheKey] = 'view';
        }

        return $this->memberAccessLevelCache[$cacheKey] = 'none';
    }

    /**
     * Whether the member may see this specialty module in nav / open workspace (any level).
     */
    public function memberCanAccess(Organization $organization, ?Member $member, string $key): bool
    {
        return $this->memberAccessLevel($organization, $member, $key) !== 'none';
    }

    /**
     * Full clinical / queue edit access for the module.
     *
     * Role primary apps (CarePermissions) are the source of truth for the
     * RBAC matrix. Legacy `doctor` desk specialists still use keyword matching.
     */
    public function memberCanManage(Organization $organization, ?Member $member, string $key): bool
    {
        $key = $this->normalizeKey($key);
        if (! $this->isEnabled($organization, $key) || ! $member) {
            return false;
        }

        $permissions = app(CarePermissions::class);
        if ($permissions->isAdmin($member)) {
            return true;
        }

        $definition = $this->definition($key);
        if (! $definition) {
            return false;
        }

        // Department managers get analytics view, not clinical manage.
        if ((string) $member->role === 'department_manager') {
            return false;
        }

        $primaryApps = $permissions->primaryAppsFor($member);

        // Matrix-driven roles (including GP / EP / specialists with app lists).
        if (is_array($primaryApps)) {
            // Legacy doctor + specialty desk: only matching modules.
            if ((string) $member->role === 'doctor'
                && $this->isDeskSpecialist($organization, $member)) {
                return $this->specialistBelongsToModule($organization, $member, $key);
            }

            return in_array($key, $primaryApps, true);
        }

        // Desk specialists only manage modules that match their specialty / assignment.
        if ($this->isDeskSpecialist($organization, $member)
            && ! $this->specialistBelongsToModule($organization, $member, $key)) {
            return false;
        }

        $tier = $this->accessTier($key);
        $role = (string) $member->role;
        $manageRoles = $definition['roles'] ?? [];
        $supportRoles = $definition['support_roles'] ?? [];

        if ($tier === 'restricted') {
            if ($this->roleListed($member, $supportRoles)) {
                return true;
            }
            if (in_array($role, ['doctor', 'general_physician'], true) && $this->roleListed($member, $manageRoles)) {
                return $this->specialistBelongsToModule($organization, $member, $key);
            }

            return false;
        }

        // general + limited: role allowlist is enough for GPs and non-doctor roles.
        // Desk specialists already passed the match gate above.
        return $this->roleListed($member, $manageRoles);
    }

    /**
     * Read results / history / workspace without clinical edit.
     */
    public function memberCanView(Organization $organization, ?Member $member, string $key): bool
    {
        $key = $this->normalizeKey($key);
        if ($this->memberCanManage($organization, $member, $key)
            || $this->memberCanRefer($organization, $member, $key)) {
            return true;
        }

        if (! $this->isEnabled($organization, $key) || ! $member) {
            return false;
        }

        $permissions = app(CarePermissions::class);
        if ((string) $member->role === 'department_manager') {
            return true;
        }

        $primaryApps = $permissions->primaryAppsFor($member);
        if (is_array($primaryApps)) {
            // Matrix roles: view only what they can manage (no cross-module GP view
            // unless listed — general_physician gets cardiology manage via apps).
            if ((string) $member->role === 'doctor'
                && $this->isDeskSpecialist($organization, $member)) {
                return $this->specialistBelongsToModule($organization, $member, $key);
            }

            if (in_array($key, $permissions->viewAppsFor($member), true)) {
                return true;
            }

            // Secondary: config view_roles (docs + legacy allowlists).
            $definition = $this->definition($key);
            if ($definition && $this->roleListed($member, $definition['view_roles'] ?? [])) {
                return true;
            }

            return in_array($key, $primaryApps, true);
        }

        // Desk specialists do not get cross-module view; only their matching desks.
        if ($this->isDeskSpecialist($organization, $member)
            && ! $this->specialistBelongsToModule($organization, $member, $key)) {
            return false;
        }

        $definition = $this->definition($key);
        if (! $definition) {
            return false;
        }

        return $this->roleListed($member, $definition['view_roles'] ?? []);
    }

    /**
     * Refer a patient into the specialty queue without full clinical edit.
     */
    public function memberCanRefer(Organization $organization, ?Member $member, string $key): bool
    {
        $key = $this->normalizeKey($key);
        if ($this->memberCanManage($organization, $member, $key)) {
            return true;
        }

        if (! $this->isEnabled($organization, $key) || ! $member) {
            return false;
        }

        $permissions = app(CarePermissions::class);
        $primaryApps = $permissions->primaryAppsFor($member);
        if (is_array($primaryApps)) {
            if ((string) $member->role === 'doctor'
                && $this->isDeskSpecialist($organization, $member)) {
                return $this->specialistBelongsToModule($organization, $member, $key);
            }

            if (in_array($key, $permissions->referAppsFor($member), true)) {
                return true;
            }

            // Secondary: config refer_roles (docs + legacy allowlists).
            $definition = $this->definition($key);
            if ($definition && $this->roleListed($member, $definition['refer_roles'] ?? [])) {
                return true;
            }

            return in_array($key, $primaryApps, true);
        }

        // Desk specialists do not get cross-module referral nav; GPs keep refer_roles.
        if ($this->isDeskSpecialist($organization, $member)
            && ! $this->specialistBelongsToModule($organization, $member, $key)) {
            return false;
        }

        $definition = $this->definition($key);
        if (! $definition) {
            return false;
        }

        return $this->roleListed($member, $definition['refer_roles'] ?? []);
    }

    /**
     * Legacy doctor with a specialty desk: non-GP specialty matching a catalog
     * module, or assigned to a provisioned specialty department.
     * Dedicated specialist roles use primary apps instead.
     */
    public function isDeskSpecialist(Organization $organization, Member $member): bool
    {
        $cacheKey = $organization->id.'|'.$member->id;
        if (isset($this->deskSpecialistCache[$cacheKey])) {
            return $this->deskSpecialistCache[$cacheKey];
        }

        if (! in_array((string) $member->role, ['doctor'], true)) {
            return $this->deskSpecialistCache[$cacheKey] = false;
        }

        foreach (array_keys($this->catalog()) as $key) {
            if ($this->specialistBelongsToModule($organization, $member, $key)) {
                return $this->deskSpecialistCache[$cacheKey] = true;
            }
        }

        return $this->deskSpecialistCache[$cacheKey] = false;
    }

    /**
     * Whether this doctor belongs to the module via department assignment or specialty keywords.
     */
    public function specialistBelongsToModule(Organization $organization, Member $member, string $key): bool
    {
        return $this->doctorAssignedToModule($organization, $member, $key)
            || $this->doctorSpecialtyMatchesModule($organization, $member, $key);
    }

    /**
     * @param  list<string>|mixed  $roles
     */
    protected function roleListed(Member $member, mixed $roles): bool
    {
        return is_array($roles) && $roles !== [] && in_array($member->role, $roles, true);
    }

    public function doctorAssignedToModule(Organization $organization, Member $member, string $key): bool
    {
        $definition = $this->definition($key);
        if (! $definition) {
            return false;
        }

        $departmentType = (string) ($definition['department_type'] ?? '');
        $provisionedIds = data_get(
            $organization->settings,
            "specialty_module_provisioning.{$key}.department_ids",
            [],
        );
        $provisionedIds = is_array($provisionedIds)
            ? array_map('intval', $provisionedIds)
            : [];

        foreach ($this->practitionersForMember($organization, $member) as $practitioner) {
            if ($provisionedIds !== [] && in_array((int) $practitioner->department_id, $provisionedIds, true)) {
                return true;
            }
            if ($departmentType !== '' && $practitioner->department?->type === $departmentType) {
                return true;
            }
        }

        return false;
    }

    /**
     * Match practitioner.specialty text to module specialist_keywords (restricted specialists).
     */
    public function doctorSpecialtyMatchesModule(Organization $organization, Member $member, string $key): bool
    {
        $definition = $this->definition($key);
        if (! $definition) {
            return false;
        }

        $keywords = $definition['specialist_keywords'] ?? [];
        if (! is_array($keywords) || $keywords === []) {
            $keywords = $definition['queue_keywords'] ?? [];
        }
        if (! is_array($keywords) || $keywords === []) {
            return false;
        }

        foreach ($this->practitionersForMember($organization, $member) as $practitioner) {
            $hay = strtolower(trim((string) ($practitioner->specialty ?? '')));
            if ($hay === '' || $this->isGeneralPracticeSpecialty($hay)) {
                continue;
            }
            foreach ($keywords as $keyword) {
                if ($this->specialtyKeywordMatches($hay, strtolower((string) $keyword))) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Match specialty text to a keyword without short-token false positives
     * (e.g. "ent" must not match "dentistry"; "ear" must not match "year").
     */
    protected function specialtyKeywordMatches(string $haystackLower, string $needleLower): bool
    {
        $needleLower = trim($needleLower);
        if ($needleLower === '' || $haystackLower === '') {
            return false;
        }

        if ($haystackLower === $needleLower) {
            return true;
        }

        $quoted = preg_quote($needleLower, '/');

        // Whole-word match covers short tokens (ent, ear, eye) and multi-word labels.
        if (preg_match('/\b'.$quoted.'\b/u', $haystackLower) === 1) {
            return true;
        }

        // Longer stems may prefix a token (dent→dentistry, cardio→cardiology).
        if (strlen($needleLower) >= 4 && preg_match('/\b'.$quoted.'/u', $haystackLower) === 1) {
            return true;
        }

        return false;
    }

    protected function isGeneralPracticeSpecialty(string $specialtyLower): bool
    {
        if (in_array($specialtyLower, [
            'general practice',
            'general',
            'gp',
            'family medicine',
            'internal medicine',
            'nursing',
        ], true)) {
            return true;
        }

        return str_contains($specialtyLower, 'general practice')
            || str_contains($specialtyLower, 'family medicine');
    }

    /**
     * @return \Illuminate\Support\Collection<int, Practitioner>
     */
    protected function practitionersForMember(Organization $organization, Member $member)
    {
        $cacheKey = $organization->id.'|'.$member->id;
        if (isset($this->practitionersCache[$cacheKey])) {
            return $this->practitionersCache[$cacheKey];
        }

        $branchId = app(OrganizationResolver::class)->branchScope($member);

        return $this->practitionersCache[$cacheKey] = Practitioner::owned((string) $organization->owner_ref)
            ->where('organization_id', $organization->id)
            ->where('is_active', true)
            ->where(function ($query) use ($member) {
                $query->where('member_id', $member->id)
                    ->orWhere('user_ref', $member->user_ref);
            })
            ->with(['department', 'branches'])
            ->get()
            ->filter(function (Practitioner $practitioner) use ($branchId) {
                if (! $branchId) {
                    return true;
                }

                return in_array((int) $branchId, $practitioner->assignedBranchIds(), true);
            });
    }

    public function canManage(Organization $organization): bool
    {
        return $this->plans->hasFeature($organization, 'specialty_modules');
    }

    /**
     * @param  array<string, bool>  $desired  module key => enabled
     * @return array{activated: list<string>, deactivated: list<string>, errors: list<string>}
     */
    public function sync(Organization $organization, string $ownerRef, array $desired): array
    {
        $activated = [];
        $deactivated = [];
        $errors = [];

        foreach ($this->catalog() as $key => $definition) {
            $want = (bool) ($desired[$key] ?? false);
            if ($this->isDefaultOnPaidPlans($key) && $this->plans->hasPaidPlan($organization)) {
                $want = true;
            }
            $have = $this->isEnabled($organization, $key);

            if ($want === $have) {
                // Still ensure defaults are provisioned even when already "enabled".
                if ($want && $this->isDefaultOnPaidPlans($key)
                    && ! data_get($organization->settings, "specialty_module_provisioning.{$key}.active")) {
                    try {
                        $this->activate($organization, $ownerRef, $key);
                        $organization->refresh();
                    } catch (\Throwable $e) {
                        $errors[] = ($definition['label'] ?? $key).': '.$e->getMessage();
                    }
                }

                continue;
            }

            try {
                if ($want) {
                    $this->activate($organization, $ownerRef, $key);
                    $activated[] = $key;
                } else {
                    $this->deactivate($organization, $ownerRef, $key);
                    $deactivated[] = $key;
                }
                $organization->refresh();
            } catch (\Throwable $e) {
                Log::warning('specialty_module.sync_failed', [
                    'key' => $key,
                    'want' => $want,
                    'message' => $e->getMessage(),
                ]);
                $errors[] = ($definition['label'] ?? $key).': '.$e->getMessage();
            }
        }

        return compact('activated', 'deactivated', 'errors');
    }

    public function activate(Organization $organization, string $ownerRef, string $key): void
    {
        $definition = $this->definition($key);
        if (! $definition) {
            throw new \InvalidArgumentException("Unknown specialty module [{$key}].");
        }

        if (! $this->canManage($organization)) {
            throw new \RuntimeException('Specialty modules require Care Pro or Enterprise.');
        }

        $settings = $organization->settings ?? [];
        $modules = is_array($settings['specialty_modules'] ?? null) ? $settings['specialty_modules'] : [];
        $modules[$key] = true;
        $settings['specialty_modules'] = $modules;

        $departments = $this->provisionDepartments($organization, $ownerRef, $definition);
        $queueStubs = $this->provisionQueueStubs($organization, $ownerRef, $key, $definition);

        $provisioning = is_array($settings['specialty_module_provisioning'] ?? null)
            ? $settings['specialty_module_provisioning']
            : [];
        $provisioning[$key] = [
            'active' => true,
            'department_ids' => $departments,
            'queues' => $queueStubs,
            'activated_at' => now()->toIso8601String(),
        ];
        $settings['specialty_module_provisioning'] = $provisioning;

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

        // Seed billable specialty services into provisioning (Billing Engine catalog).
        try {
            app(SpecialtyShellService::class)->seedServices($organization->fresh(), $key);
        } catch (\Throwable $e) {
            Log::warning('specialty_module.service_catalog_seed_failed', [
                'key' => $key,
                'message' => $e->getMessage(),
            ]);
        }

        $fresh = $organization->fresh();
        if (data_get($fresh?->settings, 'queue_integration_enabled')) {
            $provisioner = app(CareQueueProvisioner::class);
            $branches = Branch::owned($ownerRef)
                ->where('organization_id', $organization->id)
                ->where('is_active', true)
                ->pluck('id');
            foreach ($branches as $branchId) {
                try {
                    $provisioner->ensure($fresh, $key, (int) $branchId);
                } catch (\Throwable $e) {
                    Log::warning('specialty_module.native_queue_provision_failed', [
                        'key' => $key,
                        'branch_id' => $branchId,
                        'message' => $e->getMessage(),
                    ]);
                }
            }
        }
    }

    public function deactivate(Organization $organization, string $ownerRef, string $key): void
    {
        if (! $this->definition($key)) {
            throw new \InvalidArgumentException("Unknown specialty module [{$key}].");
        }

        if ($this->isDefaultOnPaidPlans($key) && $this->plans->hasPaidPlan($organization)) {
            throw new \RuntimeException(($this->definition($key)['label'] ?? $key).' stays enabled on Pro and Enterprise.');
        }

        $settings = $organization->settings ?? [];
        $modules = is_array($settings['specialty_modules'] ?? null) ? $settings['specialty_modules'] : [];
        $modules[$key] = false;
        $settings['specialty_modules'] = $modules;

        $provisioning = is_array($settings['specialty_module_provisioning'] ?? null)
            ? $settings['specialty_module_provisioning']
            : [];
        $record = is_array($provisioning[$key] ?? null) ? $provisioning[$key] : [];

        $departmentIds = $record['department_ids'] ?? [];
        if (is_array($departmentIds) && $departmentIds !== []) {
            Department::owned($ownerRef)
                ->whereIn('id', $departmentIds)
                ->update(['is_active' => false]);
        }

        $queues = is_array($record['queues'] ?? null) ? $record['queues'] : [];
        foreach ($queues as $i => $queue) {
            $queues[$i]['active'] = false;
        }

        if (data_get($settings, 'queue_integration_enabled') && $queues !== []) {
            try {
                CareServiceQueue::query()
                    ->where('organization_id', $organization->id)
                    ->where('context', $key)
                    ->update(['is_active' => false]);
            } catch (\Throwable $e) {
                Log::warning('specialty_module.queue_deactivate_failed', [
                    'key' => $key,
                    'message' => $e->getMessage(),
                ]);
            }
        }

        $provisioning[$key] = array_merge($record, [
            'active' => false,
            'queues' => $queues,
            'deactivated_at' => now()->toIso8601String(),
        ]);
        $settings['specialty_module_provisioning'] = $provisioning;

        $organization->update(['settings' => $settings]);
    }

    /**
     * @param  array<string, mixed>  $definition
     * @return list<int>
     */
    protected function provisionDepartments(Organization $organization, string $ownerRef, array $definition): array
    {
        $type = (string) ($definition['department_type'] ?? 'general');
        $name = (string) ($definition['department_name'] ?? $definition['label'] ?? 'Specialty');

        $branches = Branch::owned($ownerRef)
            ->where('organization_id', $organization->id)
            ->where('is_active', true)
            ->get();

        $ids = [];
        foreach ($branches as $branch) {
            $department = Department::withTrashed()
                ->owned($ownerRef)
                ->where('branch_id', $branch->id)
                ->where('type', $type)
                ->first();

            if ($department) {
                if ($department->trashed()) {
                    $department->restore();
                }
                $department->update([
                    'name' => $name,
                    'is_active' => true,
                ]);
            } else {
                $department = Department::create([
                    'owner_ref' => $ownerRef,
                    'branch_id' => $branch->id,
                    'name' => $name,
                    'type' => $type,
                    'is_active' => true,
                ]);
            }

            $ids[] = (int) $department->id;
        }

        return $ids;
    }

    /**
     * Care-side queue stubs (branch-aware). When Queue integration is enabled,
     * activate() creates/links real Queue queues + counters and stores UUIDs here.
     *
     * @param  array<string, mixed>  $definition
     * @return list<array<string, mixed>>
     */
    protected function provisionQueueStubs(
        Organization $organization,
        string $ownerRef,
        string $key,
        array $definition,
    ): array {
        $branches = Branch::owned($ownerRef)
            ->where('organization_id', $organization->id)
            ->where('is_active', true)
            ->get();

        $existingByBranch = collect(
            data_get($organization->settings, "specialty_module_provisioning.{$key}.queues", [])
        )->keyBy(fn ($q) => (string) ($q['branch_id'] ?? ''));

        $stubs = [];
        foreach ($branches as $branch) {
            $prior = is_array($existingByBranch->get((string) $branch->id))
                ? $existingByBranch->get((string) $branch->id)
                : [];

            $stubs[] = [
                'module' => $key,
                'branch_id' => $branch->id,
                'branch_name' => $branch->name,
                'name' => (string) ($definition['queue_name'] ?? $definition['label']),
                'prefix' => (string) ($definition['queue_prefix'] ?? strtoupper(substr($key, 0, 3))),
                'active' => true,
                'synced' => false,
                'queue_uuid' => $prior['queue_uuid'] ?? null,
                'counter_uuid' => $prior['counter_uuid'] ?? null,
                'queue_external_key' => $prior['queue_external_key']
                    ?? "care:specialty:{$key}:queue:{$branch->id}",
                'counter_external_key' => $prior['counter_external_key']
                    ?? "care:specialty:{$key}:counter:{$branch->id}",
            ];
        }

        return $stubs;
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function queueStubsFor(Organization $organization, string $key): array
    {
        $queues = data_get($organization->settings, "specialty_module_provisioning.{$key}.queues", []);

        return is_array($queues) ? array_values(array_filter($queues, fn ($q) => (bool) ($q['active'] ?? false))) : [];
    }
}
