<?php

namespace App\Services;

use App\Models\Area;
use App\Models\OrderAreaScanEvent;
use App\Models\OrdenLavanderia;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;
use RuntimeException;

/**
 * Registro de escaneos QR por orden y área.
 * Valida secuencia 1→2 por ciclo; no duplicar reglas solo en la BD (ver migración).
 */
class OrderAreaScanEventService
{
    /**
     * Registra un escaneo. Por defecto exige que el 2º no exista sin un 1º previo en el ciclo actual.
     *
     * @param  array<string, mixed>|null  $rawPayload
     */
    public function recordScan(
        OrdenLavanderia $orden,
        Area $area,
        int $scanSequence,
        ?int $userId,
        ?string $deviceLabel = null,
        ?array $rawPayload = null,
        ?string $clientIpForHash = null,
        bool $allowOutOfOrder = false
    ): OrderAreaScanEvent {
        if ($scanSequence !== OrderAreaScanEvent::SEQUENCE_FIRST && $scanSequence !== OrderAreaScanEvent::SEQUENCE_SECOND) {
            throw new InvalidArgumentException('scan_sequence debe ser 1 o 2.');
        }

        if (! $allowOutOfOrder) {
            $this->assertValidSequence($orden->id, $area->id, $scanSequence);
        }

        $ipHash = null;
        if ($clientIpForHash !== null && $clientIpForHash !== '') {
            $ipHash = hash('sha256', $clientIpForHash);
        }

        return DB::transaction(function () use ($orden, $area, $scanSequence, $userId, $deviceLabel, $rawPayload, $ipHash): OrderAreaScanEvent {
            return OrderAreaScanEvent::create([
                'orden_lavanderia_id' => $orden->id,
                'area_id' => $area->id,
                'scan_sequence' => $scanSequence,
                'user_id' => $userId,
                'scanned_at' => now(),
                'device_label' => $deviceLabel,
                'raw_payload' => $rawPayload,
                'client_ip_hash' => $ipHash,
            ]);
        });
    }

    /**
     * Último evento de escaneo para la orden en el área (por tiempo e id).
     */
    public function lastEventForOrderAndArea(int $ordenLavanderiaId, int $areaId): ?OrderAreaScanEvent
    {
        return OrderAreaScanEvent::query()
            ->forOrderAndArea($ordenLavanderiaId, $areaId)
            ->orderByDesc('scanned_at')
            ->orderByDesc('id')
            ->first();
    }

    /**
     * Reglas: tras un 1 solo puede seguir un 2; tras un 2 puede empezar un nuevo ciclo con 1;
     * el primer evento del par orden+área debe ser siempre 1.
     */
    private function assertValidSequence(int $ordenLavanderiaId, int $areaId, int $scanSequence): void
    {
        $last = $this->lastEventForOrderAndArea($ordenLavanderiaId, $areaId);

        if ($last === null) {
            if ($scanSequence !== OrderAreaScanEvent::SEQUENCE_FIRST) {
                throw new RuntimeException(
                    'El primer escaneo en esta orden y área debe ser la secuencia 1.'
                );
            }

            return;
        }

        if ($last->scan_sequence === OrderAreaScanEvent::SEQUENCE_FIRST) {
            if ($scanSequence !== OrderAreaScanEvent::SEQUENCE_SECOND) {
                throw new RuntimeException(
                    'Debe registrarse la secuencia 2 antes de un nuevo ciclo en esta orden y área.'
                );
            }

            return;
        }

        if ($last->scan_sequence === OrderAreaScanEvent::SEQUENCE_SECOND) {
            if ($scanSequence !== OrderAreaScanEvent::SEQUENCE_FIRST) {
                throw new RuntimeException(
                    'Tras completar el ciclo (secuencia 2), el siguiente escaneo debe ser la secuencia 1.'
                );
            }
        }
    }
}
