<?php

namespace App\Services\Warehouse;

use App\Models\Tenant\Part;
use App\Models\Tenant\PartBatch;
use App\Models\Tenant\PartReservation;
use App\Models\Tenant\Supplier;
use App\Models\Tenant\Warehouse;
use App\Models\Tenant\WarehouseEvent;
use App\Models\Tenant\WorkOrder;
use App\Models\Tenant\WorkOrderPart;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;

class WarehouseService
{
    /**
     * Resolve the warehouse to operate on. Default = company.default_warehouse_id.
     */
    public function defaultWarehouse(int $companyId): Warehouse
    {
        $company = \App\Models\Central\Company::withoutGlobalScopes()->findOrFail($companyId);
        if ($company->default_warehouse_id) {
            return Warehouse::findOrFail($company->default_warehouse_id);
        }
        // Lazy-create a default warehouse if missing (e.g. tenant created
        // before warehouse migration). This makes the service self-healing.
        $wh = Warehouse::create([
            'company_id' => $companyId,
            'code' => 'MAIN',
            'name' => 'Depozit principal',
            'is_default' => true,
        ]);
        $company->forceFill(['default_warehouse_id' => $wh->id])->saveQuietly();
        return $wh;
    }

    /**
     * Receive new stock — creates a batch + receipt event + updates cached qty.
     */
    public function receive(
        Part $part,
        float $qty,
        float $buyPrice,
        ?Warehouse $warehouse = null,
        ?Supplier $supplier = null,
        ?string $batchRef = null,
        ?Model $ref = null,
        ?string $notes = null,
        ?Carbon $occurredAt = null,
    ): PartBatch {
        if ($qty <= 0) {
            throw new \InvalidArgumentException('Cantitatea de recepție trebuie să fie pozitivă.');
        }

        $warehouse ??= $this->defaultWarehouse($part->company_id);
        $occurredAt ??= now();

        return DB::transaction(function () use ($part, $qty, $buyPrice, $warehouse, $supplier, $batchRef, $ref, $notes, $occurredAt) {
            $batch = PartBatch::create([
                'company_id' => $part->company_id,
                'part_id' => $part->id,
                'warehouse_id' => $warehouse->id,
                'supplier_id' => $supplier?->id,
                'batch_ref' => $batchRef,
                'qty_in' => $qty,
                'qty_remaining' => $qty,
                'buy_price' => $buyPrice,
                'received_at' => $occurredAt,
                'notes' => $notes,
            ]);

            $this->logEvent($part, $batch, $warehouse, 'receipt', $qty, $buyPrice, $ref, $notes, $occurredAt);

            $this->syncPartCachedQty($part);

            return $batch;
        });
    }

    /**
     * Consume stock FIFO across batches. Writes one event per consumed batch.
     * Throws InsufficientStockException if total available < qty.
     */
    public function issue(
        Part $part,
        float $qty,
        ?Warehouse $warehouse = null,
        ?Model $ref = null,
        ?string $notes = null,
    ): array {
        if ($qty <= 0) {
            throw new \InvalidArgumentException('Cantitatea de consum trebuie să fie pozitivă.');
        }

        $warehouse ??= $this->defaultWarehouse($part->company_id);

        return DB::transaction(function () use ($part, $qty, $warehouse, $ref, $notes) {
            $available = $this->availableForIssue($part, $warehouse);
            if ($available < $qty) {
                throw new InsufficientStockException($part->id, $qty, $available);
            }

            $remaining = $qty;
            $events = [];

            $batches = $this->fifoBatches($part, $warehouse)->lockForUpdate()->get();

            foreach ($batches as $batch) {
                if ($remaining <= 0) break;

                $take = min($remaining, (float) $batch->qty_remaining);
                if ($take <= 0) continue;

                $batch->qty_remaining = (float) $batch->qty_remaining - $take;
                $batch->save();

                $events[] = $this->logEvent(
                    $part, $batch, $warehouse, 'issue',
                    -$take, (float) $batch->buy_price, $ref, $notes
                );

                $remaining -= $take;
            }

            $this->syncPartCachedQty($part);

            return $events;
        });
    }

