<?php

namespace App\Services;

use App\Exceptions\StockReferenceException;
use App\Models\Inventory\ProductSerial;
use App\Models\Inventory\SerialOwnershipHistory;
use App\Models\Inventory\SerialRepairHistory;
use App\Models\Lookups\Status;
use App\Models\StockMovement;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;

/**
 * All serial number lifecycle operations go through this service.
 * Controllers and other services must never mutate product_serials directly.
 */
class SerialService
{
    /**
     * Create serial records from a GRN line.
     *
     * @param  array<string>  $serialNumbers  raw serial number strings
     * @return ProductSerial[]
     */
    public function createFromGrn(
        int $productId,
        ?int $stockBatchId,
        int $branchId,
        array $serialNumbers,
    ): array {
        $availableStatusId = Status::idOf(Status::CTX_PRODUCT_SERIAL, Status::SERIAL_AVAILABLE);
        $created = [];

        foreach ($serialNumbers as $number) {
            $number = trim((string) $number);
            if ($number === '') {
                continue;
            }

            $serial = ProductSerial::create([
                'product_id' => $productId,
                'stock_batch_id' => $stockBatchId,
                'branch_id' => $branchId,
                'serial_number' => $number,
                'status_id' => $availableStatusId,
                'created_by' => Auth::id(),
            ]);

            $this->logOwnership($serial, 'received', null, null, null);
            $created[] = $serial;
        }

        return $created;
    }

    /**
     * Validate that a serial is available before assigning it to an invoice.
     * Throws if it is not.
     */
    public function validateAvailable(ProductSerial $serial): void
    {
        if (! $serial->isAvailable()) {
            throw StockReferenceException::serial(
                $serial->product_id,
                "Serial #{$serial->serial_number} is not available (current status: {$serial->status?->code})",
            );
        }
    }

    /**
     * Assign a serial to an invoice (marks as sold, records ownership history).
     */
    public function assignToInvoice(ProductSerial $serial, int $invoiceId, int $customerId): void
    {
        $soldStatusId = Status::idOf(Status::CTX_PRODUCT_SERIAL, Status::SERIAL_SOLD);

        $serial->update([
            'status_id' => $soldStatusId,
            'current_customer_id' => $customerId,
            'branch_id' => null,
            'updated_by' => Auth::id(),
        ]);

        $this->logOwnership($serial, 'sold', $customerId, $invoiceId, null);
    }

    /**
     * Return a serial (revert to available at branch, clear customer ownership).
     */
    public function returnSerial(ProductSerial $serial, int $returnId, int $branchId): void
    {
        $returnedStatusId = Status::idOf(Status::CTX_PRODUCT_SERIAL, Status::SERIAL_RETURNED);

        $serial->update([
            'status_id' => $returnedStatusId,
            'current_customer_id' => null,
            'branch_id' => $branchId,
            'updated_by' => Auth::id(),
        ]);

        $this->logOwnership($serial, 'returned', null, null, $returnId);
    }

    /**
     * Restore a returned serial to available stock.
     */
    public function restoreToStock(ProductSerial $serial, int $branchId): void
    {
        $availableStatusId = Status::idOf(Status::CTX_PRODUCT_SERIAL, Status::SERIAL_AVAILABLE);

        $serial->update([
            'status_id' => $availableStatusId,
            'branch_id' => $branchId,
            'updated_by' => Auth::id(),
        ]);

        $this->logOwnership($serial, 'restocked', null, null, null);
    }

    /**
     * Single entry point for ReturnService: a sold serial is coming back on a
     * customer return. Either it goes back to available stock at the
     * receiving branch, or — if the return line is damage-flagged — it's
     * marked damaged instead. Always clears ownership.
     */
    public function receiveReturn(ProductSerial $serial, int $returnId, int $branchId, bool $isDamaged): void
    {
        $statusId = Status::idOf(
            Status::CTX_PRODUCT_SERIAL,
            $isDamaged ? Status::SERIAL_DAMAGED : Status::SERIAL_AVAILABLE,
        );

        $serial->update([
            'status_id' => $statusId,
            'branch_id' => $branchId,
            'current_customer_id' => null,
            'updated_by' => Auth::id(),
        ]);

        $this->logOwnership($serial, $isDamaged ? 'returned_damaged' : 'returned', null, null, $returnId);
    }

