<?php

namespace App\Services;

use App\Models\Bien;
use App\Models\NumOperacion;
use App\Models\Operacion;
use App\Models\RegistroStock;
use App\Models\Usuario;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

class AlmacenService
{
    public function __construct(private readonly InventoryCostingService $inventoryCostingService)
    {
    }

    private const INVENTARIO_AJUSTE_REFERENCIA = 'Ajuste manual de inventario';

    /**
     * @return array<string, mixed>
     */
    public function buildCargaData(): array
    {
        $bienes = Bien::query()
            ->with('tipoInsumo')
            ->select([
                'codigo',
                'descripcion',
                'marca',
                'tipo',
                'cantidadUnidad',
                'unidad',
                'cantidadUnidadUtil',
                'unidadUtil',
                'relacionUnidades',
                'cantidadEntregada',
            ])
            ->orderBy('descripcion')
            ->get();

        $bienesData = $bienes->map(function (Bien $bien): array {
            return [
                'codigo' => $bien->codigo,
                'descripcion' => $bien->descripcion,
                'marca' => $bien->marca,
                'tipo' => $bien->tipoInsumo ? $bien->tipoInsumo->nombre : 'Sin tipo',
                'almacenado' => trim(((string) ($bien->cantidadUnidad ?? 0)) . ' ' . ((string) ($bien->unidad ?? ''))),
                'en_stock' => trim((string) ($bien->cantidadUnidadUtil ?? 0) . ' UNIDADES'),
                'stock_disponible' => (int) ($bien->cantidadUnidadUtil ?? 0),
                'relacion_unidades' => max(1, (int) ($bien->relacionUnidades ?? 1)),
                'entregado' => trim((string) ($bien->cantidadEntregada ?? 0) . ' UNIDADES'),
            ];
        })->values();

        return [
            'bienes' => $bienes,
            'bienesData' => $bienesData,
        ];
    }

    /**
     * @return array<string, mixed>
     */
    public function buildInventarioData(string $search = '', string $itemFilter = 'todos'): array
    {
        $search = trim($search);
        $itemFilter = in_array($itemFilter, ['todos', 'almacenados', 'stock', 'ambos'], true)
            ? $itemFilter
            : 'todos';

        $query = Bien::query()
            ->with('tipoInsumo')
            ->select([
                'id',
                'sap',
                'codigo',
                'descripcion',
                'marca',
                'tipo',
                'cantidadUnidad',
                'unidad',
                'cantidadUnidadUtil',
                'unidadUtil',
                'cantidadEntregada',
                'ubicacionEstante',
                'ubicacionFila',
            ])
            ->where(function ($builder): void {
                $builder
                    ->where('cantidadUnidad', '>', 0)
                    ->orWhere('cantidadUnidadUtil', '>', 0);
            });

        if ($itemFilter === 'almacenados') {
            $query->where('cantidadUnidad', '>', 0);
        }

        if ($itemFilter === 'stock') {
            $query->where('cantidadUnidadUtil', '>', 0);
        }

        if ($itemFilter === 'ambos') {
            $query
                ->where('cantidadUnidad', '>', 0)
                ->where('cantidadUnidadUtil', '>', 0);
        }

        if ($search !== '') {
            $query->where(function ($builder) use ($search): void {
                $builder
                    ->where('sap', 'like', '%' . $search . '%')
                    ->orWhere('codigo', 'like', '%' . $search . '%')
                    ->orWhere('descripcion', 'like', '%' . $search . '%')
                    ->orWhere('ubicacionEstante', 'like', '%' . $search . '%')
                    ->orWhere('ubicacionFila', 'like', '%' . $search . '%')
                    ->orWhereHas('tipoInsumo', function ($tipoQuery) use ($search): void {
                        $tipoQuery->where('nombre', 'like', '%' . $search . '%');
                    });
            });
        }

        $summary = [
            'items' => (clone $query)->count(),
            'almacenado' => (int) ((clone $query)->sum('cantidadUnidad') ?? 0),
            'stock' => (int) ((clone $query)->sum('cantidadUnidadUtil') ?? 0),
            'entregado' => (int) ((clone $query)->sum('cantidadEntregada') ?? 0),
        ];

        $rows = $query
            ->orderBy('descripcion')
            ->paginate(20)
            ->withQueryString();

        return [
            'rows' => $rows,
            'search' => $search,
            'itemFilter' => $itemFilter,
            'summary' => $summary,
        ];
    }