    /**
     * Reserve qty from FIFO batches against a WorkOrderPart. Returns reservation rows.
     */
    public function reserve(WorkOrderPart $wop): array
    {
        return DB::transaction(function () use ($wop) {
            $part = $wop->part;
            if (! $part) return [];   // free-text part with no catalog link — skip

            $warehouse = $this->defaultWarehouse($part->company_id);
            $qty = (float) $wop->qty;

            $available = $this->availableForReservation($part, $warehouse);
            if ($available < $qty) {
                throw new InsufficientStockException($part->id, $qty, $available);
            }

            $remaining = $qty;
            $reservations = [];
            $batches = $this->fifoBatchesAvailable($part, $warehouse)->lockForUpdate()->get();

            foreach ($batches as $batch) {
                if ($remaining <= 0) break;

                $reservedOnBatch = (float) PartReservation::where('batch_id', $batch->id)
                    ->where('status', PartReservation::STATUS_ACTIVE)
                    ->sum('qty');
                $free = (float) $batch->qty_remaining - $reservedOnBatch;
                if ($free <= 0) continue;

                $take = min($remaining, $free);

                $reservations[] = PartReservation::create([
                    'company_id' => $part->company_id,
                    'work_order_id' => $wop->work_order_id,
                    'work_order_part_id' => $wop->id,
                    'part_id' => $part->id,
                    'batch_id' => $batch->id,
                    'qty' => $take,
                    'status' => PartReservation::STATUS_ACTIVE,
                    'reserved_at' => now(),
                ]);

                $remaining -= $take;
            }

            $part->qty_reserved = (float) $part->qty_reserved + ($qty - $remaining);
            $part->saveQuietly();

            return $reservations;
        });
    }

    /**
     * Release active reservations on a WorkOrderPart (e.g. WO cancelled / line removed).
     */
    public function release(WorkOrderPart $wop): int
    {
        return DB::transaction(function () use ($wop) {
            $active = PartReservation::where('work_order_part_id', $wop->id)
                ->where('status', PartReservation::STATUS_ACTIVE)
                ->lockForUpdate()
                ->get();

            $totalReleased = 0.0;
            foreach ($active as $r) {
                $r->status = PartReservation::STATUS_RELEASED;
                $r->save();
                $totalReleased += (float) $r->qty;
            }

            if ($wop->part_id && $totalReleased > 0) {
                $part = Part::find($wop->part_id);
                if ($part) {
                    $part->qty_reserved = max(0.0, (float) $part->qty_reserved - $totalReleased);
                    $part->saveQuietly();
                }
            }

            return $active->count();
        });
    }

    /**
     * Consume reservations against a closed WO — converts each active reservation
     * into an issue event and decrements batch qty_remaining.
     */
    public function consume(WorkOrder $wo): int
    {
        return DB::transaction(function () use ($wo) {
            $active = PartReservation::with(['batch', 'part'])
                ->where('work_order_id', $wo->id)
                ->where('status', PartReservation::STATUS_ACTIVE)
                ->lockForUpdate()
                ->get();

            foreach ($active as $r) {
                $batch = $r->batch;
                if (! $batch) continue;
                $take = min((float) $r->qty, (float) $batch->qty_remaining);
                if ($take <= 0) continue;

                $batch->qty_remaining = (float) $batch->qty_remaining - $take;
                $batch->save();

                $this->logEvent(
                    $r->part, $batch, $batch->warehouse,
                    'issue', -$take, (float) $batch->buy_price,
                    $wo, "WO #{$wo->number}"
                );

                $r->status = PartReservation::STATUS_CONSUMED;
                $r->consumed_at = now();
                $r->save();

                if ($r->part) {
                    $r->part->qty_reserved = max(0.0, (float) $r->part->qty_reserved - (float) $r->qty);
                    $r->part->saveQuietly();
                }
            }

            // Re-sync all touched parts.
            $partIds = $active->pluck('part_id')->unique();
            foreach ($partIds as $pid) {
                if ($p = Part::find($pid)) $this->syncPartCachedQty($p);
            }

            return $active->count();
        });
    }

