<?php

namespace App\Services\Care;

use App\Models\Branch;
use App\Models\Department;
use App\Models\FacilityWorkflow;
use App\Models\Member;
use App\Models\Organization;
use App\Models\Practitioner;
use App\Models\User;
use App\Services\Care\Workflow\WorkflowTemplateInstaller;
use App\Services\Care\Workflow\WorkflowTemplateRegistry;
use Illuminate\Support\Str;

class OrganizationResolver
{
    /** @var array<string, list<int>> */
    protected array $doctorAssignedBranchIdsCache = [];

    /** @var array<string, ?Member> */
    protected array $membershipCache = [];

    public function resolveForUser(User $user): ?Organization
    {
        $member = $this->membershipFor($user);
        if ($member) {
            $organization = Organization::query()->find($member->organization_id);
            if ($organization) {
                return $organization;
            }
        }

        return Organization::owned($user->ownerRef())->first();
    }

    public function forUser(User $user): Organization
    {
        return $this->resolveForUser($user)
            ?? throw new \RuntimeException('No organization for user.');
    }

    public function isOnboarded(User $user): bool
    {
        $organization = $this->resolveForUser($user);

        return $organization !== null
            && (bool) data_get($organization->settings, 'onboarded', false);
    }

    /**
     * Any Care membership for this user (public_id or invite/demo email key).
     * Remaps email-keyed rows to public_id when found so staff stay linked after SSO.
     */
    public function membershipFor(User $user): ?Member
    {
        $cacheKey = $user->ownerRef();
        if (array_key_exists($cacheKey, $this->membershipCache)) {
            return $this->membershipCache[$cacheKey];
        }

        $member = Member::query()->where('user_ref', $user->ownerRef())->first();
        if ($member) {
            return $this->membershipCache[$cacheKey] = $member;
        }

        $email = strtolower(trim((string) $user->email));
        if ($email === '' || $email === $user->ownerRef()) {
            return $this->membershipCache[$cacheKey] = null;
        }

        $member = Member::query()->where('user_ref', $email)->first();

        return $this->membershipCache[$cacheKey] = ($member ? $this->remapMemberUserRef($member, $user) : null);
    }

    /**
     * True when the user may run owner onboarding (create a facility).
     * Staff already attached to a tenant must not create a second org.
     */
    public function canCompleteOnboarding(User $user): bool
    {
        if ($this->membershipFor($user)) {
            return false;
        }

        return ! $this->isOnboarded($user);
    }

    public function memberFor(User $user, ?Organization $organization = null): ?Member
    {
        $organization ??= $this->resolveForUser($user);
        if (! $organization) {
            return null;
        }

        $member = Member::query()
            ->where('organization_id', $organization->id)
            ->where('user_ref', $user->ownerRef())
            ->first();
        if ($member) {
            return $member;
        }

        $email = strtolower(trim((string) $user->email));
        if ($email === '') {
            return null;
        }

        $member = Member::query()
            ->where('organization_id', $organization->id)
            ->where('user_ref', $email)
            ->first();

        return $member ? $this->remapMemberUserRef($member, $user) : null;
    }

    /**
     * Promote invite/demo email user_ref keys to the stable OIDC public_id.
     */
    protected function remapMemberUserRef(Member $member, User $user): Member
    {
        $ref = $user->ownerRef();
        if ($member->user_ref === $ref) {
            return $member;
        }

        $conflict = Member::query()
            ->where('organization_id', $member->organization_id)
            ->where('user_ref', $ref)
            ->where('id', '!=', $member->id)
            ->first();

        if ($conflict) {
            $member->delete();

            return $conflict;
        }

        $member->forceFill(['user_ref' => $ref])->save();

        return $member->fresh() ?? $member;
    }