    /**
     * @param array<string, mixed> $validated
     * @return array<string, mixed>
     */
    public function actualizar(array $validated, ?int $authId): array
    {
        $editor = $this->resolveAuthorizedEditor($authId);

        if (! $editor) {
            return [
                'ok' => false,
                'warning' => 'La cedula del usuario que autoriza no tiene permisos para actualizar almacen.',
            ];
        }

        $cedulaEditor = (string) $editor->cedula;
        $items = collect($validated['items'] ?? [])->values();
        $resultadoItems = [];
        $contProcesados = 0;
        $contOmitidos = 0;
        $fechaOperacion = now();
        $hasCantidadSap = Schema::hasColumn('bienes', 'cantidadSap');
        $hasCantidadRotacion = Schema::hasColumn('bienes', 'cantidadRotacion');

        $numeroOperacion = DB::transaction(function () use (
            $items,
            $cedulaEditor,
            $fechaOperacion,
            $hasCantidadSap,
            $hasCantidadRotacion,
            &$resultadoItems,
            &$contProcesados,
            &$contOmitidos
        ): int {
            $numOperacion = NumOperacion::query()->lockForUpdate()->first();

            if (! $numOperacion) {
                $numOperacion = NumOperacion::query()->create(['numero' => 0]);
            }

            $numero = (int) $numOperacion->numero + 1;
            $numOperacion->update(['numero' => $numero]);

            foreach ($items as $item) {
                $codigo = (string) ($item['codigo'] ?? '');
                $cantidadAlmacenado = (int) ($item['cantidad_almacenado'] ?? 0);
                $cantidadStock = (int) ($item['cantidad_stock'] ?? 0);
                $observacion = trim((string) ($item['observacion'] ?? ''));

                if ($cantidadAlmacenado === 0 && $cantidadStock === 0) {
                    $contOmitidos++;
                    continue;
                }

                $bien = Bien::query()->where('codigo', $codigo)->lockForUpdate()->first();

                if (! $bien) {
                    $contOmitidos++;
                    continue;
                }

                $cantidadUnidadAntes = (int) ($bien->cantidadUnidad ?? 0);
                $cantidadUnidadUtilAntes = (int) ($bien->cantidadUnidadUtil ?? 0);
                $cantidadSapAntes = $hasCantidadSap
                    ? (int) ($bien->getAttribute('cantidadSap') ?? 0)
                    : 0;
                $cantidadRotacionAntes = $hasCantidadRotacion
                    ? (int) ($bien->getAttribute('cantidadRotacion') ?? 0)
                    : 0;
                $relacionUnidades = max(1, (int) ($bien->relacionUnidades ?? 1));

                $cantidadUnidad = $cantidadUnidadAntes;
                $cantidadUnidadUtil = $cantidadUnidadUtilAntes;
                $cantidadSap = $cantidadSapAntes;
                $cantidadRotacion = $cantidadRotacionAntes;

                $cantidadUnidad = $cantidadUnidadAntes + $cantidadAlmacenado;
                $stockDerivadoDeAlmacenado = $cantidadAlmacenado * $relacionUnidades;
                $cantidadUnidadUtil = $cantidadUnidadUtilAntes + $stockDerivadoDeAlmacenado + $cantidadStock;

                if ($hasCantidadSap || $hasCantidadRotacion) {
                    [$cantidadSap, $cantidadRotacion] = $this->rebalanceSourceQuantities(
                        sapActual: $cantidadSapAntes,
                        rotacionActual: $cantidadRotacionAntes,
                        nuevoAlmacenado: $cantidadUnidad,
                        hasCantidadSap: $hasCantidadSap,
                        hasCantidadRotacion: $hasCantidadRotacion,
                    );
                }

                if ($cantidadUnidadUtil < 0 || $cantidadUnidad < 0 || $cantidadSap < 0 || $cantidadRotacion < 0) {
                    $contOmitidos++;
                    continue;
                }

                $updateBien = [
                    'cantidadUnidad' => $cantidadUnidad,
                    'cantidadUnidadUtil' => $cantidadUnidadUtil,
                    'fechaActualizacion' => $fechaOperacion,
                    'cedulaEditor' => $cedulaEditor,
                ];

                if ($hasCantidadSap) {
                    $updateBien['cantidadSap'] = $cantidadSap;
                }

                if ($hasCantidadRotacion) {
                    $updateBien['cantidadRotacion'] = $cantidadRotacion;
                }

                $bien->update($updateBien);

                $deltaStock = $cantidadUnidadUtil - $cantidadUnidadUtilAntes;
                if ($deltaStock > 0) {
                    $this->inventoryCostingService->registerIngreso(
                        bien: $bien,
                        cantidad: $deltaStock,
                        numOperacion: $numero,
                        referencia: 'Ingreso almacen (carga simultanea)',
                        fechaMovimiento: $fechaOperacion,
                        cedulaResponsable: $cedulaEditor,
                    );
                } elseif ($deltaStock < 0) {
                    $this->inventoryCostingService->consumeSalida(
                        bien: $bien,
                        cantidad: abs($deltaStock),
                        numOperacion: $numero,
                        referencia: 'Salida almacen (carga simultanea)',
                        fechaMovimiento: $fechaOperacion,
                        cedulaResponsable: $cedulaEditor,
                    );
                }

                if ($cantidadAlmacenado !== 0) {
                    RegistroStock::query()->create([
                        'numOperacion' => $numero,
                        'codigo' => $codigo,
                        'cantidad' => $cantidadAlmacenado,
                        'objetivo' => 1,
                        'fechaOperacion' => $fechaOperacion,
                        'observacion' => $observacion !== '' ? $observacion : null,
                        'cedulaResponsable' => $cedulaEditor,
                    ]);
                }

                if ($cantidadStock !== 0) {
                    RegistroStock::query()->create([
                        'numOperacion' => $numero,
                        'codigo' => $codigo,
                        'cantidad' => $cantidadStock,
                        'objetivo' => 2,
                        'fechaOperacion' => $fechaOperacion,
                        'observacion' => $observacion !== '' ? $observacion : null,
                        'cedulaResponsable' => $cedulaEditor,
                    ]);
                }

                $contProcesados++;
                $resultadoItems[] = [
                    'codigo' => $codigo,
                    'descripcion' => (string) ($bien->descripcion ?? ($item['descripcion'] ?? '')),
                    'cantidad_almacenado' => $cantidadAlmacenado,
                    'cantidad_stock' => $cantidadStock,
                    'observacion' => $observacion,
                    'almacenado_antes' => $cantidadUnidadAntes,
                    'almacenado_despues' => $cantidadUnidad,
                    'stock_antes' => $cantidadUnidadUtilAntes,
                    'stock_despues' => $cantidadUnidadUtil,
                ];
            }

            Operacion::query()->create([
                'numero' => $numero,
                'tipo' => 4,
                ...(
                    Schema::hasColumn('operaciones', 'cedulaDespacha')
                        ? ['cedulaDespacha' => $cedulaEditor]
                        : []
                ),
            ]);

            return $numero;
        });

        return [
            'ok' => true,
            'numeroOperacion' => $numeroOperacion,
            'items' => $items,
            'resultadoItems' => $resultadoItems,
            'contProcesados' => $contProcesados,
            'contOmitidos' => $contOmitidos,
        ];
    }