    /**
     * Issue parts NOW for a single WO line — used when the mechanic physically
     * takes the part from the shelf before the WO is closed. Same logic as
     * consume() but scoped to one work_order_part_id.
     */
    public function issueNow(WorkOrderPart $wop, ?string $signatureB64 = null, ?string $scanPayload = null): int
    {
        return DB::transaction(function () use ($wop, $signatureB64, $scanPayload) {
            $active = PartReservation::with(['batch', 'part'])
                ->where('work_order_part_id', $wop->id)
                ->where('status', PartReservation::STATUS_ACTIVE)
                ->lockForUpdate()
                ->get();

            foreach ($active as $r) {
                $batch = $r->batch;
                if (! $batch) continue;
                $take = min((float) $r->qty, (float) $batch->qty_remaining);
                if ($take <= 0) continue;

                $batch->qty_remaining = (float) $batch->qty_remaining - $take;
                $batch->save();

                $event = $this->logEvent(
                    $r->part, $batch, $batch->warehouse,
                    'issue', -$take, (float) $batch->buy_price,
                    $wop->workOrder, "WO part #{$wop->id} (issue now)"
                );
                if ($signatureB64 || $scanPayload) {
                    $event->forceFill([
                        'signature_b64' => $signatureB64,
                        'scan_payload' => $scanPayload,
                    ])->save();
                }

                $r->status = PartReservation::STATUS_CONSUMED;
                $r->consumed_at = now();
                $r->save();

                if ($r->part) {
                    $r->part->qty_reserved = max(0.0, (float) $r->part->qty_reserved - (float) $r->qty);
                    $r->part->saveQuietly();
                }
            }

            if ($wop->part) $this->syncPartCachedQty($wop->part);
            return $active->count();
        });
    }

    /**
     * Return parts physically taken back to stock. Creates a NEW batch with
     * the same buy_price as the original (FIFO-correct re-entry) and writes
     * a `return` event referencing the WO part.
     */
    public function returnPart(WorkOrderPart $wop, ?float $qty = null, ?string $notes = null): ?PartBatch
    {
        if (! $wop->part_id) return null;

        return DB::transaction(function () use ($wop, $qty, $notes) {
            $consumed = PartReservation::where('work_order_part_id', $wop->id)
                ->where('status', PartReservation::STATUS_CONSUMED)
                ->orderBy('consumed_at', 'desc')
                ->get();

            $totalConsumed = (float) $consumed->sum('qty');
            $qtyReturn = $qty !== null ? min((float) $qty, $totalConsumed) : $totalConsumed;
            if ($qtyReturn <= 0) return null;

            $unitCost = (float) ($consumed->first()?->batch?->buy_price ?? $wop->buy_price);
            $warehouse = $consumed->first()?->batch?->warehouse
                ?? $this->defaultWarehouse($wop->company_id);

            $batch = PartBatch::create([
                'company_id' => $wop->company_id,
                'part_id' => $wop->part_id,
                'warehouse_id' => $warehouse->id,
                'qty_in' => $qtyReturn,
                'qty_remaining' => $qtyReturn,
                'buy_price' => $unitCost,
                'received_at' => now(),
                'notes' => $notes ?? "Retur de la WO part #{$wop->id}",
            ]);

            $this->logEvent(
                $wop->part, $batch, $warehouse,
                'return', $qtyReturn, $unitCost,
                $wop->workOrder, $notes ?? "Retur WO #{$wop->workOrder?->number}"
            );

            $this->syncPartCachedQty($wop->part);
            return $batch;
        });
    }

    /**
     * Move stock between warehouses (FIFO from source). Creates one transfer_out
     * + one transfer_in batch in the destination warehouse.
     */
    public function transfer(
        Part $part,
        float $qty,
        Warehouse $from,
        Warehouse $to,
        ?string $notes = null,
    ): PartBatch {
        if ($qty <= 0) throw new \InvalidArgumentException('Cantitatea de transfer trebuie să fie pozitivă.');
        if ($from->id === $to->id) throw new \InvalidArgumentException('Sursa și destinația sunt identice.');

        return DB::transaction(function () use ($part, $qty, $from, $to, $notes) {
            $available = $this->availableForIssue($part, $from);
            if ($available < $qty) {
                throw new InsufficientStockException($part->id, $qty, $available);
            }

            $remaining = $qty;
            $totalCost = 0.0;

            $batches = $this->fifoBatches($part, $from)->lockForUpdate()->get();
            foreach ($batches as $batch) {
                if ($remaining <= 0) break;
                $take = min($remaining, (float) $batch->qty_remaining);
                if ($take <= 0) continue;

                $batch->qty_remaining = (float) $batch->qty_remaining - $take;
                $batch->save();

                $totalCost += $take * (float) $batch->buy_price;

                $this->logEvent($part, $batch, $from, 'transfer_out', -$take, (float) $batch->buy_price, null, $notes);
                $remaining -= $take;
            }

            $avgCost = $qty > 0 ? round($totalCost / $qty, 2) : 0.0;

            $destBatch = PartBatch::create([
                'company_id' => $part->company_id,
                'part_id' => $part->id,
                'warehouse_id' => $to->id,
                'qty_in' => $qty,
                'qty_remaining' => $qty,
                'buy_price' => $avgCost,
                'received_at' => now(),
                'notes' => $notes ?? "Transfer din {$from->code}",
            ]);

            $this->logEvent($part, $destBatch, $to, 'transfer_in', $qty, $avgCost, null, $notes);

            return $destBatch;
        });
    }

