<?php

namespace Modules\ClinicCore\Entities\Services;

use App\Transaction;
use Modules\ClinicCore\Entities\ClcControlledMovement;
use Modules\ClinicCore\Entities\ClcMedicine;

/**
 * Mirrors the IN / OUT for any controlled-substance product back into the
 * controlled-drug register whenever a Transaction is saved. Idempotent —
 * the listener can fire many times for the same transaction; rows are
 * keyed on (business_id, source_type, source_id, source_line_id).
 *
 * Phase 2b minimum:
 *  - purchase / opening_stock    → direction=in, reason=receive
 *  - sell                        → direction=out, reason=dispense_unverified (manager promotes later)
 *  - purchase_return             → direction=out, reason=return
 *  - sell_return                 → direction=in,  reason=return
 *
 * Voids / draft transactions are skipped. Lines that don't reference a
 * controlled medicine are skipped silently.
 */
class ControlledStockSyncService
{
    /** Transaction types we mirror. Anything else is ignored. */
    private const HANDLED = [
        'purchase'        => ['in',  'receive'],
        'opening_stock'   => ['in',  'receive'],
        'purchase_return' => ['out', 'return'],
        'sell'            => ['out', 'dispense_unverified'],
        'sell_return'     => ['in',  'return'],
    ];

    /** Statuses considered "live" (worth mirroring). */
    private const LIVE_STATUSES = ['final', 'received', 'pending'];

    public static function syncTransaction(Transaction $tx): void
    {
        if (!isset(self::HANDLED[$tx->type])) return;

        // Detect a void: was live → no longer live (cancelled/draft/etc).
        // Reverse every previously-synced row and exit; the new status
        // doesn't represent a real stock movement any more.
        $wasLive = in_array($tx->getOriginal('status'), self::LIVE_STATUSES, true);
        $isLive  = in_array($tx->status, self::LIVE_STATUSES, true);
        if ($wasLive && !$isLive) {
            self::reverseTransaction($tx, 'void');
            return;
        }
        if (!$isLive) return;

        try {
            [$direction, $reason] = self::HANDLED[$tx->type];

            $isSell = in_array($tx->type, ['sell', 'sell_return'], true);
            // Always re-query the lines (don't use the cached relation) — the
            // observer fires on the very first $tx->save() before UltimatePOS
            // has inserted any line rows, so we have to re-fetch on every save.
            $lines = $isSell
                ? $tx->sell_lines()->with('lot_details')->get()
                : $tx->purchase_lines()->get();

            if ($lines->isEmpty()) return;

            // Pre-fetch the controlled medicines referenced by this transaction
            // in one query — avoids a per-line lookup.
            $productIds = $lines->pluck('product_id')->filter()->unique()->all();
            $medByProduct = ClcMedicine::where('business_id', $tx->business_id)
                ->controlled()
                ->whereIn('product_id', $productIds)
                ->get()->keyBy('product_id');

            if ($medByProduct->isEmpty()) return;

            $sourceType = 'transaction:' . $tx->type;

            foreach ($lines as $line) {
                $med = $medByProduct->get($line->product_id);
                if (!$med) continue;

                // Resolve batch + expiry. Purchase lines carry them directly;
                // sell lines point back via lot_no_line_id → purchase_lines.
                [$batch, $expiry] = self::resolveBatch($line, $isSell);
                $qty = abs((float) $line->quantity);
                if ($qty <= 0) continue;

                self::upsertMovement($tx, $line, $med, $direction, $reason, $qty, $batch, $expiry, $sourceType);
            }
        } catch (\Throwable $e) {
            // The hot path of a transaction save must never break because of
            // the controlled register — log and move on.
            \Log::warning('ClinicCore controlled-sync failed for tx ' . $tx->id . ': ' . $e->getMessage());
        }
    }

