<?php

namespace App\Services;

use App\Exceptions\InsufficientStockException;
use App\Models\BranchStock;
use App\Models\Catalog\Product;
use App\Models\Lookups\MovementType;
use App\Models\Scopes\BranchScope;
use App\Models\StockMovement;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;

/**
 * The Golden Rule (coding_principles.md §1.2): every stock change goes through
 * this service. Direct writes to branch_stock.quantity are forbidden.
 *
 * Mechanics:
 *   1. DB::transaction()
 *   2. SELECT … FOR UPDATE on branch_stock(branch, product) (creates row if missing)
 *   3. Compute quantity_after; throw InsufficientStockException if < 0
 *   4. Update branch_stock.quantity
 *   5. Insert stock_movements
 *   6. Optional: emit a structured audit log entry
 *
 * See: docs/workflows/stock-movement.md
 */
class StockMovementService
{
    /**
     * Record a stock movement.
     *
     * @param  string  $movementTypeCode  One of MovementType::* code constants
     * @param  float  $quantityChange  Signed quantity. Positive = IN, negative = OUT.
     * @param  float|null  $unitCost  Optional cost-per-unit at time of movement (for valuation)
     * @param  string|Model|null  $referenceType  FQCN string or a Model to derive from
     * @param  string|null  $referenceNumber  Document number (INV-…, RTN-…, DOC-…) for the Bin Card UI
     * @param  int|null  $stockBatchId  Pin the lot: required for serial-linked movements, and the lot ledger is the source of truth for on-hand stock
     *
     * @throws InsufficientStockException
     */
    public function record(
        int $branchId,
        int $productId,
        string $movementTypeCode,
        float $quantityChange,
        ?float $unitCost = null,
        string|Model|null $referenceType = null,
        ?int $referenceId = null,
        ?string $referenceNumber = null,
        ?string $note = null,
        ?string $productName = null,
        ?string $branchName = null,
        ?int $stockBatchId = null,
    ): StockMovement {
        $movementTypeId = MovementType::idOf($movementTypeCode);
        if ($movementTypeId === null) {
            throw new \InvalidArgumentException("Unknown movement type code: {$movementTypeCode}");
        }

        // Allow callers to pass a Model directly; derive class + id
        if ($referenceType instanceof Model) {
            $referenceId ??= $referenceType->getKey();
            $referenceType = $referenceType::class;
        }

        return DB::transaction(function () use (
            $branchId, $productId, $movementTypeId, $quantityChange,
            $unitCost, $referenceType, $referenceId, $referenceNumber, $note,
            $productName, $branchName, $stockBatchId
        ) {
            // branch_stock(branch, product) is the single source of truth for
            // available stock. Availability is checked, and the running
            // quantity is computed, per branch — never against a global
            // product total. $branchId is an explicit business parameter
            // that may differ from the acting user's current branch context
            // (e.g. a stock transfer's destination branch), so this must
            // bypass BranchScope rather than silently AND it in.
            $stock = BranchStock::query()
                ->withoutGlobalScope(BranchScope::class)
                ->where('branch_id', $branchId)
                ->where('product_id', $productId)
                ->lockForUpdate()
                ->first();

            $quantityBefore = $stock ? (float) $stock->quantity : 0.0;
            $quantityAfter = $quantityBefore + $quantityChange;

            if ($quantityAfter < 0) {
                throw new InsufficientStockException(
                    branchId: $branchId,
                    productId: $productId,
                    available: $quantityBefore,
                    requested: abs($quantityChange),
                );
            }

            if ($stock) {
                $stock->update(['quantity' => $quantityAfter]);
            } else {
                BranchStock::create([
                    'branch_id' => $branchId,
                    'product_id' => $productId,
                    'quantity' => $quantityAfter,
                ]);
            }

            // Denormalized snapshot of the product's base unit at the time of the
            // movement (plan decision A7) — avoids a JOIN for offline exports.
            $unitId = Product::query()->whereKey($productId)->value('base_unit_id');

            $movement = StockMovement::create([
                'branch_id' => $branchId,
                'product_id' => $productId,
                'stock_batch_id' => $stockBatchId,
                'movement_type_id' => $movementTypeId,
                'reference_type' => is_string($referenceType) ? $referenceType : null,
                'reference_id' => $referenceId,
                'reference_number' => $referenceNumber,
                'quantity_before' => $quantityBefore,
                'quantity_change' => $quantityChange,
                'quantity_after' => $quantityAfter,
                'unit_id' => $unitId,
                'unit_cost' => $unitCost,
                'note' => $note,
                'created_by' => Auth::id(),
                'created_at' => now(),
            ]);

            // Emit structured audit event (best-effort; bind off when AuditLog isn't available)
            AuditLog::record('stock.movement', $movement, [
                'product' => $productName ?? "Product#{$productId}",
                'branch' => $branchName ?? "Branch#{$branchId}",
                'movement_type' => $movement->movementType?->name ?? $movement->movementType?->code ?? $movementTypeCode,
                'movement_code' => $movement->movementType?->code ?? $movementTypeCode,
                'quantity_before' => $quantityBefore,
                'quantity_change' => $quantityChange,
                'quantity_after' => $quantityAfter,
                'reference_number' => $referenceNumber,
                'note' => $note,
            ]);

            return $movement;
        });
    }
}
