<?php

namespace App\Services;

use App\Events\OrdenLavanderiaTerminada;
use App\Models\Area;
use App\Models\OrdenLavanderia;
use App\Models\OrderAreaPresence;
use App\Models\OrderAreaScanEvent;
use App\Models\User;
use App\Support\AreaPermission;
use App\Support\LaundryAuditLogger;
use App\Support\LaundryScanInput;
use App\Support\UserAreaAssignment;
use Illuminate\Contracts\Cache\LockTimeoutException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use RuntimeException;

/**
 * Flujo de escaneo por área: orden + permiso + evento de escaneo + presencia.
 * El QR es un solo identificador (misma cadena aunque se imprima dos veces en recepción).
 *
 * C5: la asignación de usuario a área (`users.area_id`) debe coincidir con el `area_code`
 * del escaneo (salvo Administrador). Ver {@see \App\Support\UserAreaAssignment}.
 */
class LaundryWorkflowService
{
    public function __construct(
        private readonly OrderAreaScanEventService $scanEventService
    ) {}

    /**
     * Registra un escaneo en un área y actualiza presencia.
     *
     * @return array{ok: true, data: array<string, mixed>}|array{ok: false, status: int, message: string, errors?: array<string, array<int, string>>}
     */
    public function registerScan(
        string $qrOrFolio,
        string $areaCode,
        User $user,
        ?string $clientIp = null
    ): array {
        $area = Area::query()->operational()->where('code', $areaCode)->first();
        if ($area === null) {
            LaundryAuditLogger::scanDenied('area_not_operational', [
                'area_code' => $areaCode,
                'user_id' => $user->id,
            ]);

            return [
                'ok' => false,
                'status' => 404,
                'message' => 'Área no encontrada o inactiva.',
                'errors' => ['area_code' => ['El código de área no existe o no está habilitado.']],
            ];
        }

        if (! AreaPermission::canOperate($user, $area)) {
            LaundryAuditLogger::scanDenied('permission_denied', [
                'area_id' => $area->id,
                'area_code' => $area->code,
                'user_id' => $user->id,
            ]);

            return [
                'ok' => false,
                'status' => 403,
                'message' => 'No tiene permiso para operar en esta área.',
                'errors' => ['permission' => ['Operación no autorizada para el área seleccionada.']],
            ];
        }

        $deniedAssign = UserAreaAssignment::scanDeniedMessage($user, $area);
        if ($deniedAssign !== null) {
            LaundryAuditLogger::scanDenied('area_assignment_mismatch', [
                'area_id' => $area->id,
                'area_code' => $area->code,
                'user_id' => $user->id,
                'user_area_id' => $user->area_id,
            ]);

            return [
                'ok' => false,
                'status' => 403,
                'message' => $deniedAssign,
                'errors' => ['area_id' => [$deniedAssign]],
            ];
        }

        $orden = $this->resolveOrden(trim($qrOrFolio));
        if ($orden === null) {
            LaundryAuditLogger::scanDenied('order_not_found', [
                'input_fp' => substr(hash('sha256', trim($qrOrFolio)), 0, 12),
                'area_id' => $area->id,
                'user_id' => $user->id,
            ]);

            return [
                'ok' => false,
                'status' => 404,
                'message' => 'Orden no encontrada.',
                'errors' => ['qr_or_folio' => ['No se encontró una orden con ese folio o código QR.']],
            ];
        }

        if (in_array($orden->estatus, [
            OrdenLavanderia::ESTATUS_TERMINADO,
            OrdenLavanderia::ESTATUS_ENTREGADO,
            OrdenLavanderia::ESTATUS_CANCELADO,
        ], true)) {
            LaundryAuditLogger::scanDenied('order_closed', [
                'orden_id' => $orden->id,
                'folio_fp' => LaundryAuditLogger::folioFingerprint($orden->folio_unico),
                'estatus' => $orden->estatus,
                'area_id' => $area->id,
                'user_id' => $user->id,
            ]);

            return [
                'ok' => false,
                'status' => 422,
                'message' => 'La orden no admite escaneos en su estado actual.',
                'errors' => ['orden' => ['La orden está terminada, entregada o cancelada.']],
            ];
        }

        try {
            return Cache::lock('laundry_workflow:'.$orden->id.':'.$area->id, 15)->block(8, function () use ($orden, $area, $user, $clientIp): array {
                return DB::transaction(function () use ($orden, $area, $user, $clientIp): array {
                    $ordenLocked = OrdenLavanderia::query()
                        ->whereKey($orden->id)
                        ->lockForUpdate()
                        ->firstOrFail();

                    $lastEvent = OrderAreaScanEvent::query()
                        ->forOrderAndArea($ordenLocked->id, $area->id)
                        ->orderByDesc('scanned_at')
                        ->orderByDesc('id')
                        ->lockForUpdate()
                        ->first();

                    $nextSequence = $this->computeNextScanSequence($lastEvent);

                    try {
                        $scanEvent = $this->scanEventService->recordScan(
                            $ordenLocked,
                            $area,
                            $nextSequence,
                            $user->id,
                            null,
                            null,
                            $clientIp,
                            false
                        );
                    } catch (RuntimeException $e) {
                        LaundryAuditLogger::invalidScanSequence($e->getMessage(), [
                            'orden_id' => $ordenLocked->id,
                            'folio_fp' => LaundryAuditLogger::folioFingerprint($ordenLocked->folio_unico),
                            'area_id' => $area->id,
                            'user_id' => $user->id,
                        ]);

                        return [
                            'ok' => false,
                            'status' => 422,
                            'message' => $e->getMessage(),
                            'errors' => ['scan' => [$e->getMessage()]],
                        ];
                    }

                    $presence = $this->syncPresenceAfterScan($ordenLocked, $area, $nextSequence, $user);
                    $ordenLocked = $this->syncOrderFlowAfterScan($ordenLocked, $area, $nextSequence, $user);

                    return [
                        'ok' => true,
                        'data' => [
                            'orden' => [
                                'id' => $ordenLocked->id,
                                'folio_unico' => $ordenLocked->folio_unico,
                                'estatus' => $ordenLocked->estatus,
                                'current_step' => $ordenLocked->current_step,
                            ],
                            'area' => [
                                'id' => $area->id,
                                'code' => $area->code,
                                'name' => $area->name,
                            ],
                            'scan_sequence' => $nextSequence,
                            'scan_event' => [
                                'id' => $scanEvent->id,
                                'scan_sequence' => $scanEvent->scan_sequence,
                                'scanned_at' => $scanEvent->scanned_at?->toIso8601String(),
                            ],
                            'presence' => [
                                'id' => $presence->id,
                                'status' => $presence->status,
                                'started_at' => $presence->started_at?->toIso8601String(),
                                'completed_at' => $presence->completed_at?->toIso8601String(),
                            ],
                        ],
                    ];
                });
            });
        } catch (LockTimeoutException $e) {
            LaundryAuditLogger::scanDenied('lock_timeout', [
                'orden_id' => $orden->id,
                'folio_fp' => LaundryAuditLogger::folioFingerprint($orden->folio_unico),
                'area_code' => $areaCode,
                'user_id' => $user->id,
            ]);

            return [
                'ok' => false,
                'status' => 429,
                'message' => 'Demasiadas solicitudes para esta orden y área; reintente en unos segundos.',
                'errors' => ['lock' => ['No se pudo adquirir el bloqueo de proceso.']],
            ];
        }
    }

