<?php

namespace Modules\ClinicCore\Entities\Services;

use App\PurchaseLine;
use Illuminate\Support\Facades\DB;
use Modules\ClinicCore\Entities\ClcControlledMovement;
use Modules\ClinicCore\Entities\ClcMedicine;
use Modules\ClinicCore\Entities\ClcPrescriptionItem;

/**
 * Bridge between the pharmacy catalogue (clc_medicines) and the ERP inventory
 * (purchase_lines) for the DISPENSING clinical act.
 *
 *   - fefoSuggestions()  — sorted list of available batches for an item
 *                          (first-expired first-out), with qty_available +
 *                          exp_date for the batch-guidance UI. DISPLAY-ONLY.
 *   - dispense()          — stamps the prescription item's dispense fields and,
 *                          for a controlled medicine, writes ONE witnessed
 *                          `dispense` row into the controlled register.
 *
 * ── NO DOUBLE STOCK DEDUCTION (SIFA P4 architecture rule) ──────────────────
 * Dispensing is a CLINICAL act — it never touches ERP stock. Inventory is
 * decremented later by MedBilling's charge-capture → bill sell: every dispensed
 * Rx item (dispensed_at set) is captured by ChargeCaptureService and billed as
 * an ERP sell line, and the sell is what decrements variation_location_details
 * (and, via ControlledStockSyncService, writes the `dispense_unverified` OUT
 * mirror keyed to the sell). This service deliberately performs NO
 * decreaseProductQuantity / quantity_sold write — doing so here would deduct the
 * same units twice. The controlled `dispense` row written here is the witnessed
 * clinical record of the hand-over; the sell's `dispense_unverified` mirror is a
 * separate, reversible stock-movement row (distinct source_type), so the two do
 * not collide in the register.
 */
class ClcMedicineDispenseService
{
    /**
     * Return purchase_lines that still have qty for the medicine's linked ERP
     * product, sorted FEFO (earliest exp_date first; nulls last). Display-only
     * batch guidance — nothing is written from this call.
     *
     * @return \Illuminate\Support\Collection<int, object>
     */
    public function fefoSuggestions(ClcMedicine $medicine, ?int $locationId = null)
    {
        if (empty($medicine->product_id)) {
            return collect();
        }

        $rows = PurchaseLine::query()
            ->where('product_id', $medicine->product_id)
            ->whereColumn('quantity', '>', 'quantity_sold')      // still has stock
            ->orderByRaw('CASE WHEN exp_date IS NULL THEN 1 ELSE 0 END, exp_date ASC')
            ->orderBy('id')
            ->get([
                'id', 'product_id', 'variation_id', 'transaction_id',
                'lot_number', 'mfg_date', 'exp_date',
                'quantity', 'quantity_sold', 'quantity_returned',
                'purchase_price', 'purchase_price_inc_tax',
            ]);

        return $rows->map(function ($r) {
            $available = (float) $r->quantity
                       - (float) $r->quantity_sold
                       - (float) ($r->quantity_returned ?? 0);
            return (object) [
                'purchase_line_id' => $r->id,
                'variation_id'     => $r->variation_id,
                'lot_number'       => $r->lot_number,
                'mfg_date'         => $r->mfg_date,
                'exp_date'         => $r->exp_date,
                'qty_available'    => max(0.0, $available),
            ];
        })->filter(fn($x) => $x->qty_available > 0)->values();
    }

