<?php

namespace App\Services\Care;

use App\Models\Bill;
use App\Models\BillLineItem;
use App\Models\FinancialObligation;
use App\Models\InvestigationRequest;
use App\Models\Organization;
use App\Models\Payment;
use App\Models\Prescription;
use App\Models\User;
use App\Models\Visit;
use App\Services\Care\Workflow\WorkflowStageCompletionService;
use App\Services\Payments\MerchantGatewayService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;
use InvalidArgumentException;
use RuntimeException;

class BillService
{
    public function __construct(
        protected InvoiceNumberGenerator $invoices,
        protected MerchantGatewayService $gateway,
        protected CareQueueBridge $queueBridge,
        protected WorkflowStageCompletionService $workflowCompletion,
    ) {}

    public function queryForOrganization(string $ownerRef, int $organizationId): Builder
    {
        return Bill::owned($ownerRef)->where('organization_id', $organizationId);
    }

    /**
     * @param  array<string, mixed>  $filters
     */
    public function list(string $ownerRef, int $organizationId, array $filters = [], ?int $branchId = null): LengthAwarePaginator
    {
        $query = $this->queryForOrganization($ownerRef, $organizationId)
            ->with(['patient', 'branch', 'visit'])
            ->orderByDesc('created_at');

        if ($branchId !== null) {
            $query->where('branch_id', $branchId);
        }

        $status = $filters['status'] ?? null;
        if ($status === 'outstanding') {
            $query->whereIn('status', [Bill::STATUS_OPEN, Bill::STATUS_PARTIAL])
                ->where('balance_minor', '>', 0);
        } elseif ($status) {
            $query->where('status', $status);
        }

        if ($patientId = $filters['patient_id'] ?? null) {
            $query->where('patient_id', $patientId);
        }

        return $query->paginate((int) ($filters['per_page'] ?? 20))->withQueryString();
    }

    public function generateFromVisit(Visit $visit, string $ownerRef, ?string $actorRef = null): Bill
    {
        $existing = Bill::owned($ownerRef)
            ->where('visit_id', $visit->id)
            ->whereNotIn('status', [Bill::STATUS_VOID])
            ->first();

        if ($existing) {
            return $this->syncLineItemsFromVisit($existing, $ownerRef, $actorRef);
        }

        $visit->load(['patient', 'consultations', 'organization']);

        $bill = Bill::create([
            'owner_ref' => $ownerRef,
            'organization_id' => $visit->organization_id,
            'branch_id' => $visit->branch_id,
            'visit_id' => $visit->id,
            'patient_id' => $visit->patient_id,
            'invoice_number' => $this->invoices->generate($visit->organization),
            'status' => Bill::STATUS_OPEN,
            'created_by' => $actorRef,
        ]);

        $this->syncLineItemsFromVisit($bill, $ownerRef, $actorRef);

        AuditLogger::record($ownerRef, 'bill.created', $visit->organization_id, $actorRef, Bill::class, $bill->id);

        $bill = $bill->fresh(['lineItems', 'patient', 'payments']);
        if ($visit->organization) {
            $this->queueBridge->issueForBill($visit->organization, $bill);
        }

        return $bill->fresh(['lineItems', 'patient', 'payments']);
    }