    /**
     * @param array<string, mixed> $validated
     * @return array<string, mixed>
     */
    public function updateInventarioItem(int $id, array $validated, ?int $authId): array
    {
        $editor = $this->resolveAuthorizedEditor($authId);

        if (! $editor) {
            return [
                'ok' => false,
                'warning' => 'La cedula del usuario que autoriza no tiene permisos para actualizar almacen.',
            ];
        }

        $cantidades = [
            'cantidadUnidad' => (int) ($validated['cantidad_almacenada'] ?? 0),
            'cantidadUnidadUtil' => (int) ($validated['cantidad_stock'] ?? 0),
            'cantidadEntregada' => (int) ($validated['cantidad_entregada'] ?? 0),
        ];

        return $this->persistInventarioItem(
            id: $id,
            cantidades: $cantidades,
            editor: $editor,
            referenciaMovimiento: self::INVENTARIO_AJUSTE_REFERENCIA,
            statusMessage: 'Item de inventario actualizado correctamente.'
        );
    }

    /**
     * @return array<string, mixed>
     */
    public function destroyInventarioItem(int $id, ?int $authId): array
    {
        $editor = $this->resolveAuthorizedEditor($authId);

        if (! $editor) {
            return [
                'ok' => false,
                'warning' => 'La cedula del usuario que autoriza no tiene permisos para actualizar almacen.',
            ];
        }

        return $this->persistInventarioItem(
            id: $id,
            cantidades: [
                'cantidadUnidad' => 0,
                'cantidadUnidadUtil' => 0,
                'cantidadEntregada' => 0,
            ],
            editor: $editor,
            referenciaMovimiento: 'Eliminacion manual de inventario',
            statusMessage: 'Item eliminado del inventario.'
        );
    }

