<?php

namespace App\Domain\Inventory\Services;

use App\Domain\Inventory\Enums\MovementType;
use App\Domain\Inventory\Enums\StockCountItemStatus;
use App\Domain\Inventory\Enums\StockCountStatus;
use App\Domain\Inventory\Events\StockCountCompleted;
use App\Domain\Inventory\Models\InventoryLevel;
use App\Domain\Inventory\Models\Product;
use App\Domain\Inventory\Models\StockCount;
use App\Domain\Inventory\Models\StockCountItem;
use App\Domain\Inventory\Models\Warehouse;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Exceptions\InvalidStatusTransitionException;
use App\Models\User;
use Illuminate\Support\Facades\DB;

class StockCountService
{
    public function __construct(
        private readonly InventoryService $inventoryService,
    ) {}

    /**
     * Create a stock count session with items for specified products.
     * Snapshots current system_quantity for each product at creation time.
     */
    public function create(int $warehouseId, array $productIds, User $creator): StockCount
    {
        return DB::transaction(function () use ($warehouseId, $productIds, $creator) {
            $warehouse = Warehouse::findOrFail($warehouseId);

            $stockCount = StockCount::create([
                'academy_id' => $warehouse->academy_id,
                // A count is of one warehouse's stock, so it belongs to that
                // warehouse's branch. Null (a shared, academy-wide warehouse)
                // falls through to the creating hook.
                'branch_id' => $warehouse->branch_id,
                'warehouse_id' => $warehouseId,
                'count_number' => $this->generateCountNumber($warehouse->academy_id),
                'status' => StockCountStatus::Open->value,
                'started_at' => now(),
                'total_items' => count($productIds),
                'total_discrepancies' => 0,
                'total_variance_value' => 0,
                'created_by' => $creator->id,
                'metadata' => [],
            ]);

            foreach ($productIds as $productId) {
                // Snapshot current system quantity.
                //
                // Read the warehouse's row, not the reader's branch's: this
                // number becomes system_quantity, the discrepancy is measured
                // against it, and applyAdjustments() then posts that difference
                // as a real inventory movement. A level hidden by the scope
                // would snapshot as 0 and the count would "correct" stock that
                // was never missing.
                $level = InventoryLevel::withoutBranchScope()
                    ->where('product_id', $productId)
                    ->where('warehouse_id', $warehouseId)
                    ->first();

                $systemQty = $level ? $level->quantity_on_hand : 0;

                StockCountItem::create([
                    'stock_count_id' => $stockCount->id,
                    'product_id' => $productId,
                    'system_quantity' => $systemQty,
                    'status' => StockCountItemStatus::Pending->value,
                ]);
            }

            return $stockCount->load('items.product');
        });
    }

