<?php

namespace App\Services\Care\Workflow;

use App\Models\FacilityWorkflow;
use App\Models\Visit;
use App\Models\VisitStageAdvance;
use App\Models\WorkflowStage;
use Illuminate\Support\Facades\DB;

/**
 * Drives a visit through its facility workflow. Responsible for placing a visit
 * on its first stage, advancing between stages, and — together with
 * FinancialGateService — deciding whether a stage may be entered or whether the
 * visit is blocked awaiting payment / authorization.
 */
class WorkflowEngine
{
    public function __construct(
        protected FinancialGateService $gate,
    ) {}

    /** The active workflow for a visit's org + branch (branch-specific wins). */
    public function workflowFor(Visit $visit): ?FacilityWorkflow
    {
        return FacilityWorkflow::query()
            ->where('organization_id', $visit->organization_id)
            ->where('is_active', true)
            ->where(function ($q) use ($visit) {
                $q->where('branch_id', $visit->branch_id)->orWhereNull('branch_id');
            })
            ->orderByRaw('branch_id is null')
            ->with('stages')
            ->first();
    }

    /**
     * Place the visit on the workflow's first stage. Idempotent — returns the
     * existing active advance if the visit is already started.
     */
    public function start(Visit $visit, ?FacilityWorkflow $workflow = null): ?VisitStageAdvance
    {
        return DB::transaction(function () use ($visit, $workflow) {
            Visit::query()->whereKey($visit->id)->lockForUpdate()->first();

            $workflow ??= $this->workflowFor($visit);
            if ($workflow === null) {
                return null;
            }

            $existing = $this->currentAdvance($visit);
            if ($existing !== null) {
                return $existing;
            }

            $first = $workflow->firstStage();
            if ($first === null) {
                return null;
            }

            return $this->enterStage($visit, $workflow, $first);
        });
    }

    public function currentAdvance(Visit $visit): ?VisitStageAdvance
    {
        if ($visit->relationLoaded('currentStageAdvance')) {
            return $visit->currentStageAdvance;
        }

        if ($visit->relationLoaded('stageAdvances')) {
            return $visit->stageAdvances
                ->whereIn('status', [VisitStageAdvance::STATUS_ACTIVE, VisitStageAdvance::STATUS_BLOCKED])
                ->sortByDesc('id')
                ->first();
        }

        return VisitStageAdvance::query()
            ->where('visit_id', $visit->id)
            ->whereIn('status', [VisitStageAdvance::STATUS_ACTIVE, VisitStageAdvance::STATUS_BLOCKED])
            ->orderByDesc('id')
            ->first();
    }

    public function currentStage(Visit $visit): ?WorkflowStage
    {
        return $this->currentAdvance($visit)?->stage;
    }

    /**
     * Whether the visit may advance to $toCode (or the current stage's
     * default_next when omitted). Blocked when the target stage has an unmet
     * "before" financial gate.
     */
    public function canAdvance(Visit $visit, ?string $toCode = null): bool
    {
        $current = $this->refreshGate($visit);
        if ($current === null || $current->isBlocked()) {
            return false;
        }

        $target = $this->resolveTarget($visit, $toCode);

        return $target !== null;
    }

    public function canManuallyAdvance(Visit $visit): bool
    {
        $stage = $this->refreshGate($visit)?->stage;
        if ($stage === null || in_array($stage->type, [
            'consultation',
            'laboratory',
            'imaging',
            'procedure',
            'pharmacy',
            'payment',
        ], true)) {
            return false;
        }

        return $this->canAdvance($visit);
    }

    /**
     * Advance the visit to the next stage. Completes the current advance and
     * enters the target stage; if the target is gated and unpaid the new
     * advance is created in the "blocked" state (visit is not queued until the
     * obligation clears).
     */
    public function advance(Visit $visit, ?string $toCode = null): ?VisitStageAdvance
    {
        return DB::transaction(function () use ($visit, $toCode) {
            Visit::query()->whereKey($visit->id)->lockForUpdate()->first();

            $workflow = $this->workflowFor($visit);
            if ($workflow === null) {
                return null;
            }

            $target = $this->resolveTarget($visit, $toCode, $workflow);
            if ($target === null) {
                return null;
            }

            $current = $this->refreshGate($visit);
            if ($current?->isBlocked()) {
                throw new \DomainException('Clear the current stage financial gate before advancing this visit.');
            }

            if ($current !== null) {
                $current->forceFill([
                    'status' => VisitStageAdvance::STATUS_COMPLETED,
                    'completed_at' => now(),
                ])->save();
            }

            return $this->enterStage($visit, $workflow, $target);
        });
    }