    /**
     * Undo a sale (invoice deletion): serial goes back to available at the
     * branch it was sold from.
     */
    public function reverseSale(ProductSerial $serial, int $branchId): void
    {
        $availableStatusId = Status::idOf(Status::CTX_PRODUCT_SERIAL, Status::SERIAL_AVAILABLE);

        $serial->update([
            'status_id' => $availableStatusId,
            'branch_id' => $branchId,
            'current_customer_id' => null,
            'updated_by' => Auth::id(),
        ]);

        $this->logOwnership($serial, 'sale_reversed', null, null, null);
    }

    /**
     * Send a serial back to its supplier on a confirmed Return GRN: the unit
     * leaves the business entirely (branch cleared, flagged
     * returned_to_supplier) so it can never be sold or found by branch
     * scoping until a return-GRN deletion brings it back.
     */
    public function returnToSupplier(ProductSerial $serial, string $referenceNumber): void
    {
        $statusId = Status::idOf(Status::CTX_PRODUCT_SERIAL, Status::SERIAL_RETURNED_TO_SUPPLIER);

        $serial->update([
            'status_id' => $statusId,
            'branch_id' => null,
            'notes' => "Returned to supplier via {$referenceNumber}",
            'updated_by' => Auth::id(),
        ]);

        $this->logOwnership($serial, 'returned_to_supplier', null, null, null, "Return GRN {$referenceNumber}");
    }

    /**
     * Reverse a return-to-supplier (return GRN deletion): the serial comes
     * back to available stock at the branch it was returned from.
     */
    public function reverseReturnToSupplier(ProductSerial $serial, int $branchId, string $referenceNumber): void
    {
        $availableStatusId = Status::idOf(Status::CTX_PRODUCT_SERIAL, Status::SERIAL_AVAILABLE);

        $serial->update([
            'status_id' => $availableStatusId,
            'branch_id' => $branchId,
            'notes' => null,
            'updated_by' => Auth::id(),
        ]);

        $this->logOwnership($serial, 'return_to_supplier_reversed', null, null, null, "Return GRN {$referenceNumber} deleted");
    }

    /**
     * Look up a serial a customer is physically returning by number. Must
     * currently be `sold` — that's the only state a customer could be holding
     * the unit in.
     */
    public function findSoldSerial(int $productId, string $serialNumber): ?ProductSerial
    {
        $soldStatusId = Status::idOf(Status::CTX_PRODUCT_SERIAL, Status::SERIAL_SOLD);

        return ProductSerial::query()
            ->where('product_id', $productId)
            ->where('serial_number', $serialNumber)
            ->where('status_id', $soldStatusId)
            ->first();
    }

    /**
     * Move discrete serial rows to another branch (Stock Transfer). No
     * quantity math — each serial is a discrete unit.
     *
     * @param  array<int>  $serialIds
     */
    public function transfer(array $serialIds, int $toBranchId): void
    {
        ProductSerial::query()
            ->whereIn('id', $serialIds)
            ->update(['branch_id' => $toBranchId, 'updated_by' => Auth::id()]);
    }

    /** Mark a serial as damaged. */
    public function markDamaged(ProductSerial $serial, string $notes = ''): void
    {
        $statusId = Status::idOf(Status::CTX_PRODUCT_SERIAL, Status::SERIAL_DAMAGED);
        $serial->update(['status_id' => $statusId, 'notes' => $notes, 'updated_by' => Auth::id()]);
        $this->logOwnership($serial, 'damaged', null, null, null, $notes);
    }