    /**
     * Inventory adjustment (stock-take correction). Positive delta = mystery gain;
     * negative = write-off. For losses we consume FIFO; for gains we open a new batch.
     */
    public function adjust(Part $part, float $delta, ?Warehouse $warehouse = null, ?string $notes = null): void
    {
        if (abs($delta) < 0.001) return;
        $warehouse ??= $this->defaultWarehouse($part->company_id);

        DB::transaction(function () use ($part, $delta, $warehouse, $notes) {
            if ($delta > 0) {
                // Use average current cost or last buy_price as cost for the gain batch.
                $cost = (float) ($part->batches()->latest('received_at')->value('buy_price') ?? $part->buy_price);
                $batch = PartBatch::create([
                    'company_id' => $part->company_id,
                    'part_id' => $part->id,
                    'warehouse_id' => $warehouse->id,
                    'qty_in' => $delta,
                    'qty_remaining' => $delta,
                    'buy_price' => $cost,
                    'received_at' => now(),
                    'notes' => $notes ?? 'Ajustare manuală',
                ]);
                $this->logEvent($part, $batch, $warehouse, 'adjustment', $delta, $cost, null, $notes);
            } else {
                $remaining = -$delta;
                $batches = $this->fifoBatches($part, $warehouse)->lockForUpdate()->get();
                foreach ($batches as $batch) {
                    if ($remaining <= 0) break;
                    $take = min($remaining, (float) $batch->qty_remaining);
                    if ($take <= 0) continue;
                    $batch->qty_remaining = (float) $batch->qty_remaining - $take;
                    $batch->save();
                    $this->logEvent($part, $batch, $warehouse, 'adjustment', -$take, (float) $batch->buy_price, null, $notes);
                    $remaining -= $take;
                }
            }

            $this->syncPartCachedQty($part);
        });
    }

    // ─── Internals ─────────────────────────────────────────────────

    /**
     * Available qty for FIFO issue ignoring reservations
     * (caller decides whether reservations matter).
     */
    public function availableForIssue(Part $part, Warehouse $warehouse): float
    {
        return (float) PartBatch::where('part_id', $part->id)
            ->where('warehouse_id', $warehouse->id)
            ->sum('qty_remaining');
    }

    /**
     * Available for new reservation = on_hand − active_reservations.
     */
    public function availableForReservation(Part $part, Warehouse $warehouse): float
    {
        $onHand = $this->availableForIssue($part, $warehouse);

        $reserved = (float) PartReservation::where('part_id', $part->id)
            ->where('status', PartReservation::STATUS_ACTIVE)
            ->whereHas('batch', fn ($q) => $q->where('warehouse_id', $warehouse->id))
            ->sum('qty');

        return max(0.0, $onHand - $reserved);
    }

    protected function fifoBatches(Part $part, Warehouse $warehouse)
    {
        return PartBatch::where('part_id', $part->id)
            ->where('warehouse_id', $warehouse->id)
            ->where('qty_remaining', '>', 0)
            ->orderBy('received_at')
            ->orderBy('id');
    }

    protected function fifoBatchesAvailable(Part $part, Warehouse $warehouse)
    {
        return $this->fifoBatches($part, $warehouse);
    }

    protected function logEvent(
        Part $part,
        PartBatch $batch,
        Warehouse $warehouse,
        string $type,
        float $qtyDelta,
        ?float $unitCost,
        ?Model $ref,
        ?string $notes,
        ?Carbon $occurredAt = null,
    ): WarehouseEvent {
        return WarehouseEvent::create([
            'company_id' => $part->company_id,
            'part_id' => $part->id,
            'batch_id' => $batch->id,
            'warehouse_id' => $warehouse->id,
            'type' => $type,
            'qty_delta' => $qtyDelta,
            'unit_cost' => $unitCost,
            'ref_type' => $ref ? get_class($ref) : null,
            'ref_id' => $ref?->getKey(),
            'user_id' => auth()->id(),
            'occurred_at' => $occurredAt ?? now(),
            'notes' => $notes,
        ]);
    }

    /** Keep parts.qty in sync as a cached aggregate across all warehouses. */
    protected function syncPartCachedQty(Part $part): void
    {
        $total = (float) PartBatch::where('part_id', $part->id)->sum('qty_remaining');
        $part->qty = $total;
        $part->saveQuietly();
    }
}