    /**
     * @param array<string, int> $cantidades
     * @return array<string, mixed>
     */
    private function persistInventarioItem(int $id, array $cantidades, Usuario $editor, string $referenciaMovimiento, string $statusMessage): array
    {
        $cedulaEditor = trim((string) $editor->cedula);

        if ($cedulaEditor === '') {
            return [
                'ok' => false,
                'warning' => 'Error en uno de los registros de la base de datos.',
            ];
        }

        $fechaOperacion = now();
        $hasCantidadSap = Schema::hasColumn('bienes', 'cantidadSap');
        $hasCantidadRotacion = Schema::hasColumn('bienes', 'cantidadRotacion');

        DB::transaction(function () use (
            $id,
            $cantidades,
            $cedulaEditor,
            $fechaOperacion,
            $hasCantidadSap,
            $hasCantidadRotacion,
            $referenciaMovimiento
        ): void {
            $bien = Bien::query()->lockForUpdate()->find($id);

            if (! $bien) {
                throw new \RuntimeException('El item seleccionado ya no existe.');
            }

            $cantidadUnidadAntes = (int) ($bien->cantidadUnidad ?? 0);
            $cantidadUnidadUtilAntes = (int) ($bien->cantidadUnidadUtil ?? 0);
            $cantidadEntregadaAntes = (int) ($bien->cantidadEntregada ?? 0);

            $cantidadUnidad = max(0, (int) ($cantidades['cantidadUnidad'] ?? 0));
            $cantidadUnidadUtil = max(0, (int) ($cantidades['cantidadUnidadUtil'] ?? 0));
            $cantidadEntregada = max(0, (int) ($cantidades['cantidadEntregada'] ?? 0));

            $updateBien = [
                'cantidadUnidad' => $cantidadUnidad,
                'cantidadUnidadUtil' => $cantidadUnidadUtil,
                'cantidadEntregada' => $cantidadEntregada,
                'fechaActualizacion' => $fechaOperacion,
                'cedulaEditor' => $cedulaEditor,
            ];

            if ($hasCantidadSap || $hasCantidadRotacion) {
                [$cantidadSap, $cantidadRotacion] = $this->rebalanceSourceQuantities(
                    sapActual: $hasCantidadSap ? (int) ($bien->getAttribute('cantidadSap') ?? 0) : 0,
                    rotacionActual: $hasCantidadRotacion ? (int) ($bien->getAttribute('cantidadRotacion') ?? 0) : 0,
                    nuevoAlmacenado: $cantidadUnidad,
                    hasCantidadSap: $hasCantidadSap,
                    hasCantidadRotacion: $hasCantidadRotacion,
                );

                if ($hasCantidadSap) {
                    $updateBien['cantidadSap'] = $cantidadSap;
                }

                if ($hasCantidadRotacion) {
                    $updateBien['cantidadRotacion'] = $cantidadRotacion;
                }
            }

            $bien->update($updateBien);

            $deltaStock = $cantidadUnidadUtil - $cantidadUnidadUtilAntes;
            if ($deltaStock === 0) {
                return;
            }

            $numeroOperacion = $this->nextOperationNumber();

            Operacion::query()->create([
                'numero' => $numeroOperacion,
                'tipo' => 4,
                ...(
                    Schema::hasColumn('operaciones', 'cedulaDespacha')
                        ? ['cedulaDespacha' => $cedulaEditor]
                        : []
                ),
            ]);

            $bien->forceFill($updateBien);

            if ($deltaStock > 0) {
                $this->inventoryCostingService->registerIngreso(
                    bien: $bien,
                    cantidad: $deltaStock,
                    numOperacion: $numeroOperacion,
                    referencia: $referenciaMovimiento,
                    fechaMovimiento: $fechaOperacion,
                    cedulaResponsable: $cedulaEditor,
                );

                return;
            }

            $this->inventoryCostingService->consumeSalida(
                bien: $bien,
                cantidad: abs($deltaStock),
                numOperacion: $numeroOperacion,
                referencia: $referenciaMovimiento,
                fechaMovimiento: $fechaOperacion,
                cedulaResponsable: $cedulaEditor,
            );
        });

        return [
            'ok' => true,
            'status' => $statusMessage,
        ];
    }