    /**
     * Attach a workflow charge intent to an editable visit bill. A later stage
     * may create a new bill when the previous encounter bill is already paid.
     */
    public function billForObligation(FinancialObligation $obligation, string $ownerRef, ?string $actorRef = null): Bill
    {
        if ($obligation->bill_id) {
            $linked = Bill::query()->find($obligation->bill_id);
            if ($linked) {
                return $linked;
            }
        }

        $visit = $obligation->visit()->with(['organization', 'patient'])->firstOrFail();
        $bill = Bill::owned($ownerRef)
            ->where('visit_id', $visit->id)
            ->whereIn('status', [Bill::STATUS_DRAFT, Bill::STATUS_OPEN, Bill::STATUS_PARTIAL])
            ->latest('id')
            ->first();

        if (! $bill) {
            $bill = Bill::create([
                'owner_ref' => $ownerRef,
                'organization_id' => $visit->organization_id,
                'branch_id' => $visit->branch_id,
                'visit_id' => $visit->id,
                'patient_id' => $visit->patient_id,
                'invoice_number' => $this->invoices->generate($visit->organization),
                'status' => Bill::STATUS_OPEN,
                'created_by' => $actorRef,
            ]);

            AuditLogger::record($ownerRef, 'bill.created', $visit->organization_id, $actorRef, Bill::class, $bill->id);
        }

        $exists = $bill->lineItems()
            ->where('source_type', FinancialObligation::class)
            ->where('source_id', $obligation->id)
            ->exists();

        if (! $exists) {
            $this->addLineItem($bill, $ownerRef, [
                'type' => $this->lineTypeForCharge($obligation->charge_code),
                'description' => $obligation->label ?? 'Workflow charge',
                'quantity' => 1,
                'unit_price_minor' => $obligation->amount_minor,
                'source_type' => FinancialObligation::class,
                'source_id' => $obligation->id,
            ]);
            $this->recalculate($bill);
        }

        $obligation->forceFill(['bill_id' => $bill->id])->save();

        return $bill->fresh(['lineItems', 'payments', 'patient']);
    }

    public function syncLineItemsFromVisit(Bill $bill, string $ownerRef, ?string $actorRef = null): Bill
    {
        if (in_array($bill->status, [Bill::STATUS_PAID, Bill::STATUS_VOID], true)) {
            return $bill;
        }

        $visit = $bill->visit()->with(['consultations'])->firstOrFail();

        $bill->lineItems()
            ->whereNotNull('source_type')
            ->where('source_type', '!=', FinancialObligation::class)
            ->delete();

        $workflowChargeCodes = FinancialObligation::query()
            ->where('bill_id', $bill->id)
            ->pluck('charge_code')
            ->filter()
            ->all();

        if (! in_array('consultation', $workflowChargeCodes, true)
            && $visit->consultations()->where('status', 'completed')->exists()) {
            $org = Organization::query()->find($visit->organization_id);
            $fee = $org
                ? app(CarePricingService::class)->consultationFee($org)
                : (int) config('care.billing.consultation_fee_minor', 5000);
            $this->addLineItem($bill, $ownerRef, [
                'type' => 'consultation',
                'description' => 'Consultation fee',
                'quantity' => 1,
                'unit_price_minor' => $fee,
                'source_type' => Visit::class,
                'source_id' => $visit->id,
            ]);
        }

        InvestigationRequest::owned($ownerRef)
            ->where('visit_id', $visit->id)
            ->whereIn('status', [InvestigationRequest::STATUS_COMPLETED, InvestigationRequest::STATUS_DELIVERED])
            ->with('investigationType')
            ->get()
            ->each(function (InvestigationRequest $request) use ($bill, $ownerRef, $workflowChargeCodes) {
                $chargeCode = in_array(
                    $request->investigationType?->category,
                    ['xray', 'ultrasound', 'ct', 'mri'],
                    true,
                ) ? 'imaging' : 'lab';
                if (in_array($chargeCode, $workflowChargeCodes, true)) {
                    return;
                }

                $this->addLineItem($bill, $ownerRef, [
                    'type' => $chargeCode,
                    'description' => $request->investigationType->name,
                    'quantity' => 1,
                    'unit_price_minor' => $request->investigationType->price_minor,
                    'source_type' => InvestigationRequest::class,
                    'source_id' => $request->id,
                ]);
            });

        Prescription::owned($ownerRef)
            ->where('visit_id', $visit->id)
            ->where('status', Prescription::STATUS_DISPENSED)
            ->with('items.drug')
            ->get()
            ->each(function (Prescription $rx) use ($bill, $ownerRef, $workflowChargeCodes) {
                if (in_array('pharmacy', $workflowChargeCodes, true)) {
                    return;
                }

                foreach ($rx->items as $item) {
                    if ($item->is_procedure) {
                        $this->addLineItem($bill, $ownerRef, [
                            'type' => 'procedure',
                            'description' => $item->name,
                            'quantity' => 1,
                            'unit_price_minor' => 0,
                            'source_type' => Prescription::class,
                            'source_id' => $rx->id,
                        ]);

                        continue;
                    }

                    $price = $item->drug?->unit_price_minor ?? 0;
                    $qty = max(1, (int) preg_replace('/\D/', '', (string) $item->quantity) ?: 1);
                    $this->addLineItem($bill, $ownerRef, [
                        'type' => 'pharmacy',
                        'description' => $item->name,
                        'quantity' => $qty,
                        'unit_price_minor' => $price,
                        'source_type' => Prescription::class,
                        'source_id' => $rx->id,
                    ]);
                }
            });

        $this->recalculate($bill);
        AuditLogger::record($ownerRef, 'bill.updated', $bill->organization_id, $actorRef, Bill::class, $bill->id);

        return $bill->fresh(['lineItems', 'payments', 'patient']);
    }

