<?php

namespace App\Http\Controllers;

use App\Models\AperturaCaja;
use App\Models\Caja;
use App\Models\CierreCaja;
use App\Models\MovimientoCaja;
use App\Models\TraspasoCaja;
use App\Models\Venta;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;

class CajaController extends Controller
{
    private const POS_SALE_MOVEMENT_PREFIX = 'Venta POS #';

    public function index(): View
    {
        $empresaId = tenant_empresa_id();
        abort_if(! $empresaId, 403, 'No hay empresa activa.');

        $userId = (int) (auth_user()?->id ?? 0);
        $tenantSucursalId = tenant_sucursal_id();

        $current = $this->resolveCurrentOpening($empresaId, $userId);

        $currentSales = [
            'ventas_count' => 0,
            'ventas_total' => 0.0,
        ];
        $currentAdjustments = [
            'total_ingresos' => 0.0,
            'total_egresos' => 0.0,
            'neto_ajustes' => 0.0,
        ];
        $currentMovements = collect();
        $pendingIncomingTransfers = collect();
        $pendingOutgoingTransfers = collect();
        $transferTargets = collect();
        $currentExpected = 0.0;

        if ($current) {
            $currentSales = $this->currentSalesSummary($empresaId, (int) $current->id);
            $currentAdjustments = $this->adjustmentSummary($empresaId, (int) $current->id, true);
            $currentExpected = $this->expectedForOpening($current);

            $currentMovements = MovimientoCaja::query()
                ->where('empresa_id', $empresaId)
                ->where('apertura_id', $current->id)
                ->with('usuario')
                ->latest('id')
                ->limit(30)
                ->get();

            $pendingIncomingTransfers = TraspasoCaja::query()
                ->where('empresa_id', $empresaId)
                ->where('apertura_destino_id', $current->id)
                ->where('estado', 'pendiente')
                ->with(['usuarioOrigen', 'aperturaOrigen.caja.sucursal'])
                ->latest('id')
                ->limit(20)
                ->get();

            $pendingOutgoingTransfers = TraspasoCaja::query()
                ->where('empresa_id', $empresaId)
                ->where('apertura_origen_id', $current->id)
                ->where('estado', 'pendiente')
                ->with(['usuarioDestino', 'aperturaDestino.caja.sucursal'])
                ->latest('id')
                ->limit(20)
                ->get();

            $transferTargets = AperturaCaja::query()
                ->where('empresa_id', $empresaId)
                ->where('estado', 'abierta')
                ->where('id', '!=', $current->id)
                ->with(['usuario', 'caja.sucursal'])
                ->orderByDesc('id')
                ->get();
        }

        $canViewAllClosings = has_permission('reportes.ver');

        $historyQuery = CierreCaja::query()
            ->from('cierres_caja as c')
            ->join('aperturas_caja as a', 'a.id', '=', 'c.apertura_id')
            ->leftJoin('cajas as caja', 'caja.id', '=', 'a.caja_id')
            ->leftJoin('sucursales as s', 's.id', '=', 'caja.sucursal_id')
            ->leftJoin('usuarios as u', 'u.id', '=', 'c.usuario_id')
            ->where('c.empresa_id', $empresaId)
            ->select([
                'c.id',
                'c.apertura_id',
                'c.monto_esperado',
                'c.monto_real',
                'c.diferencia',
                'c.created_at as fecha_cierre',
                'a.monto_apertura',
                'a.notas',
                'caja.nombre as caja',
                's.nombre as sucursal',
                'u.nombre as cajero',
            ])
            ->orderByDesc('c.id');

        if (! $canViewAllClosings) {
            $historyQuery->where('c.usuario_id', $userId);
        }

        if ($tenantSucursalId) {
            $historyQuery->where('s.id', $tenantSucursalId);
        }

        $history = $historyQuery
            ->limit(120)
            ->get();

        $historySummary = [
            'total_cierres' => $history->count(),
            'total_esperado' => (float) $history->sum('monto_esperado'),
            'total_contado' => (float) $history->sum('monto_real'),
            'total_diferencia' => (float) $history->sum('diferencia'),
        ];

        $cashBoxes = Caja::query()
            ->where('empresa_id', $empresaId)
            ->where('estado', 'activa')
            ->when($tenantSucursalId, static function ($query, int $sucursalId): void {
                $query->where(static function ($nested) use ($sucursalId): void {
                    $nested->whereNull('sucursal_id')
                        ->orWhere('sucursal_id', $sucursalId);
                });
            })
            ->with('sucursal')
            ->orderBy('nombre')
            ->get();

        $branches = DB::table('sucursales')
            ->where('empresa_id', $empresaId)
            ->orderBy('nombre')
            ->get(['id', 'nombre']);

        return view('modules.caja.index', [
            'current' => $current,
            'currentSales' => $currentSales,
            'currentAdjustments' => $currentAdjustments,
            'currentMovements' => $currentMovements,
            'pendingIncomingTransfers' => $pendingIncomingTransfers,
            'pendingOutgoingTransfers' => $pendingOutgoingTransfers,
            'transferTargets' => $transferTargets,
            'currentExpected' => $currentExpected,
            'history' => $history,
            'historySummary' => $historySummary,
            'cashBoxes' => $cashBoxes,
            'branches' => $branches,
            'canViewAllClosings' => $canViewAllClosings,
        ]);
    }

