<?php

namespace Modules\ClinicCore\Entities\Services;

use App\Transaction;
use App\Utils\ProductUtil;
use App\Utils\TransactionUtil;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

/**
 * SIFA P4-T2 — thin inventory-consumption bridge.
 *
 * Some clinical events consume physical stock but NEVER reach billing (e.g. a
 * vaccine dose given as part of a bundled visit, ward supplies, in-house
 * reagents). For those, this service records a proper ERP **stock adjustment**
 * (type=stock_adjustment) through the SAME util pathway StockAdjustmentController
 * uses — decreaseProductQuantity → Transaction::create → stock_adjustment_lines
 * → mapPurchaseSell — so accounting, FEFO purchase-sell mapping and the stock
 * ledger all stay correct. It NEVER writes a raw variation_location_details
 * decrement.
 *
 * IMPORTANT — this is NOT the pharmacy-dispensing path. Dispensed Rx items ride
 * MedBilling's charge-capture → bill sell (which decrements stock there). Using
 * this bridge for a billable event would double-deduct. Wire it ONLY for events
 * that never become a sell line.
 *
 * Every call is fail-soft: an unresolvable product/location/variation logs a
 * warning and returns null (the clinical action still succeeds).
 *
 * Usage:
 *   app(ClinicalConsumptionService::class)->consume(
 *       $businessId, $locationId, $productId, 1, 'vaccination',
 *       ['vaccination_id' => $vax->id]
 *   );  // returns the stock_adjustment transactions.id, or null
 */
class ClinicalConsumptionService
{
    protected ProductUtil $productUtil;

    protected TransactionUtil $transactionUtil;

    public function __construct()
    {
        $this->productUtil = app(ProductUtil::class);
        $this->transactionUtil = app(TransactionUtil::class);
    }

    /**
     * Consume `$qty` of `$productId` at `$locationId` as an ERP stock adjustment.
     *
     * @param  array<string,mixed>  $refs  reference ids echoed into additional_notes
     * @return int|null  the created stock_adjustment transactions.id, or null (fail-soft)
     */
    public function consume(
        int $businessId,
        int $locationId,
        int $productId,
        float $qty,
        string $reason,
        array $refs = []
    ): ?int {
        if ($businessId <= 0 || $locationId <= 0 || $productId <= 0 || $qty <= 0) {
            return null;
        }

        try {
            $variation = $this->resolvePrimaryVariation($businessId, $productId);
            if (! $variation) {
                \Log::warning("ClinicalConsumptionService: no variation for product {$productId} (business {$businessId}); skipping {$reason} consumption.");
                return null;
            }

            $notes = $this->buildNotes($reason, $refs);

            return DB::transaction(function () use ($businessId, $locationId, $productId, $variation, $qty, $notes) {
                $userId = auth()->check() ? (int) auth()->id() : null;

                // Same reference-number + type=stock_adjustment recipe as
                // StockAdjustmentController::store.
                $refCount = $this->productUtil->setAndGetReferenceCount('stock_adjustment');
                $refNo = $this->productUtil->generateReferenceNumber('stock_adjustment', $refCount);

                $input = [
                    'business_id'            => $businessId,
                    'location_id'            => $locationId,
                    'type'                   => 'stock_adjustment',
                    'adjustment_type'        => 'normal',
                    'transaction_date'       => now()->toDateTimeString(),
                    'ref_no'                 => $refNo,
                    'total_amount_recovered' => 0,
                    'final_total'            => 0,
                    'additional_notes'       => $notes,
                    'created_by'             => $userId,
                ];

                $unitPrice = (float) ($variation->dpp_inc_tax ?? $variation->default_purchase_price ?? 0);

                // Decrease available quantity via the util (never a raw decrement).
                $this->productUtil->decreaseProductQuantity(
                    $productId,
                    $variation->id,
                    $locationId,
                    $qty
                );

                $adjustment = Transaction::create($input);
                $adjustment->stock_adjustment_lines()->createMany([[
                    'product_id'   => $productId,
                    'variation_id' => $variation->id,
                    'quantity'     => $qty,
                    'unit_price'   => $unitPrice,
                ]]);

                // FIFO/LIFO purchase-adjustment mapping (fail-soft — mirrors the
                // controller, but never breaks the clinical action).
                try {
                    $business = [
                        'id'                => $businessId,
                        'accounting_method' => session('business.accounting_method', 'fifo'),
                        'location_id'       => $locationId,
                    ];
                    $this->transactionUtil->mapPurchaseSell(
                        $business,
                        $adjustment->stock_adjustment_lines,
                        'stock_adjustment'
                    );
                } catch (\Throwable $e) {
                    \Log::warning('ClinicalConsumptionService mapPurchaseSell skipped: ' . $e->getMessage());
                }

                return (int) $adjustment->id;
            });
        } catch (\Throwable $e) {
            \Log::warning('ClinicalConsumptionService consume failed (' . $reason . '): ' . $e->getMessage());
            return null;
        }
    }

    /** Human-readable additional_notes carrying the reason + reference ids. */
    protected function buildNotes(string $reason, array $refs): string
    {
        $parts = ['ClinicCore consumption: ' . $reason];
        foreach ($refs as $k => $v) {
            $parts[] = $k . '=' . (is_scalar($v) ? $v : json_encode($v));
        }
        return implode(' | ', $parts);
    }

    /**
     * Resolve the primary (first, non-deleted) variation for a business product.
     * Returns an object with id + purchase-price fields, or null.
     */
    protected function resolvePrimaryVariation(int $businessId, int $productId)
    {
        if (! Schema::hasTable('variations')) {
            return null;
        }

        // Confirm the product belongs to the business (defensive; core products
        // are business-scoped).
        $belongs = DB::table('products')->where('id', $productId)->where('business_id', $businessId)->exists();
        if (! $belongs) {
            return null;
        }

        return DB::table('variations')
            ->where('product_id', $productId)
            ->whereNull('deleted_at')
            ->orderBy('id')
            ->first(['id', 'default_purchase_price', 'dpp_inc_tax']);
    }
}