    private function resolveOrden(string $input): ?OrdenLavanderia
    {
        return \App\Support\LaundryScanResolver::findOrden($input);
    }

    private function computeNextScanSequence(?OrderAreaScanEvent $lastEvent): int
    {
        if ($lastEvent === null) {
            return OrderAreaScanEvent::SEQUENCE_FIRST;
        }

        if ($lastEvent->scan_sequence === OrderAreaScanEvent::SEQUENCE_FIRST) {
            return OrderAreaScanEvent::SEQUENCE_SECOND;
        }

        return OrderAreaScanEvent::SEQUENCE_FIRST;
    }

    private function syncPresenceAfterScan(
        OrdenLavanderia $orden,
        Area $area,
        int $scanSequence,
        User $user
    ): OrderAreaPresence {
        if ($scanSequence === OrderAreaScanEvent::SEQUENCE_FIRST) {
            return OrderAreaPresence::query()->updateOrCreate(
                [
                    'orden_lavanderia_id' => $orden->id,
                    'area_id' => $area->id,
                ],
                [
                    'status' => OrderAreaPresence::STATUS_IN_PROGRESS,
                    'started_at' => now(),
                    'started_by_user_id' => $user->id,
                    'completed_at' => null,
                    'completed_by_user_id' => null,
                ]
            );
        }

        $presence = OrderAreaPresence::query()->firstOrCreate(
            [
                'orden_lavanderia_id' => $orden->id,
                'area_id' => $area->id,
            ],
            [
                'status' => OrderAreaPresence::STATUS_IN_PROGRESS,
                'started_at' => now(),
                'started_by_user_id' => $user->id,
            ]
        );

        $presence->fill([
            'status' => OrderAreaPresence::STATUS_COMPLETED,
            'completed_at' => now(),
            'completed_by_user_id' => $user->id,
        ]);
        $presence->save();

        return $presence;
    }