    /** Mark a serial as lost. */
    public function markLost(ProductSerial $serial, string $notes = ''): void
    {
        $statusId = Status::idOf(Status::CTX_PRODUCT_SERIAL, Status::SERIAL_LOST);
        $serial->update(['status_id' => $statusId, 'branch_id' => null, 'notes' => $notes, 'updated_by' => Auth::id()]);
        $this->logOwnership($serial, 'lost', null, null, null, $notes);
    }

    /** Write off a serial permanently. */
    public function writeOff(ProductSerial $serial, string $reason): void
    {
        $statusId = Status::idOf(Status::CTX_PRODUCT_SERIAL, Status::SERIAL_WRITTEN_OFF);
        $serial->update(['status_id' => $statusId, 'branch_id' => null, 'notes' => $reason, 'updated_by' => Auth::id()]);
        $this->logOwnership($serial, 'written_off', null, null, null, $reason);
    }

    /**
     * Begin a repair workflow — serial moves to 'under_repair'.
     *
     * @param  array{branch_id?:int|null, description:string, started_at:string, repaired_by?:string|null, notes?:string|null}  $data
     */
    public function beginRepair(ProductSerial $serial, array $data): SerialRepairHistory
    {
        $statusId = Status::idOf(Status::CTX_PRODUCT_SERIAL, Status::SERIAL_UNDER_REPAIR);
        $serial->update(['status_id' => $statusId, 'updated_by' => Auth::id()]);

        $this->logOwnership($serial, 'under_repair', null, null, null, $data['description'] ?? null);

        return SerialRepairHistory::create([
            'product_serial_id' => $serial->id,
            'branch_id' => $data['branch_id'] ?? null,
            'description' => $data['description'],
            'started_at' => $data['started_at'],
            'repaired_by' => $data['repaired_by'] ?? null,
            'notes' => $data['notes'] ?? null,
            'created_by' => Auth::id(),
        ]);
    }

    /**
     * Complete a repair. Outcome: 'repaired', 'unrepairable', 'returned_to_customer'.
     */
    public function completeRepair(SerialRepairHistory $repair, string $outcome, ?string $notes, int $branchId): void
    {
        $repair->update([
            'completed_at' => now()->toDateString(),
            'outcome' => $outcome,
            'notes' => $notes,
        ]);

        $serial = $repair->serial;
        $newStatusCode = match ($outcome) {
            'repaired' => Status::SERIAL_AVAILABLE,
            'unrepairable' => Status::SERIAL_DAMAGED,
            default => Status::SERIAL_RETURNED,
        };

        $newStatusId = Status::idOf(Status::CTX_PRODUCT_SERIAL, $newStatusCode);
        $serial->update([
            'status_id' => $newStatusId,
            'branch_id' => $outcome === 'returned_to_customer' ? null : $branchId,
            'updated_by' => Auth::id(),
        ]);

        $this->logOwnership($serial, "repair_{$outcome}", null, null, null, $notes);
    }

    /**
     * Link serial IDs to a stock movement (writes stock_movement_serials rows).
     *
     * @param  array<int>  $serialIds
     */
    public function linkToMovement(StockMovement $movement, array $serialIds): void
    {
        foreach ($serialIds as $serialId) {
            DB::table('stock_movement_serials')->insertOrIgnore([
                'stock_movement_id' => $movement->id,
                'product_serial_id' => $serialId,
                'created_at' => now(),
                'updated_at' => now(),
            ]);
        }
    }

    // ── Private helpers ───────────────────────────────────────────────────────

    private function logOwnership(
        ProductSerial $serial,
        string $eventType,
        ?int $customerId,
        ?int $invoiceId,
        ?int $returnId,
        ?string $notes = null,
    ): void {
        SerialOwnershipHistory::create([
            'product_serial_id' => $serial->id,
            'customer_id' => $customerId,
            'invoice_id' => $invoiceId,
            'return_id' => $returnId,
            'event_type' => $eventType,
            'occurred_at' => now(),
            'notes' => $notes,
            'created_by' => Auth::id(),
        ]);
    }
}