    /**
     * Record a physical count for an item.
     */
    public function recordCount(StockCountItem $item, int $countedQty, User $user): StockCountItem
    {
        $stockCount = $item->stockCount;
        $status = $stockCount->status instanceof StockCountStatus
            ? $stockCount->status->value
            : $stockCount->status;

        if (! in_array($status, ['open', 'in_progress'])) {
            throw new DomainException('لا يمكن تسجيل عد في جلسة جرد مكتملة أو ملغاة');
        }

        return DB::transaction(function () use ($item, $countedQty, $user, $stockCount) {
            $discrepancy = $countedQty - $item->system_quantity;

            // Calculate discrepancy value using product's cost price
            $product = Product::findOrFail($item->product_id);
            $unitCost = $product->cost_price ?? 0;
            $discrepancyValue = $discrepancy * $unitCost;

            $item->update([
                'counted_quantity' => $countedQty,
                'discrepancy' => $discrepancy,
                'discrepancy_value' => $discrepancyValue,
                'status' => StockCountItemStatus::Counted->value,
                'counted_by' => $user->id,
                'counted_at' => now(),
            ]);

            // Update stock count to in_progress if it was open
            $currentStatus = $stockCount->status instanceof StockCountStatus
                ? $stockCount->status->value
                : $stockCount->status;

            if ($currentStatus === 'open') {
                $stockCount->update([
                    'status' => StockCountStatus::InProgress->value,
                ]);
            }

            // Update totals
            $this->recalculateTotals($stockCount);

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

    /**
     * Complete the stock count. All items must be counted.
     */
    public function completeCount(StockCount $stockCount, User $actor): StockCount
    {
        $status = $stockCount->status instanceof StockCountStatus
            ? $stockCount->status->value
            : $stockCount->status;

        if (! in_array($status, ['open', 'in_progress'])) {
            throw new InvalidStatusTransitionException(
                "لا يمكن إكمال جلسة جرد بحالة '{$status}'"
            );
        }

        // Check all items are counted
        $uncounted = $stockCount->items()
            ->where('status', StockCountItemStatus::Pending->value)
            ->count();

        if ($uncounted > 0) {
            throw new DomainException("لا يزال هناك {$uncounted} منتج لم يتم عده");
        }

        return DB::transaction(function () use ($stockCount, $actor) {
            $this->recalculateTotals($stockCount);

            $stockCount->update([
                'status' => StockCountStatus::Completed->value,
                'completed_at' => now(),
                'completed_by' => $actor->id,
            ]);

            StockCountCompleted::dispatch($stockCount, $actor);

            return $stockCount->fresh('items');
        });
    }

    /**
     * Apply inventory adjustments for all items with discrepancies.
     * Creates count_adjustment_up or count_adjustment_down movements.
     */
    public function applyAdjustments(StockCount $stockCount, User $actor): StockCount
    {
        $status = $stockCount->status instanceof StockCountStatus
            ? $stockCount->status->value
            : $stockCount->status;

        if ($status !== 'completed') {
            throw new DomainException('لا يمكن تطبيق التسويات إلا بعد إكمال الجرد');
        }

        $warehouse = Warehouse::findOrFail($stockCount->warehouse_id);

        return DB::transaction(function () use ($stockCount, $warehouse, $actor) {
            $items = $stockCount->items()
                ->whereNotNull('discrepancy')
                ->where('discrepancy', '!=', 0)
                ->where('status', '!=', StockCountItemStatus::Adjusted->value)
                ->get();

            foreach ($items as $item) {
                $product = Product::findOrFail($item->product_id);
                $discrepancy = $item->discrepancy;

                if ($discrepancy > 0) {
                    // System had less than physical — adjust up
                    $this->inventoryService->createMovement(
                        product: $product,
                        warehouse: $warehouse,
                        type: MovementType::CountAdjustmentUp,
                        quantity: $discrepancy,
                        actor: $actor,
                        reference: $stockCount,
                        reason: "تسوية جرد #{$stockCount->count_number} — زيادة",
                    );
                } else {
                    // System had more than physical — adjust down
                    $this->inventoryService->createMovement(
                        product: $product,
                        warehouse: $warehouse,
                        type: MovementType::CountAdjustmentDown,
                        quantity: abs($discrepancy),
                        actor: $actor,
                        reference: $stockCount,
                        reason: "تسوية جرد #{$stockCount->count_number} — نقص",
                    );
                }

                $item->update([
                    'status' => StockCountItemStatus::Adjusted->value,
                ]);
            }

            return $stockCount->fresh('items');
        });
    }

    /**
     * Cancel a stock count (only if open or in_progress).
     */
    public function cancel(StockCount $stockCount, User $actor): StockCount
    {
        $status = $stockCount->status instanceof StockCountStatus
            ? $stockCount->status->value
            : $stockCount->status;

        if (! in_array($status, ['open', 'in_progress'])) {
            throw new InvalidStatusTransitionException(
                "لا يمكن إلغاء جلسة جرد بحالة '{$status}'"
            );
        }

        return DB::transaction(function () use ($stockCount) {
            $stockCount->update([
                'status' => StockCountStatus::Cancelled->value,
            ]);

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

    /**
     * Recalculate totals on the stock count.
     */
    private function recalculateTotals(StockCount $stockCount): void
    {
        $items = $stockCount->items()->whereNotNull('discrepancy')->get();

        $totalDiscrepancies = $items->filter(fn ($item) => $item->discrepancy !== 0)->count();
        $totalVarianceValue = $items->sum(fn ($item) => abs($item->discrepancy_value ?? 0));

        $stockCount->update([
            'total_discrepancies' => $totalDiscrepancies,
            'total_variance_value' => $totalVarianceValue,
        ]);
    }

    /**
     * Generate a unique count number.
     */
    private function generateCountNumber(int $academyId): string
    {
        $date = now()->format('Ymd');
        $prefix = "SC-{$date}";

        // Per academy, so it has to be counted per academy: scoped to one
        // branch, two branches counting on the same day both mint SC-…-1.
        $lastCount = StockCount::withoutBranchScope()
            ->where('academy_id', $academyId)
            ->where('count_number', 'like', "{$prefix}-%")
            ->orderByDesc('count_number')
            ->first();

        if ($lastCount) {
            $lastSeq = (int) substr($lastCount->count_number, strrpos($lastCount->count_number, '-') + 1);
            $seq = $lastSeq + 1;
        } else {
            $seq = 1;
        }

        return "{$prefix}-" . str_pad($seq, 4, '0', STR_PAD_LEFT);
    }
}