    /**
     * Write reversing entries for every previously-synced row for this
     * transaction. Idempotent — uses a "reversal" source_type variant so the
     * unique index guarantees we never double-reverse a row.
     *
     * @param string $cause "void" | "delete"
     */
    public static function reverseTransaction(Transaction $tx, string $cause = 'void'): void
    {
        try {
            $originals = ClcControlledMovement::where('business_id', $tx->business_id)
                ->where('source_id', $tx->id)
                ->where('source_type', 'like', 'transaction:%')
                ->where('source_type', 'not like', '%:reversal')
                ->get();
            if ($originals->isEmpty()) return;

            foreach ($originals as $orig) {
                $reversalType = $orig->source_type . ':reversal';
                // Skip if already reversed.
                $exists = ClcControlledMovement::where('business_id', $tx->business_id)
                    ->where('source_type', $reversalType)
                    ->where('source_id', $orig->source_id)
                    ->where('source_line_id', $orig->source_line_id)
                    ->exists();
                if ($exists) continue;

                $invDir = match ($orig->direction) {
                    'in' => 'out',
                    'out' => 'in',
                    default => 'adjust',
                };
                $prevBalance = ClcControlledMovement::where('business_id', $tx->business_id)
                    ->where('medicine_id', $orig->medicine_id)
                    ->where('batch_no', $orig->batch_no)
                    ->orderByDesc('id')->value('balance_after') ?? 0;
                $qty = (float) $orig->qty;
                $newBalance = $invDir === 'in' ? $prevBalance + $qty : $prevBalance - $qty;

                ClcControlledMovement::create([
                    'business_id'     => $tx->business_id,
                    'serial_no'       => ClcControlledMovement::nextSerial((int) $tx->business_id),
                    'medicine_id'     => $orig->medicine_id,
                    'direction'       => $invDir,
                    'qty'             => $qty,
                    'unit'            => $orig->unit,
                    'batch_no'        => $orig->batch_no,
                    'expiry_date'     => $orig->expiry_date,
                    'balance_after'   => $newBalance,
                    'reason_code'     => 'correction',
                    'contact_id'      => $orig->contact_id,
                    'source_type'     => $reversalType,
                    'source_id'       => $orig->source_id,
                    'source_line_id'  => $orig->source_line_id,
                    'notes'           => ucfirst($cause) . " of {$orig->source_type} → reverses {$orig->serial_no}",
                    'created_by'      => auth()->id() ?: $orig->created_by,
                ]);
            }
        } catch (\Throwable $e) {
            \Log::warning('ClinicCore controlled-reversal failed for tx ' . $tx->id . ': ' . $e->getMessage());
        }
    }

    /** Recompute and insert (or update) the matching register row. */
    private static function upsertMovement(
        Transaction $tx, $line, ClcMedicine $med,
        string $direction, string $reason, float $qty,
        ?string $batch, $expiry, string $sourceType
    ): void {
        $existing = ClcControlledMovement::where([
                'business_id'    => $tx->business_id,
                'source_type'    => $sourceType,
                'source_id'      => $tx->id,
                'source_line_id' => $line->id,
            ])->first();

        // Running balance is computed from prior history for (medicine, batch),
        // excluding the row we're about to upsert (it shouldn't double-count).
        $prevBalance = ClcControlledMovement::where('business_id', $tx->business_id)
            ->where('medicine_id', $med->id)
            ->where('batch_no', $batch)
            ->when($existing, fn($q) => $q->where('id', '<>', $existing->id))
            ->orderByDesc('id')->value('balance_after') ?? 0;
        $prevBalance = (float) $prevBalance;
        $balance = $direction === 'in' ? $prevBalance + $qty : $prevBalance - $qty;

        $contactId = $tx->contact_id ?? null;

        $payload = [
            'business_id'   => $tx->business_id,
            'serial_no'     => $existing ? $existing->serial_no : ClcControlledMovement::nextSerial((int) $tx->business_id),
            'medicine_id'   => $med->id,
            'direction'     => $direction,
            'qty'           => $qty,
            'unit'          => $med->unit,
            'batch_no'      => $batch,
            'expiry_date'   => $expiry,
            'balance_after' => $balance,
            'reason_code'   => $reason,
            'contact_id'    => $contactId,
            'source_type'   => $sourceType,
            'source_id'     => $tx->id,
            'source_line_id'=> $line->id,
            'notes'         => 'Auto-synced from ' . $tx->type . ' #' . ($tx->invoice_no ?: $tx->ref_no ?: $tx->id),
            'created_by'    => $tx->created_by,
        ];

        if ($existing) {
            $existing->update($payload);
        } else {
            ClcControlledMovement::create($payload);
        }
    }

    /** Resolve [batch_no, expiry_date] for a purchase or sell line. */
    private static function resolveBatch($line, bool $isSell): array
    {
        if ($isSell) {
            $lot = $line->lot_details ?? null;
            return [
                $lot ? ($lot->lot_number ?: null) : null,
                $lot ? ($lot->exp_date ?: null)   : null,
            ];
        }
        return [
            $line->lot_number ?: null,
            $line->exp_date   ?: null,
        ];
    }
}