    /**
     * Dispense `$qty` of the prescription item. Stamps the item's clinical
     * dispense fields (dispensed_at/by, dispense_qty, location, chosen batch for
     * traceability) and, for a controlled medicine, writes a witnessed
     * `dispense` row into the controlled register. NEVER decrements ERP stock —
     * see the class docblock (MedBilling's capture→bill sell does that).
     *
     * @param array{witness_user_id?:int|null, batch_no?:string|null, expiry_date?:string|null} $opts
     * @return ClcPrescriptionItem the updated item
     * @throws \InvalidArgumentException on validation errors (callers map to 422)
     */
    public function dispense(
        ClcPrescriptionItem $item,
        ?int $purchaseLineId,
        float $qty,
        int $locationId,
        ?int $userId = null,
        array $opts = []
    ): ClcPrescriptionItem {
        if ($qty <= 0) {
            throw new \InvalidArgumentException('Dispense quantity must be positive.');
        }

        $medicine = $item->medicine_id ? ClcMedicine::find($item->medicine_id) : null;

        // Resolve the chosen batch for traceability (display FEFO guidance). A
        // catalogue-linked medicine may point at a purchase_line; free-text items
        // simply stamp the qty with no batch.
        $line = null;
        $batchNo = $opts['batch_no'] ?? null;
        $expiry  = $opts['expiry_date'] ?? null;
        if ($purchaseLineId && $medicine && !empty($medicine->product_id)) {
            $line = PurchaseLine::where('id', $purchaseLineId)
                ->where('product_id', $medicine->product_id)
                ->first();
            if (!$line) {
                throw new \InvalidArgumentException('That batch does not match the prescribed medicine.');
            }
            $batchNo = $line->lot_number ?: $batchNo;
            $expiry  = $line->exp_date ?: $expiry;
        }

        // Controlled dispenses require a witness (double verification).
        $isControlled = $medicine && $medicine->is_controlled;
        $witnessUserId = $opts['witness_user_id'] ?? null;
        if ($isControlled && empty($witnessUserId)) {
            throw new \InvalidArgumentException('A witness is required to dispense a controlled medicine.');
        }

        return DB::transaction(function () use ($item, $line, $qty, $locationId, $userId, $medicine, $isControlled, $witnessUserId, $batchNo, $expiry) {
            $item->update([
                'purchase_line_id' => $line?->id,
                'dispensed_qty'    => $qty,
                'dispensed_at'     => now(),
                'dispensed_by'     => $userId,
                'location_id'      => $locationId,
            ]);

            // Controlled register: witnessed `dispense` row — the clinical record
            // of the hand-over. This is NOT a stock decrement; it is an insert-only
            // audit movement in clc_controlled_stock_movements. Fail-soft so the
            // dispense itself never breaks on a register write hiccup.
            if ($isControlled) {
                try {
                    $this->writeControlledDispense($item, $medicine, $qty, $witnessUserId, $userId, $batchNo, $expiry);
                } catch (\Throwable $e) {
                    \Log::warning('ClinicCore controlled dispense-register write failed for item ' . $item->id . ': ' . $e->getMessage());
                }
            }

            return $item->fresh();
        });
    }

    /**
     * Insert one witnessed `dispense` OUT movement for a controlled medicine.
     * Mirrors the register semantics used by ControlledStockSyncService
     * (running balance per medicine+batch, per-business serial). Idempotent per
     * (business, source_type=clc_dispense, source_id=prescription_item_id).
     */
    private function writeControlledDispense(
        ClcPrescriptionItem $item,
        ClcMedicine $medicine,
        float $qty,
        int $witnessUserId,
        ?int $userId,
        ?string $batchNo,
        $expiry
    ): void {
        $businessId = (int) $item->prescription->business_id;
        $sourceType = 'clc_dispense';

        $exists = ClcControlledMovement::where('business_id', $businessId)
            ->where('source_type', $sourceType)
            ->where('source_id', $item->id)
            ->exists();
        if ($exists) {
            return;
        }

        $prevBalance = (float) (ClcControlledMovement::where('business_id', $businessId)
            ->where('medicine_id', $medicine->id)
            ->where('batch_no', $batchNo)
            ->orderByDesc('id')->value('balance_after') ?? 0);

        $rx = $item->prescription;

        ClcControlledMovement::create([
            'business_id'     => $businessId,
            'serial_no'       => ClcControlledMovement::nextSerial($businessId),
            'medicine_id'     => $medicine->id,
            'direction'       => 'out',
            'qty'             => $qty,
            'unit'            => $medicine->unit,
            'batch_no'        => $batchNo,
            'expiry_date'     => $expiry,
            'balance_after'   => $prevBalance - $qty,
            'reason_code'     => 'dispense',
            'patient_id'      => $rx->patient_id,
            'contact_id'      => $rx->contact_id,
            'appointment_id'  => $rx->appointment_id,
            'prescription_id' => $rx->id,
            'doctor_id'       => $rx->doctor_id,
            'witness_user_id' => $witnessUserId,
            'source_type'     => $sourceType,
            'source_id'       => $item->id,
            'notes'           => 'Pharmacy dispense of ' . ($item->medicine_name ?: $medicine->name) . ' (Rx ' . ($rx->ref_no ?: $rx->id) . ')',
            'created_by'      => $userId,
        ]);
    }
}