    private function syncOrderFlowAfterScan(
        OrdenLavanderia $orden,
        Area $area,
        int $scanSequence,
        User $user
    ): OrdenLavanderia {
        if ($scanSequence === OrderAreaScanEvent::SEQUENCE_FIRST) {
            $updates = $this->buildOrderUpdatesForAreaStart($orden, $area, $user);
            if ($updates !== []) {
                $orden->fill($updates);
                $orden->save();
            }
            return $orden->refresh();
        }

        $nextArea = Area::query()
            ->operational()
            ->where('sort_order', '>', $area->sort_order)
            ->first();

        if ($nextArea !== null) {
            OrderAreaPresence::query()->updateOrCreate(
                [
                    'orden_lavanderia_id' => $orden->id,
                    'area_id' => $nextArea->id,
                ],
                [
                    'status' => OrderAreaPresence::STATUS_PENDING,
                    'started_at' => null,
                    'started_by_user_id' => null,
                    'completed_at' => null,
                    'completed_by_user_id' => null,
                ]
            );
        }

        $updates = $this->buildOrderUpdatesForAreaCompletion($orden, $area, $nextArea);
        if ($updates !== []) {
            $orden->fill($updates);
            $orden->save();
        }

        if (($updates['estatus'] ?? null) === OrdenLavanderia::ESTATUS_TERMINADO) {
            event(new OrdenLavanderiaTerminada($orden));
        }

        return $orden->refresh();
    }

    /**
     * @return array<string, mixed>
     */
    private function buildOrderUpdatesForAreaStart(OrdenLavanderia $orden, Area $area, User $user): array
    {
        $step = $this->stepForAreaCode($area->code);
        $updates = [];
        if ($orden->estatus === OrdenLavanderia::ESTATUS_PENDIENTE) {
            $updates['estatus'] = OrdenLavanderia::ESTATUS_EN_PROCESO;
        }
        if ($step !== null && $orden->current_step !== $step) {
            $updates['current_step'] = $step;
            $updates['current_step_started_at'] = now();
        }

        if ($step === OrdenLavanderia::STEP_LAVADO && $orden->lavado_started_at === null) {
            $updates['lavado_started_at'] = now();
            $updates['operador_lavado_id'] = $user->id;
        } elseif ($step === OrdenLavanderia::STEP_PLANCHADO && $orden->planchado_started_at === null) {
            $updates['planchado_started_at'] = now();
            $updates['operador_planchado_id'] = $user->id;
        }

        return $updates;
    }

    /**
     * @return array<string, mixed>
     */
    private function buildOrderUpdatesForAreaCompletion(OrdenLavanderia $orden, Area $area, ?Area $nextArea): array
    {
        $updates = [];
        $currentStepByArea = $this->stepForAreaCode($area->code);
        if ($currentStepByArea === OrdenLavanderia::STEP_LAVADO && $orden->lavado_finished_at === null) {
            $updates['lavado_finished_at'] = now();
        }
        if ($currentStepByArea === OrdenLavanderia::STEP_PLANCHADO && $orden->planchado_finished_at === null) {
            $updates['planchado_finished_at'] = now();
        }

        if ($nextArea === null) {
            $updates['current_step'] = OrdenLavanderia::STEP_TERMINADO;
            $updates['current_step_started_at'] = now();
            if ($orden->terminado_at === null) {
                $updates['terminado_at'] = now();
            }
            $updates['estatus'] = OrdenLavanderia::ESTATUS_TERMINADO;
            return $updates;
        }

        $nextStep = $this->stepForAreaCode($nextArea->code);
        if ($nextStep !== null && $orden->current_step !== $nextStep) {
            $updates['current_step'] = $nextStep;
            $updates['current_step_started_at'] = now();
        }

        return $updates;
    }

    private function stepForAreaCode(string $areaCode): ?string
    {
        return match ($areaCode) {
            'reception' => OrdenLavanderia::STEP_RECEPCION,
            'selection', 'heavy_wash', 'light_wash' => OrdenLavanderia::STEP_LAVADO,
            'ironing' => OrdenLavanderia::STEP_PLANCHADO,
            'storage', 'shipping' => OrdenLavanderia::STEP_TERMINADO,
            default => null,
        };
    }
}