    public function open(Request $request): RedirectResponse
    {
        $empresaId = tenant_empresa_id();
        abort_if(! $empresaId, 403, 'No hay empresa activa.');

        $userId = (int) (auth_user()?->id ?? 0);
        abort_if($userId <= 0, 403, 'Usuario invalido para abrir caja.');

        $tenantSucursalId = tenant_sucursal_id();

        $validated = $request->validate([
            'caja_id' => ['required', 'integer'],
            'monto_apertura' => ['required', 'numeric', 'min:0'],
            'notas' => ['nullable', 'string'],
        ]);

        DB::transaction(function () use ($empresaId, $userId, $tenantSucursalId, $validated): void {
            $existingOpening = $this->resolveCurrentOpening($empresaId, $userId, true);
            if ($existingOpening) {
                throw ValidationException::withMessages([
                    'caja_id' => 'Ya tienes una caja abierta.',
                ]);
            }

            $caja = Caja::query()
                ->whereKey((int) $validated['caja_id'])
                ->where('empresa_id', $empresaId)
                ->where('estado', 'activa')
                ->lockForUpdate()
                ->first();

            if (! $caja) {
                throw ValidationException::withMessages([
                    'caja_id' => 'La caja seleccionada no pertenece a la empresa activa.',
                ]);
            }

            if (
                $tenantSucursalId
                && $caja->sucursal_id
                && (int) $caja->sucursal_id !== (int) $tenantSucursalId
            ) {
                throw ValidationException::withMessages([
                    'caja_id' => 'La caja no pertenece a la sucursal activa.',
                ]);
            }

            $inUse = AperturaCaja::query()
                ->where('empresa_id', $empresaId)
                ->where('caja_id', $caja->id)
                ->where('estado', 'abierta')
                ->lockForUpdate()
                ->exists();

            if ($inUse) {
                throw ValidationException::withMessages([
                    'caja_id' => 'La caja ya tiene una apertura activa.',
                ]);
            }

            AperturaCaja::query()->create([
                'empresa_id' => $empresaId,
                'caja_id' => $caja->id,
                'usuario_id' => $userId,
                'monto_apertura' => (float) $validated['monto_apertura'],
                'monto_cierre' => null,
                'diferencia' => 0,
                'estado' => 'abierta',
                'fecha_apertura' => now(),
                'fecha_cierre' => null,
                'notas' => $validated['notas'] ?? null,
                'created_at' => now(),
            ]);
        });

        return redirect()->route('caja.index')
            ->with('status', 'Caja abierta correctamente.');
    }

