<?php

namespace App\Services\Care\Workflow;

use App\Models\FinancialObligation;
use App\Models\InvestigationRequest;
use App\Models\Organization;
use App\Models\Prescription;
use App\Models\Visit;
use App\Models\WorkflowStage;
use App\Services\Care\CareFeatures;
use App\Services\Care\CarePricingService;

/**
 * Owns FinancialObligation lifecycle and answers whether a stage's financial
 * gate is satisfied for a given visit.
 *
 * A "before" gate blocks entry to the stage's service queue until the stage's
 * obligation is cleared (paid / authorized / waived / deferred). An "after"
 * gate never blocks entry but is created so the balance is collected at exit.
 *
 * When Care Queue Engine is enabled, WorkflowEngine / WorkflowQueueReleaseService
 * place awaiting_payment visits on the billing (cashier) queue via an open bill.
 */
class FinancialGateService
{
    public function __construct(
        protected CareFeatures $features,
    ) {}

    /**
     * Create (or return existing) obligation for a visit + stage. Only stages
     * that require payment produce an obligation.
     *
     * @param  array<string, mixed>  $overrides
     */
    public function obligationFor(Visit $visit, WorkflowStage $stage, array $overrides = []): ?FinancialObligation
    {
        if (! $stage->requires_payment) {
            return null;
        }

        $organization = Organization::query()->find($visit->organization_id);
        if ($organization && ! $this->features->enabled($organization, CareFeatures::FINANCIAL_GATES)) {
            return null;
        }

        $amount = (int) ($overrides['amount_minor']
            ?? $stage->default_amount_minor
            ?? $this->defaultAmountFor($stage, $visit));
        $defaults = array_merge([
            'owner_ref' => $visit->owner_ref,
            'organization_id' => $visit->organization_id,
            'branch_id' => $visit->branch_id,
            'patient_id' => $visit->patient_id,
            'stage_code' => $stage->code,
            'charge_code' => $stage->charge_code,
            'label' => $stage->charge_label ?? $stage->name,
            'amount_minor' => $amount,
            'status' => $amount > 0
                ? FinancialObligation::STATUS_PENDING
                : FinancialObligation::STATUS_WAIVED,
            'clearance_method' => $amount > 0 ? null : 'no_charge',
            'cleared_at' => $amount > 0 ? null : now(),
        ], $overrides);

        $obligation = FinancialObligation::firstOrCreate(
            [
                'visit_id' => $visit->id,
                'workflow_stage_id' => $stage->id,
            ],
            $defaults,
        );

        // A stage may initially have no billable items (for example pharmacy
        // before a prescription is saved). Re-evaluate no-charge clearances
        // whenever the workflow gate is refreshed.
        if ($obligation->clearance_method === 'no_charge' && $amount > 0) {
            $obligation->forceFill([
                'amount_minor' => $amount,
                'status' => FinancialObligation::STATUS_PENDING,
                'clearance_method' => null,
                'cleared_at' => null,
            ])->save();
        }

        return $obligation;
    }

    /**
     * Whether the visit may enter the stage. Stages with a "before" payment
     * gate require a cleared obligation; everything else is allowed.
     */
    public function isSatisfied(Visit $visit, WorkflowStage $stage): bool
    {
        if (! $stage->gatesBeforeService()) {
            return true;
        }

        $organization = Organization::query()->find($visit->organization_id);
        if ($organization && ! $this->features->enabled($organization, CareFeatures::FINANCIAL_GATES)) {
            return true;
        }

        $obligation = FinancialObligation::query()
            ->where('visit_id', $visit->id)
            ->where('workflow_stage_id', $stage->id)
            ->whereNull('deleted_at')
            ->first();

        return $obligation !== null && $obligation->isCleared();
    }

    public function blockedReason(Visit $visit, WorkflowStage $stage): ?string
    {
        if ($this->isSatisfied($visit, $stage)) {
            return null;
        }

        return 'awaiting_payment';
    }

    /**
     * Clear an obligation (paid / insurance authorized / waived / credit).
     */
    public function clear(
        FinancialObligation $obligation,
        string $method,
        ?string $actorRef = null,
        ?string $paymentMode = null,
        ?string $externalReference = null,
    ): FinancialObligation {
        $status = match ($method) {
            'insurance' => FinancialObligation::STATUS_AUTHORIZED,
            'waiver', 'override' => FinancialObligation::STATUS_WAIVED,
            'credit' => FinancialObligation::STATUS_DEFERRED,
            default => FinancialObligation::STATUS_PAID,
        };

        $obligation->forceFill([
            'status' => $status,
            'clearance_method' => $method,
            'payment_mode' => $paymentMode,
            'external_reference' => $externalReference,
            'cleared_by' => $actorRef,
            'cleared_at' => now(),
        ])->save();

        $organization = Organization::query()->find($obligation->organization_id);
        if ($organization) {
            // Lazy resolve avoids a constructor cycle with WorkflowQueueReleaseService.
            app(WorkflowQueueReleaseService::class)->completeCashier(
                $organization,
                $obligation->fresh(['bill']) ?? $obligation,
                $method,
            );
        }

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

    public function void(FinancialObligation $obligation): FinancialObligation
    {
        $obligation->forceFill(['status' => FinancialObligation::STATUS_VOID])->save();

        return $obligation;
    }

    protected function defaultAmountFor(WorkflowStage $stage, Visit $visit): int
    {
        $org = Organization::query()->find($visit->organization_id);
        $pricing = app(CarePricingService::class);

        return match ($stage->charge_code) {
            'patient_card', 'registration' => $org
                ? $pricing->patientCardFee($org)
                : (int) config('care.billing.patient_card_fee_minor', 0),
            'consultation' => $org
                ? $pricing->consultationFee($org)
                : (int) config('care.billing.consultation_fee_minor', 0),
            'lab' => $this->investigationTotal($visit, imaging: false),
            'imaging' => $this->investigationTotal($visit, imaging: true),
            'pharmacy' => $this->pharmacyTotal($visit),
            'visit_total' => (int) $visit->bills()
                ->whereNotIn('status', ['paid', 'void'])
                ->sum('balance_minor'),
            default => 0,
        };
    }

    protected function investigationTotal(Visit $visit, bool $imaging): int
    {
        $imagingCategories = ['xray', 'ultrasound', 'ct', 'mri'];

        return (int) InvestigationRequest::query()
            ->where('visit_id', $visit->id)
            ->with('investigationType')
            ->get()
            ->filter(function (InvestigationRequest $request) use ($imaging, $imagingCategories) {
                $isImaging = in_array($request->investigationType?->category, $imagingCategories, true);

                return $imaging ? $isImaging : ! $isImaging;
            })
            ->sum(fn (InvestigationRequest $request) => (int) ($request->investigationType?->price_minor ?? 0));
    }

    protected function pharmacyTotal(Visit $visit): int
    {
        return (int) Prescription::query()
            ->where('visit_id', $visit->id)
            ->with('items.drug')
            ->get()
            ->sum(function (Prescription $prescription) {
                return $prescription->items->sum(function ($item) {
                    if ($item->is_procedure) {
                        return 0;
                    }

                    $quantity = max(1, (int) preg_replace('/\D/', '', (string) $item->quantity) ?: 1);

                    return $quantity * (int) ($item->drug?->unit_price_minor ?? 0);
                });
            });
    }
}
