<?php

namespace App\Modules\Inventory\Stock\Services;

use App\Modules\Inventory\Reservation\Repositories\InventoryReservationMovementRepository;
use App\Modules\Inventory\Reservation\Support\ReservationEntryType;
use App\Modules\Inventory\Stock\Models\InventoryStockProjection;
use App\Modules\Inventory\Stock\Repositories\InventoryStockMovementRepository;
use App\Modules\Inventory\Stock\Repositories\InventoryStockProjectionRepository;
use App\Modules\Inventory\Stock\Support\DecimalQuantity;
use Illuminate\Support\Facades\DB;

final class InventoryStockRebuildService
{
    public function __construct(
        private readonly InventoryStockMovementRepository $movements,
        private readonly InventoryReservationMovementRepository $reservationMovements,
        private readonly InventoryStockProjectionRepository $projections,
        private readonly InventoryStockObservability $obs,
    ) {}

    /**
     * @return array{rows: int, mismatched_before: bool}
     */
    public function rebuildTenant(int $tenantId): array
    {
        $mismatched = $this->detectMismatch($tenantId);

        DB::transaction(function () use ($tenantId): void {
            $rows = $this->aggregateFromLedger($tenantId);
            $this->projections->bulkReplaceForTenant($tenantId, $rows);
        });

        if ($mismatched) {
            $this->obs->rebuildMismatch('rebuild_applied_after_drift', ['tenant_id' => $tenantId]);
        }

        return [
            'rows' => InventoryStockProjection::withoutGlobalScopes()->where('tenant_id', $tenantId)->count(),
            'mismatched_before' => $mismatched,
        ];
    }

    public function detectMismatch(int $tenantId): bool
    {
        $expected = $this->aggregateFromLedger($tenantId);
        $byKey = [];
        foreach ($expected as $r) {
            $byKey[$r['warehouse_id'].':'.$r['item_id']] = $r;
        }

        $proj = InventoryStockProjection::withoutGlobalScopes()
            ->where('tenant_id', $tenantId)
            ->get(['warehouse_id', 'item_id', 'on_hand_quantity', 'reserved_quantity', 'available_quantity']);

        foreach ($byKey as $k => $exp) {
            $row = $proj->first(fn (InventoryStockProjection $p): bool => ($p->warehouse_id.':'.$p->item_id) === $k);
            if ($row === null) {
                return true;
            }
            if (! DecimalQuantity::equals((string) $row->on_hand_quantity, $exp['on_hand'])) {
                return true;
            }
            if (! DecimalQuantity::equals((string) $row->reserved_quantity, $exp['reserved'])) {
                return true;
            }
            if (! DecimalQuantity::equals((string) $row->available_quantity, $exp['available'])) {
                return true;
            }
        }

        foreach ($proj as $p) {
            $k = $p->warehouse_id.':'.$p->item_id;
            if (! isset($byKey[$k])) {
                $zero = (string) $p->on_hand_quantity === '0'
                    && (string) $p->reserved_quantity === '0'
                    && (string) $p->available_quantity === '0';

                return ! $zero;
            }
        }

        return false;
    }

    /**
     * @return array<int, array{warehouse_id:int,item_id:int,on_hand:string,reserved:string,available:string,last_movement_id:?int}>
     */
    private function aggregateFromLedger(int $tenantId): array
    {
        $stock = [];
        foreach ($this->movements->cursorForTenantOrdered($tenantId) as $mov) {
            $k = $mov->warehouse_id.':'.$mov->item_id;
            if (! isset($stock[$k])) {
                $stock[$k] = [
                    'warehouse_id' => (int) $mov->warehouse_id,
                    'item_id' => (int) $mov->item_id,
                    'sum' => '0',
                    'max_id' => 0,
                ];
            }
            $stock[$k]['sum'] = DecimalQuantity::add($stock[$k]['sum'], (string) $mov->quantity);
            $stock[$k]['max_id'] = max($stock[$k]['max_id'], (int) $mov->id);
        }

        $reserved = [];
        foreach ($this->reservationMovements->cursorForTenantOrdered($tenantId) as $mov) {
            $k = $mov->warehouse_id.':'.$mov->item_id;
            if (! isset($reserved[$k])) {
                $reserved[$k] = ['sum' => '0'];
            }
            $q = (string) $mov->quantity;
            $delta = match ($mov->entry_type) {
                ReservationEntryType::Reserve => $q,
                ReservationEntryType::Release, ReservationEntryType::Consume => DecimalQuantity::negate($q),
            };
            $reserved[$k]['sum'] = DecimalQuantity::add($reserved[$k]['sum'], $delta);
        }

        $keys = array_unique(array_merge(array_keys($stock), array_keys($reserved)));
        $rows = [];
        foreach ($keys as $k) {
            $on = $stock[$k]['sum'] ?? '0';
            $res = $reserved[$k]['sum'] ?? '0';
            $avail = DecimalQuantity::add($on, DecimalQuantity::negate($res));
            $maxId = $stock[$k]['max_id'] ?? 0;

            [$wh, $it] = $this->warehouseItemIdsForKey($k, $stock);
            $rows[] = [
                'warehouse_id' => $wh,
                'item_id' => $it,
                'on_hand' => $on,
                'reserved' => $res,
                'available' => $avail,
                'last_movement_id' => $maxId > 0 ? $maxId : null,
            ];
        }

        return $rows;
    }

    /**
     * @param  array<string, array{warehouse_id:int, item_id:int, sum:string, max_id:int}>  $stock
     * @return array{0:int,1:int}
     */
    private function warehouseItemIdsForKey(string $k, array $stock): array
    {
        if (isset($stock[$k])) {
            return [$stock[$k]['warehouse_id'], $stock[$k]['item_id']];
        }
        $p = array_map('intval', explode(':', $k, 2));

        return [$p[0], $p[1]];
    }
}