    public function movement(Request $request): RedirectResponse
    {
        $empresaId = tenant_empresa_id();
        abort_if(! $empresaId, 403, 'No hay empresa activa.');

        $userId = (int) (auth_user()?->id ?? 0);
        abort_if($userId <= 0, 403, 'Usuario invalido para registrar movimiento.');

        $validated = $request->validate([
            'tipo' => ['required', 'string', 'in:ingreso,egreso'],
            'concepto' => ['required', 'string', 'max:160'],
            'monto' => ['required', 'numeric', 'gt:0'],
            'observacion' => ['nullable', 'string'],
        ]);

        DB::transaction(function () use ($empresaId, $userId, $validated): void {
            $apertura = $this->resolveCurrentOpening($empresaId, $userId, true);
            if (! $apertura) {
                throw ValidationException::withMessages([
                    'tipo' => 'Debes abrir caja antes de registrar movimientos.',
                ]);
            }

            $monto = (float) $validated['monto'];
            if ((string) $validated['tipo'] === 'egreso') {
                $disponible = $this->expectedForOpening($apertura);
                if ($monto > $disponible) {
                    throw ValidationException::withMessages([
                        'monto' => 'El egreso supera el disponible esperado en caja.',
                    ]);
                }
            }

            MovimientoCaja::query()->create([
                'empresa_id' => $empresaId,
                'apertura_id' => $apertura->id,
                'usuario_id' => $userId,
                'tipo' => $validated['tipo'],
                'concepto' => trim((string) $validated['concepto']),
                'monto' => $monto,
                'observacion' => $validated['observacion'] ?? null,
                'created_at' => now(),
            ]);
        });

        return redirect()->route('caja.index')
            ->with('status', 'Movimiento de caja registrado correctamente.');
    }

    public function transfer(Request $request): RedirectResponse
    {
        $empresaId = tenant_empresa_id();
        abort_if(! $empresaId, 403, 'No hay empresa activa.');

        $userId = (int) (auth_user()?->id ?? 0);
        abort_if($userId <= 0, 403, 'Usuario invalido para registrar traspaso.');

        $validated = $request->validate([
            'apertura_destino_id' => ['required', 'integer'],
            'monto' => ['required', 'numeric', 'gt:0'],
            'concepto' => ['required', 'string', 'max:160'],
            'observacion' => ['nullable', 'string'],
        ]);

        DB::transaction(function () use ($empresaId, $userId, $validated): void {
            $aperturaOrigen = $this->resolveCurrentOpening($empresaId, $userId, true);
            if (! $aperturaOrigen) {
                throw ValidationException::withMessages([
                    'apertura_destino_id' => 'Debes abrir caja antes de solicitar traspasos.',
                ]);
            }

            $aperturaDestino = AperturaCaja::query()
                ->where('empresa_id', $empresaId)
                ->where('estado', 'abierta')
                ->whereKey((int) $validated['apertura_destino_id'])
                ->lockForUpdate()
                ->first();

            if (! $aperturaDestino) {
                throw ValidationException::withMessages([
                    'apertura_destino_id' => 'La caja destino no tiene una apertura activa.',
                ]);
            }

            if ((int) $aperturaDestino->id === (int) $aperturaOrigen->id) {
                throw ValidationException::withMessages([
                    'apertura_destino_id' => 'Debes seleccionar una caja destino diferente.',
                ]);
            }

            $disponible = $this->expectedForOpening($aperturaOrigen);
            $monto = (float) $validated['monto'];
            if ($monto > $disponible) {
                throw ValidationException::withMessages([
                    'monto' => 'El traspaso supera el disponible esperado de la caja origen.',
                ]);
            }

            TraspasoCaja::query()->create([
                'empresa_id' => $empresaId,
                'apertura_origen_id' => $aperturaOrigen->id,
                'apertura_destino_id' => $aperturaDestino->id,
                'usuario_origen_id' => $userId,
                'usuario_destino_id' => (int) $aperturaDestino->usuario_id,
                'monto' => $monto,
                'estado' => 'pendiente',
                'concepto' => trim((string) $validated['concepto']),
                'observacion' => $validated['observacion'] ?? null,
                'respondido_por_id' => null,
                'respondido_at' => null,
                'observacion_respuesta' => null,
                'created_at' => now(),
            ]);
        });

        return redirect()->route('caja.index')
            ->with('status', 'Traspaso enviado para aprobación.');
    }