    private function resolveAuthorizedEditor(?int $authId): ?Usuario
    {
        $editorId = (int) ($authId ?? 0);
        $editor = $editorId > 0 ? Usuario::query()->find($editorId) : null;

        if (! $editor || trim((string) $editor->cedula) === '') {
            return null;
        }

        if (! method_exists($editor, 'canAccessAlmacen') || ! $editor->canAccessAlmacen()) {
            return null;
        }

        return $editor;
    }

    private function nextOperationNumber(): int
    {
        $numOperacion = NumOperacion::query()->lockForUpdate()->first();

        if (! $numOperacion) {
            $numOperacion = NumOperacion::query()->create(['numero' => 0]);
        }

        $numero = (int) $numOperacion->numero + 1;
        $numOperacion->update(['numero' => $numero]);

        return $numero;
    }

    /**
     * @return array{0: int, 1: int}
     */
    private function rebalanceSourceQuantities(
        int $sapActual,
        int $rotacionActual,
        int $nuevoAlmacenado,
        bool $hasCantidadSap,
        bool $hasCantidadRotacion
    ): array {
        $nuevoAlmacenado = max(0, $nuevoAlmacenado);

        if (! $hasCantidadSap && ! $hasCantidadRotacion) {
            return [0, 0];
        }

        if ($hasCantidadSap && ! $hasCantidadRotacion) {
            return [$nuevoAlmacenado, 0];
        }

        if (! $hasCantidadSap && $hasCantidadRotacion) {
            return [0, $nuevoAlmacenado];
        }

        $sapActual = max(0, $sapActual);
        $rotacionActual = max(0, $rotacionActual);
        $totalActual = $sapActual + $rotacionActual;

        if ($nuevoAlmacenado === 0) {
            return [0, 0];
        }

        if ($totalActual <= 0) {
            return [$nuevoAlmacenado, 0];
        }

        $nuevoSap = (int) round($nuevoAlmacenado * ($sapActual / $totalActual));
        $nuevoSap = max(0, min($nuevoAlmacenado, $nuevoSap));
        $nuevoRotacion = $nuevoAlmacenado - $nuevoSap;

        return [$nuevoSap, $nuevoRotacion];
    }
}