    /**
     * Whether the current stage may release the patient into its Queue service
     * point. False while a "before" gate is unmet.
     */
    public function isQueueReleasable(Visit $visit): bool
    {
        $advance = $this->currentAdvance($visit);
        if ($advance === null || $advance->stage === null) {
            return true;
        }

        return $this->gate->isSatisfied($visit, $advance->stage);
    }

    /**
     * Re-evaluate the current advance after an obligation changes. Moves a
     * blocked advance to active once its gate clears.
     */
    public function refreshGate(Visit $visit): ?VisitStageAdvance
    {
        $advance = $this->currentAdvance($visit);
        if ($advance === null || $advance->stage === null) {
            return $advance;
        }

        $this->gate->obligationFor($visit, $advance->stage);
        $satisfied = $this->gate->isSatisfied($visit, $advance->stage);
        $becameBlocked = false;

        if ($advance->isBlocked() && $satisfied) {
            $advance->forceFill([
                'status' => VisitStageAdvance::STATUS_ACTIVE,
                'blocked_reason' => null,
                'cleared_at' => now(),
            ])->save();
        } elseif (! $advance->isBlocked() && ! $satisfied) {
            $advance->forceFill([
                'status' => VisitStageAdvance::STATUS_BLOCKED,
                'blocked_reason' => $this->gate->blockedReason($visit, $advance->stage),
                'cleared_at' => null,
            ])->save();
            $becameBlocked = true;
        }

        $fresh = $advance->fresh();
        // Only enqueue when the gate newly blocks — not on every refresh — so
        // CareQueueBridge::issueForBill → canRelease → refreshGate cannot recurse.
        if ($becameBlocked && $fresh?->blocked_reason === 'awaiting_payment') {
            $this->enqueueCashierBooth($visit);
        }

        return $fresh;
    }

    protected function enterStage(Visit $visit, FacilityWorkflow $workflow, WorkflowStage $stage): VisitStageAdvance
    {
        $this->gate->obligationFor($visit, $stage);

        $satisfied = $this->gate->isSatisfied($visit, $stage);

        $advance = VisitStageAdvance::create([
            'owner_ref' => $visit->owner_ref,
            'visit_id' => $visit->id,
            'workflow_id' => $workflow->id,
            'workflow_stage_id' => $stage->id,
            'stage_code' => $stage->code,
            'status' => $satisfied ? VisitStageAdvance::STATUS_ACTIVE : VisitStageAdvance::STATUS_BLOCKED,
            'blocked_reason' => $satisfied ? null : $this->gate->blockedReason($visit, $stage),
            'entered_at' => now(),
            'cleared_at' => $satisfied ? now() : null,
        ]);

        if ($advance->isBlocked() && $advance->blocked_reason === 'awaiting_payment') {
            $this->enqueueCashierBooth($visit);
        }

        return $advance;
    }

    /**
     * Resolve lazily to avoid a constructor cycle with WorkflowQueueReleaseService.
     */
    protected function enqueueCashierBooth(Visit $visit): void
    {
        $organization = $visit->organization
            ?? \App\Models\Organization::query()->find($visit->organization_id);
        if ($organization === null) {
            return;
        }

        app(WorkflowQueueReleaseService::class)->enqueueCashier($organization, $visit);
    }

    protected function resolveTarget(Visit $visit, ?string $toCode, ?FacilityWorkflow $workflow = null): ?WorkflowStage
    {
        $workflow ??= $this->workflowFor($visit);
        if ($workflow === null) {
            return null;
        }

        if ($toCode !== null && $toCode !== '') {
            $target = $workflow->stage($toCode);
            $current = $this->currentAdvance($visit)?->stage;
            if ($target === null || $current === null) {
                return null;
            }

            $isConfiguredNext = $current->default_next === $target->code;
            $mayBranch = $current->creates_child || $current->can_return || $current->can_skip;

            return $isConfiguredNext || $mayBranch ? $target : null;
        }

        $current = $this->currentAdvance($visit)?->stage;
        $nextCode = $current?->default_next;

        return $nextCode ? $workflow->stage($nextCode) : null;
    }
}