    public function ensureOwnerMember(User $user, Organization $organization): Member
    {
        return Member::firstOrCreate(
            [
                'organization_id' => $organization->id,
                'user_ref' => $user->ownerRef(),
            ],
            [
                'owner_ref' => $user->ownerRef(),
                'role' => 'hospital_admin',
            ],
        );
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public function completeOnboarding(User $user, array $data): Organization
    {
        $ref = $user->ownerRef();

        $registry = app(WorkflowTemplateRegistry::class);

        // New onboarding leads with Facility Category (modules) + Workflow
        // Template (patient flow); legacy callers may still pass facility_type.
        $category = $data['facility_category'] ?? $this->categoryFromLegacyType($data['facility_type'] ?? null);
        $templateKey = $data['workflow_template']
            ?? $registry->defaultTemplateForCategory($category);
        $modules = $registry->modulesForCategory($category);

        $settings = [
            'onboarded' => true,
            'facility_category' => $category,
            'facility_type' => $data['facility_type'] ?? $category,
            'modules' => $modules,
            'workflow_template' => $templateKey,
            'rollout' => [
                CareFeatures::WORKFLOW_ENGINE => $templateKey !== 'legacy_clinic',
                // Financial gates require the paid billing module and are
                // explicitly enabled by an administrator in Settings.
                CareFeatures::FINANCIAL_GATES => false,
            ],
        ];

        // Reuse an existing owner org (e.g. demo shell that lost the race) so
        // onboarding never creates a second tenant for the same owner_ref.
        $organization = Organization::withTrashed()->where('owner_ref', $ref)->first();
        if ($organization) {
            if ($organization->trashed()) {
                $organization->restore();
            }
            $organization->forceFill([
                'name' => $data['organization_name'],
                'slug' => Str::slug($data['organization_name']).'-'.substr($ref, 0, 6),
                'timezone' => $data['timezone'] ?? config('app.timezone', 'UTC'),
                'settings' => array_merge($organization->settings ?? [], $settings),
            ])->save();
            $organization = $organization->fresh();
        } else {
            $organization = Organization::create([
                'owner_ref' => $ref,
                'name' => $data['organization_name'],
                'slug' => Str::slug($data['organization_name']).'-'.substr($ref, 0, 6),
                'timezone' => $data['timezone'] ?? config('app.timezone', 'UTC'),
                'settings' => $settings,
            ]);
        }

        $this->ensureOwnerMember($user, $organization);

        $branch = Branch::query()
            ->where('organization_id', $organization->id)
            ->orderBy('id')
            ->first();

        if (! $branch) {
            $branch = Branch::create([
                'owner_ref' => $ref,
                'organization_id' => $organization->id,
                'name' => $data['branch_name'],
                'address' => $data['branch_address'] ?? null,
                'phone' => $data['branch_phone'] ?? null,
                'is_active' => true,
            ]);

            Department::create([
                'owner_ref' => $ref,
                'branch_id' => $branch->id,
                'name' => 'General Outpatient',
                'type' => 'outpatient',
                'is_active' => true,
            ]);
        } else {
            $branch->forceFill([
                'name' => $data['branch_name'],
                'address' => $data['branch_address'] ?? $branch->address,
                'phone' => $data['branch_phone'] ?? $branch->phone,
                'is_active' => true,
            ])->save();
        }

        if ($registry->hasTemplate($templateKey)
            && ! FacilityWorkflow::query()->where('organization_id', $organization->id)->exists()) {
            app(WorkflowTemplateInstaller::class)->install($organization, $branch, $templateKey, [
                'category' => $category,
            ]);
        }

        AuditLogger::record($ref, 'organization.created', $organization->id, $ref, Organization::class, $organization->id);
        AuditLogger::record($ref, 'branch.created', $organization->id, $ref, Branch::class, $branch->id);

        return $organization;
    }

    protected function categoryFromLegacyType(?string $type): string
    {
        return match ($type) {
            'hospital' => 'hospital',
            'diagnostic' => 'diagnostic',
            'specialist' => 'specialist',
            default => 'clinic',
        };
    }

    /**
     * Branch IDs the member may access.
     * null = all branches (admins). Empty array = none assigned.
     *
     * @return list<int>|null
     */
    public function allowedBranchIds(?Member $member): ?array
    {
        if ($member === null) {
            return null;
        }

        if (in_array($member->role, ['super_admin', 'hospital_admin', 'accountant'], true)) {
            return null;
        }

        if (app(CarePermissions::class)->usesPractitionerBranchScope($member)) {
            return $this->doctorAssignedBranchIds($member);
        }

        if ($member->branch_id) {
            return [(int) $member->branch_id];
        }

        return null;
    }

    /**
     * Explicit branch IDs for a doctor's linked practitioner desks (pivot + legacy branch_id).
     * GPs with no practitioner desk fall back to their member branch so they can still
     * open the shared consultation Queue board.
     *
     * @return list<int>
     */
    public function doctorAssignedBranchIds(Member $member): array
    {
        $cacheKey = (string) $member->id;
        if (isset($this->doctorAssignedBranchIdsCache[$cacheKey])) {
            return $this->doctorAssignedBranchIdsCache[$cacheKey];
        }

        $practitioners = Practitioner::query()
            ->where('is_active', true)
            ->where(function ($query) use ($member) {
                $query->where('member_id', $member->id)
                    ->orWhere('user_ref', $member->user_ref);
            })
            ->with('branches')
            ->orderBy('id')
            ->get();

        $ids = [];
        foreach ($practitioners as $practitioner) {
            $ids = array_merge($ids, $practitioner->assignedBranchIds());
        }

        $ids = array_values(array_unique(array_map('intval', $ids)));

        if ($ids === [] && $member->branch_id) {
            $ids = [(int) $member->branch_id];
        }

        return $this->doctorAssignedBranchIdsCache[$cacheKey] = $ids;
    }

    public function mayAccessBranch(?Member $member, ?int $branchId): bool
    {
        if ($branchId === null || $branchId <= 0) {
            return false;
        }

        $allowed = $this->allowedBranchIds($member);
        if ($allowed === null) {
            return true;
        }

        return in_array((int) $branchId, $allowed, true);
    }

    /** Current working branch ID; null = all branches (admins only). */
    public function branchScope(?Member $member): ?int
    {
        if ($member === null) {
            return null;
        }

        if (in_array($member->role, ['super_admin', 'hospital_admin', 'accountant'], true)) {
            return null;
        }

        if (app(CarePermissions::class)->usesPractitionerBranchScope($member)) {
            $ids = $this->doctorAssignedBranchIds($member);
            if ($ids === []) {
                return 0;
            }
            if (count($ids) === 1) {
                return $ids[0];
            }

            $preferred = (int) session(BranchContext::SESSION_KEY, 0);
            if (in_array($preferred, $ids, true)) {
                return $preferred;
            }

            return $ids[0];
        }

        if ($member->branch_id) {
            return (int) $member->branch_id;
        }

        return $member->branch_id;
    }

    /**
     * Practitioner IDs a doctor is locked to. Null = no practitioner lock
     * (non-doctors, or GPs with no linked desk — they work the shared consultation board).
     * Non-empty = desk-locked specialist / assigned GP.
     *
     * @return list<int>|null
     */
    public function practitionerScope(Organization $organization, ?Member $member): ?array
    {
        if ($member === null || $member->role !== 'doctor') {
            return null;
        }

        $ids = 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);
            })
            ->orderBy('id')
            ->pluck('id')
            ->map(fn ($id) => (int) $id)
            ->values()
            ->all();

        return $ids === [] ? null : $ids;
    }
}