    protected function lineTypeForCharge(?string $chargeCode): string
    {
        $candidate = match ($chargeCode) {
            'consultation' => 'consultation',
            'lab' => 'lab',
            'imaging' => 'imaging',
            'pharmacy' => 'pharmacy',
            default => 'misc',
        };

        return array_key_exists($candidate, (array) config('care.bill_line_types', []))
            ? $candidate
            : 'misc';
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public function addLineItem(Bill $bill, string $ownerRef, array $data): BillLineItem
    {
        $this->assertEditable($bill);

        $quantity = max(1, (int) ($data['quantity'] ?? 1));
        $unitPrice = (int) ($data['unit_price_minor'] ?? 0);

        return BillLineItem::create([
            'owner_ref' => $ownerRef,
            'bill_id' => $bill->id,
            'type' => $data['type'] ?? 'misc',
            'description' => $data['description'],
            'quantity' => $quantity,
            'unit_price_minor' => $unitPrice,
            'total_minor' => $quantity * $unitPrice,
            'source_type' => $data['source_type'] ?? null,
            'source_id' => $data['source_id'] ?? null,
        ]);
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public function addManualLineItem(Bill $bill, string $ownerRef, array $data, ?string $actorRef = null): Bill
    {
        $this->addLineItem($bill, $ownerRef, $data);
        $this->recalculate($bill);
        AuditLogger::record($ownerRef, 'bill.line_item_added', $bill->organization_id, $actorRef, Bill::class, $bill->id);

        return $bill->fresh(['lineItems', 'payments']);
    }

    public function applyAdjustments(Bill $bill, string $ownerRef, int $discountMinor = 0, int $taxMinor = 0, ?string $actorRef = null): Bill
    {
        $this->assertEditable($bill);

        $bill->update([
            'discount_minor' => max(0, $discountMinor),
            'tax_minor' => max(0, $taxMinor),
        ]);

        $this->recalculate($bill);
        AuditLogger::record($ownerRef, 'bill.updated', $bill->organization_id, $actorRef, Bill::class, $bill->id);

        return $bill->fresh(['lineItems', 'payments']);
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public function recordPayment(Bill $bill, string $ownerRef, array $data, ?string $actorRef = null): Payment
    {
        if ($bill->status === Bill::STATUS_VOID) {
            throw new InvalidArgumentException('Cannot pay a void bill.');
        }

        $amount = (int) $data['amount_minor'];
        if ($amount <= 0) {
            throw new InvalidArgumentException('Payment amount must be positive.');
        }

        $payment = Payment::create([
            'owner_ref' => $ownerRef,
            'bill_id' => $bill->id,
            'amount_minor' => $amount,
            'method' => $data['method'] ?? Payment::METHOD_CASH,
            'status' => Payment::STATUS_PAID,
            'reference' => $data['reference'] ?? null,
            'paid_at' => $data['paid_at'] ?? now(),
            'recorded_by' => $actorRef,
            'notes' => $data['notes'] ?? null,
        ]);

        $this->syncPaymentTotals($bill);

        AuditLogger::record($ownerRef, 'payment.recorded', $bill->organization_id, $actorRef, Payment::class, $payment->id, [
            'bill_id' => $bill->id,
            'amount_minor' => $amount,
        ]);

        return $payment;
    }

    /**
     * Start an online checkout (Paystack / Flutterwave / Hubtel) against the bill balance.
     *
     * @return array{payment: Payment, checkout_url: string, reference: string, provider: string}
     */
    public function startGatewayPayment(
        Bill $bill,
        Organization $organization,
        string $ownerRef,
        int $amountMinor,
        ?User $actor = null,
        ?string $actorRef = null,
    ): array {
        if ($bill->status === Bill::STATUS_VOID) {
            throw new InvalidArgumentException('Cannot pay a void bill.');
        }

        if ($bill->balance_minor <= 0) {
            throw new InvalidArgumentException('This bill is already fully paid.');
        }

        $amount = max(1, min($amountMinor, (int) $bill->balance_minor));

        $email = trim((string) ($bill->patient?->email ?? ''));
        if ($email === '' || ! str_contains($email, '@')) {
            $email = trim((string) ($actor?->email ?? ''));
        }
        if ($email === '' || ! str_contains($email, '@')) {
            $email = 'care+'.($organization->id).'@checkout.ladill.local';
        }

        $reference = 'CARE-'.strtoupper(Str::random(16));
        $setting = $this->gateway->settingFor($organization);

        $payment = Payment::create([
            'owner_ref' => $ownerRef,
            'bill_id' => $bill->id,
            'amount_minor' => $amount,
            'method' => Payment::METHOD_GATEWAY,
            'status' => Payment::STATUS_PENDING,
            'gateway_provider' => $setting?->provider,
            'reference' => $reference,
            'paid_at' => now(), // overwritten when verified; pending rows are excluded from totals
            'recorded_by' => $actorRef,
            'notes' => 'Online checkout initiated',
        ]);

        try {
            $checkout = $this->gateway->initializeCheckout(
                $organization,
                $amount,
                (string) config('care.billing.currency', 'GHS'),
                $email,
                route('care.bills.payments.callback'),
                $reference,
                [
                    'title' => 'Invoice '.$bill->invoice_number,
                    'bill_id' => $bill->id,
                    'bill_uuid' => $bill->uuid,
                    'payment_id' => $payment->id,
                    'organization_id' => $organization->id,
                    'channel' => 'care_encounter',
                ],
            );
        } catch (RuntimeException $e) {
            $payment->update(['status' => Payment::STATUS_FAILED, 'notes' => $e->getMessage()]);

            throw $e;
        }

        $payment->update([
            'reference' => $checkout['reference'],
            'gateway_provider' => $checkout['provider'],
        ]);

        AuditLogger::record($ownerRef, 'payment.checkout_started', $bill->organization_id, $actorRef, Payment::class, $payment->id, [
            'bill_id' => $bill->id,
            'amount_minor' => $amount,
            'provider' => $checkout['provider'],
            'reference' => $checkout['reference'],
        ]);

        return [
            'payment' => $payment->fresh(),
            'checkout_url' => $checkout['checkout_url'],
            'reference' => $checkout['reference'],
            'provider' => $checkout['provider'],
        ];
    }

    public function completeGatewayPayment(string $reference): Payment
    {
        $existingPaid = Payment::query()
            ->where('reference', $reference)
            ->where('status', Payment::STATUS_PAID)
            ->where('method', Payment::METHOD_GATEWAY)
            ->with(['bill'])
            ->first();

        if ($existingPaid) {
            return $existingPaid;
        }

        $payment = Payment::query()
            ->where('reference', $reference)
            ->where('status', Payment::STATUS_PENDING)
            ->where('method', Payment::METHOD_GATEWAY)
            ->with(['bill.organization', 'bill.patient'])
            ->firstOrFail();

        $bill = $payment->bill;
        $organization = $bill->organization ?? Organization::query()->findOrFail($bill->organization_id);

        $result = $this->gateway->verify($organization, $reference);
        if (! $result['paid']) {
            $payment->update([
                'status' => Payment::STATUS_FAILED,
                'notes' => 'Gateway reported payment incomplete.',
            ]);

            throw new RuntimeException('Payment was not completed.');
        }

        $verifiedAmount = (int) ($result['amount_minor'] ?: $payment->amount_minor);
        if ($verifiedAmount <= 0) {
            $verifiedAmount = (int) $payment->amount_minor;
        }

        $payment->update([
            'status' => Payment::STATUS_PAID,
            'amount_minor' => $verifiedAmount,
            'paid_at' => now(),
            'gateway_provider' => $result['provider'] ?? $payment->gateway_provider,
            'notes' => 'Online payment verified',
        ]);

        $this->syncPaymentTotals($bill);

        AuditLogger::record($payment->owner_ref, 'payment.recorded', $bill->organization_id, $payment->recorded_by, Payment::class, $payment->id, [
            'bill_id' => $bill->id,
            'amount_minor' => $verifiedAmount,
            'provider' => $payment->gateway_provider,
            'reference' => $reference,
            'channel' => 'gateway',
        ]);

        return $payment->fresh(['bill']);
    }

    public function void(Bill $bill, string $ownerRef, ?string $actorRef = null): Bill
    {
        if ($bill->status === Bill::STATUS_PAID) {
            throw new InvalidArgumentException('Paid bills cannot be voided.');
        }

        $bill->update(['status' => Bill::STATUS_VOID, 'balance_minor' => 0]);
        AuditLogger::record($ownerRef, 'bill.voided', $bill->organization_id, $actorRef, Bill::class, $bill->id);

        return $bill->fresh();
    }

    protected function recalculate(Bill $bill): void
    {
        $bill->refresh();
        $subtotal = (int) $bill->lineItems()->sum('total_minor');
        $total = max(0, $subtotal - $bill->discount_minor + $bill->tax_minor);
        $paid = $this->paidSum($bill);
        $balance = max(0, $total - $paid);

        $status = $bill->status;
        if ($status !== Bill::STATUS_VOID) {
            if ($paid === 0) {
                $status = Bill::STATUS_OPEN;
            } elseif ($balance > 0) {
                $status = Bill::STATUS_PARTIAL;
            } else {
                $status = Bill::STATUS_PAID;
            }
        }

        $bill->update([
            'subtotal_minor' => $subtotal,
            'total_minor' => $total,
            'amount_paid_minor' => $paid,
            'balance_minor' => $balance,
            'status' => $status,
        ]);
    }

    protected function syncPaymentTotals(Bill $bill): void
    {
        $bill->refresh();
        $paid = $this->paidSum($bill);
        $balance = max(0, $bill->total_minor - $paid);
        $wasPaid = $bill->status === Bill::STATUS_PAID;

        $status = Bill::STATUS_OPEN;
        if ($paid > 0 && $balance > 0) {
            $status = Bill::STATUS_PARTIAL;
        } elseif ($balance === 0 && $bill->total_minor > 0) {
            $status = Bill::STATUS_PAID;
        } elseif ($paid >= $bill->total_minor) {
            $status = Bill::STATUS_PAID;
        }

        if ($bill->status === Bill::STATUS_VOID) {
            $status = Bill::STATUS_VOID;
        }

        $bill->update([
            'amount_paid_minor' => $paid,
            'balance_minor' => $balance,
            'status' => $status,
        ]);

        if (! $wasPaid && $status === Bill::STATUS_PAID) {
            $organization = Organization::query()->find($bill->organization_id);
            if ($organization) {
                $this->queueBridge->complete($organization, $bill);
                $visit = $bill->visit;
                if ($visit) {
                    $this->workflowCompletion->complete(
                        $organization,
                        $visit,
                        CareQueueContexts::BILLING,
                        (string) $bill->owner_ref,
                        $bill->payments()->latest('id')->value('recorded_by'),
                    );
                }
            }
        }
    }

    protected function paidSum(Bill $bill): int
    {
        return (int) $bill->payments()
            ->where('status', Payment::STATUS_PAID)
            ->sum('amount_minor');
    }

    protected function assertEditable(Bill $bill): void
    {
        if (in_array($bill->status, [Bill::STATUS_PAID, Bill::STATUS_VOID], true)) {
            throw new InvalidArgumentException('Bill cannot be modified.');
        }
    }
}
