<?php

namespace App\Services\Inventory;

use App\Models\Item;
use App\Models\StockMovement;
use App\Scopes\StoreScope;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class StockMovementService
{
    public function increase(int|Item $item, int $quantity, array $context = []): StockMovement
    {
        return $this->move($item, abs($quantity), array_merge(['movement_type' => 'purchase'], $context));
    }

    public function decrease(int|Item $item, int $quantity, array $context = []): StockMovement
    {
        return $this->move($item, -abs($quantity), array_merge(['movement_type' => 'sale'], $context));
    }

    public function reverseSale(int|Item $item, int $quantity, array $context = []): StockMovement
    {
        return $this->move($item, abs($quantity), array_merge(['movement_type' => 'return'], $context));
    }

    public function move(int|Item $item, int $quantityDelta, array $context = []): StockMovement
    {
        if ($quantityDelta === 0) {
            throw ValidationException::withMessages([
                'quantity' => translate('Stock movement quantity can not be zero.'),
            ]);
        }

        return DB::transaction(function () use ($item, $quantityDelta, $context) {
            $itemId = $item instanceof Item ? $item->id : $item;
            $movementType = $context['movement_type'] ?? ($quantityDelta > 0 ? 'increase' : 'decrease');
            $variationType = $context['variation_type'] ?? null;
            $idempotencyKey = $context['idempotency_key'] ?? $this->idempotencyKey(
                $itemId,
                $variationType,
                $movementType,
                $quantityDelta,
                $context
            );

            $lockedItem = Item::withoutGlobalScopes()
                ->withoutGlobalScope(StoreScope::class)
                ->lockForUpdate()
                ->findOrFail($itemId);

            if ($idempotencyKey) {
                $existingMovement = StockMovement::where('idempotency_key', $idempotencyKey)
                    ->lockForUpdate()
                    ->first();
                if ($existingMovement) {
                    return $existingMovement;
                }
            }

            $stockBefore = (int) ($lockedItem->stock ?? 0);
            $stockAfter = $stockBefore + $quantityDelta;

            if ($stockAfter < 0) {
                throw ValidationException::withMessages([
                    'stock' => translate('Insufficient stock for') . ' ' . $lockedItem->name,
                ]);
            }

            [$encodedVariations, $variationStockBefore, $variationStockAfter] = $this->applyVariationDelta(
                $lockedItem,
                $variationType,
                $quantityDelta
            );

            $lockedItem->stock = $stockAfter;
            if ($encodedVariations !== null) {
                $lockedItem->variations = $encodedVariations;
            }
            $lockedItem->save();

            return StockMovement::create([
                'item_id' => $lockedItem->id,
                'variation_type' => $variationType,
                'quantity_delta' => $quantityDelta,
                'qty_in' => $quantityDelta > 0 ? $quantityDelta : 0,
                'qty_out' => $quantityDelta < 0 ? abs($quantityDelta) : 0,
                'stock_before' => $stockBefore,
                'stock_after' => $stockAfter,
                'before_qty' => $stockBefore,
                'after_qty' => $stockAfter,
                'variation_stock_before' => $variationStockBefore,
                'variation_stock_after' => $variationStockAfter,
                'unit_cost' => $context['unit_cost'] ?? 0,
                'movement_type' => $movementType,
                'source_type' => $context['source_type'] ?? 'admin/manual',
                'source_id' => $context['source_id'] ?? null,
                'source_reference' => $context['source_reference'] ?? null,
                'sale_channel' => $context['sale_channel'] ?? 'admin/manual',
                'created_by_admin_id' => $context['created_by_admin_id'] ?? null,
                'created_by' => $context['created_by'] ?? ($context['created_by_admin_id'] ?? null),
                'voucher_id' => $context['voucher_id'] ?? null,
                'note' => $context['note'] ?? null,
                'idempotency_key' => $idempotencyKey,
            ]);
        });
    }

    private function idempotencyKey(int $itemId, ?string $variationType, string $movementType, int $quantityDelta, array $context): ?string
    {
        $sourceType = $context['source_type'] ?? null;
        $sourceId = $context['source_id'] ?? null;
        $sourceReference = $context['source_reference'] ?? null;

        if (blank($sourceType) || (blank($sourceId) && blank($sourceReference))) {
            return null;
        }

        return implode(':', [
            $sourceType,
            $sourceId ?: $sourceReference,
            $itemId,
            $variationType ?: '',
            $movementType,
            $quantityDelta,
        ]);
    }

    private function applyVariationDelta(Item $item, ?string $variationType, int $quantityDelta): array
    {
        if (blank($variationType)) {
            return [null, null, null];
        }

        $rawVariations = $item->variations;
        $variations = is_array($rawVariations) ? $rawVariations : json_decode($rawVariations ?: '[]', true);
        if (! is_array($variations) || count($variations) === 0) {
            return [null, null, null];
        }

        foreach ($variations as $index => $variation) {
            if (! is_array($variation) || ($variation['type'] ?? null) !== $variationType) {
                continue;
            }

            $before = (int) ($variation['stock'] ?? 0);
            $after = $before + $quantityDelta;
            if ($after < 0) {
                throw ValidationException::withMessages([
                    'stock' => translate('Insufficient variation stock for') . ' ' . $item->name,
                ]);
            }

            $variations[$index]['stock'] = $after;
            return [json_encode($variations), $before, $after];
        }

        throw ValidationException::withMessages([
            'variation_type' => translate('Selected variation was not found for the chosen product.'),
        ]);
    }
}