    public function approveTransfer(Request $request, TraspasoCaja $traspaso): RedirectResponse
    {
        $empresaId = tenant_empresa_id();
        abort_if(! $empresaId, 403, 'No hay empresa activa.');

        $userId = (int) (auth_user()?->id ?? 0);
        abort_if($userId <= 0, 403, 'Usuario invalido para aprobar traspaso.');

        $validated = $request->validate([
            'observacion_respuesta' => ['nullable', 'string'],
        ]);

        DB::transaction(function () use ($empresaId, $userId, $traspaso, $validated): void {
            $transfer = TraspasoCaja::query()
                ->whereKey($traspaso->id)
                ->where('empresa_id', $empresaId)
                ->lockForUpdate()
                ->first();

            if (! $transfer) {
                abort(404, 'Traspaso no encontrado en la empresa activa.');
            }

            if ((int) $transfer->usuario_destino_id !== $userId) {
                abort(403, 'Solo el usuario destino puede aprobar este traspaso.');
            }

            if ((string) $transfer->estado !== 'pendiente') {
                throw ValidationException::withMessages([
                    'traspaso' => 'El traspaso ya fue respondido.',
                ]);
            }

            $aperturaOrigen = AperturaCaja::query()
                ->where('empresa_id', $empresaId)
                ->where('estado', 'abierta')
                ->whereKey((int) $transfer->apertura_origen_id)
                ->lockForUpdate()
                ->first();
            $aperturaDestino = AperturaCaja::query()
                ->where('empresa_id', $empresaId)
                ->where('estado', 'abierta')
                ->whereKey((int) $transfer->apertura_destino_id)
                ->lockForUpdate()
                ->first();

            if (! $aperturaOrigen || ! $aperturaDestino) {
                throw ValidationException::withMessages([
                    'traspaso' => 'La apertura origen o destino ya no está activa.',
                ]);
            }

            $monto = (float) $transfer->monto;
            $disponible = $this->expectedForOpening($aperturaOrigen);
            if ($monto > $disponible) {
                throw ValidationException::withMessages([
                    'traspaso' => 'La caja origen ya no tiene saldo disponible para aprobar.',
                ]);
            }

            $transfer->fill([
                'estado' => 'aprobado',
                'respondido_por_id' => $userId,
                'respondido_at' => now(),
                'observacion_respuesta' => $validated['observacion_respuesta'] ?? null,
            ]);
            $transfer->save();

            MovimientoCaja::query()->create([
                'empresa_id' => $empresaId,
                'apertura_id' => $aperturaOrigen->id,
                'usuario_id' => (int) $transfer->usuario_origen_id,
                'tipo' => 'egreso',
                'concepto' => 'Traspaso enviado #'.$transfer->id,
                'monto' => $monto,
                'observacion' => 'traspaso_id='.$transfer->id.';destino_apertura='.$aperturaDestino->id,
                'created_at' => now(),
            ]);

            MovimientoCaja::query()->create([
                'empresa_id' => $empresaId,
                'apertura_id' => $aperturaDestino->id,
                'usuario_id' => (int) $transfer->usuario_destino_id,
                'tipo' => 'ingreso',
                'concepto' => 'Traspaso recibido #'.$transfer->id,
                'monto' => $monto,
                'observacion' => 'traspaso_id='.$transfer->id.';origen_apertura='.$aperturaOrigen->id,
                'created_at' => now(),
            ]);
        });

        return redirect()->route('caja.index')
            ->with('status', 'Traspaso aprobado correctamente.');
    }

    public function rejectTransfer(Request $request, TraspasoCaja $traspaso): RedirectResponse
    {
        $empresaId = tenant_empresa_id();
        abort_if(! $empresaId, 403, 'No hay empresa activa.');

        $userId = (int) (auth_user()?->id ?? 0);
        abort_if($userId <= 0, 403, 'Usuario invalido para rechazar traspaso.');

        $validated = $request->validate([
            'observacion_respuesta' => ['nullable', 'string'],
        ]);

        DB::transaction(function () use ($empresaId, $userId, $traspaso, $validated): void {
            $transfer = TraspasoCaja::query()
                ->whereKey($traspaso->id)
                ->where('empresa_id', $empresaId)
                ->lockForUpdate()
                ->first();

            if (! $transfer) {
                abort(404, 'Traspaso no encontrado en la empresa activa.');
            }

            if ((int) $transfer->usuario_destino_id !== $userId) {
                abort(403, 'Solo el usuario destino puede rechazar este traspaso.');
            }

            if ((string) $transfer->estado !== 'pendiente') {
                throw ValidationException::withMessages([
                    'traspaso' => 'El traspaso ya fue respondido.',
                ]);
            }

            $transfer->fill([
                'estado' => 'rechazado',
                'respondido_por_id' => $userId,
                'respondido_at' => now(),
                'observacion_respuesta' => $validated['observacion_respuesta'] ?? null,
            ]);
            $transfer->save();
        });

        return redirect()->route('caja.index')
            ->with('status', 'Traspaso rechazado.');
    }

    public function close(Request $request): RedirectResponse
    {
        $empresaId = tenant_empresa_id();
        abort_if(! $empresaId, 403, 'No hay empresa activa.');

        $userId = (int) (auth_user()?->id ?? 0);
        abort_if($userId <= 0, 403, 'Usuario invalido para cerrar caja.');

        $validated = $request->validate([
            'monto_real' => ['required', 'numeric', 'min:0'],
            'observacion_cierre' => ['nullable', 'string'],
        ]);

        DB::transaction(function () use ($empresaId, $userId, $validated): void {
            $apertura = $this->resolveCurrentOpening($empresaId, $userId, true);
            if (! $apertura) {
                throw ValidationException::withMessages([
                    'monto_real' => 'No tienes una caja abierta para cerrar.',
                ]);
            }

            $montoEsperado = $this->expectedForOpening($apertura);
            $montoReal = (float) $validated['monto_real'];
            $diferencia = round($montoReal - $montoEsperado, 2);

            CierreCaja::query()->create([
                'empresa_id' => $empresaId,
                'apertura_id' => $apertura->id,
                'usuario_id' => $userId,
                'monto_esperado' => $montoEsperado,
                'monto_real' => $montoReal,
                'diferencia' => $diferencia,
                'created_at' => now(),
            ]);

            $notasCierre = trim((string) ($validated['observacion_cierre'] ?? ''));
            $notasActuales = trim((string) ($apertura->notas ?? ''));

            $apertura->fill([
                'estado' => 'cerrada',
                'monto_cierre' => $montoReal,
                'diferencia' => $diferencia,
                'fecha_cierre' => now(),
                'notas' => $notasCierre === ''
                    ? ($notasActuales === '' ? null : $notasActuales)
                    : ($notasActuales === '' ? $notasCierre : $notasActuales."\n".$notasCierre),
            ]);
            $apertura->save();
        });

        return redirect()->route('caja.index')
            ->with('status', 'Caja cerrada correctamente.');
    }

    private function resolveCurrentOpening(int $empresaId, int $userId, bool $forUpdate = false): ?AperturaCaja
    {
        $query = AperturaCaja::query()
            ->where('empresa_id', $empresaId)
            ->where('usuario_id', $userId)
            ->where('estado', 'abierta')
            ->with(['caja.sucursal'])
            ->orderByDesc('id');

        if ($forUpdate) {
            $query->lockForUpdate();
        }

        return $query->first();
    }

    /**
     * @return array{ventas_count: int, ventas_total: float}
     */
    private function currentSalesSummary(int $empresaId, int $aperturaId): array
    {
        return [
            'ventas_count' => (int) Venta::query()
                ->where('empresa_id', $empresaId)
                ->where('apertura_caja_id', $aperturaId)
                ->count(),
            'ventas_total' => (float) Venta::query()
                ->where('empresa_id', $empresaId)
                ->where('apertura_caja_id', $aperturaId)
                ->sum('total'),
        ];
    }

    /**
     * @return array{total_ingresos: float, total_egresos: float, neto_ajustes: float}
     */
    private function adjustmentSummary(int $empresaId, int $aperturaId, bool $excludePosSales): array
    {
        $baseQuery = MovimientoCaja::query()
            ->where('empresa_id', $empresaId)
            ->where('apertura_id', $aperturaId);

        if ($excludePosSales) {
            $baseQuery->where('concepto', 'not like', self::POS_SALE_MOVEMENT_PREFIX.'%');
        }

        $totalIngresos = (float) (clone $baseQuery)
            ->where('tipo', 'ingreso')
            ->sum('monto');
        $totalEgresos = (float) (clone $baseQuery)
            ->where('tipo', 'egreso')
            ->sum('monto');

        return [
            'total_ingresos' => $totalIngresos,
            'total_egresos' => $totalEgresos,
            'neto_ajustes' => $totalIngresos - $totalEgresos,
        ];
    }

    private function expectedForOpening(AperturaCaja $apertura): float
    {
        $empresaId = (int) $apertura->empresa_id;
        $aperturaId = (int) $apertura->id;

        $sales = $this->currentSalesSummary($empresaId, $aperturaId);
        $adjustments = $this->adjustmentSummary($empresaId, $aperturaId, true);

        return (float) $apertura->monto_apertura
            + (float) $sales['ventas_total']
            + (float) $adjustments['neto_ajustes'];
    }
}
