<?php

namespace App\Http\Controllers;

use App\Http\Requests\CheckoutSaleRequest;
use App\Models\Client;
use App\Models\Product;
use App\Models\ProductReservation;
use App\Models\Sale;
use App\Models\SaleItem;
use App\Models\StockMovement;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\View\View;

class PosController extends Controller
{
    private const CART_KEY = 'pos_cart';

    private const RESERVATION_KEY = 'pos_reservation_id';

    private const CLIENT_KEY = 'pos_client_id';

    private const DISCOUNT_KEY = 'pos_discount';

    protected function currencySettings(): array
    {
        $primary = config('poskn.currency.primary', 'usd');
        $primary = in_array($primary, ['usd', 'ves'], true) ? $primary : 'usd';
        $rate = (float) config('poskn.currency.usd_to_ves_rate', 36.5);

        return [
            'primary' => $primary,
            'secondary' => $primary === 'usd' ? 'ves' : 'usd',
            'enable_secondary' => (bool) config('poskn.currency.enable_secondary', true),
            'usd_to_ves_rate' => $rate > 0 ? $rate : 1,
        ];
    }

    protected function hasColumn(string $table, string $column): bool
    {
        static $cache = [];
        $key = $table.'.'.$column;
        if (! array_key_exists($key, $cache)) {
            $cache[$key] = Schema::hasColumn($table, $column);
        }

        return $cache[$key];
    }

    protected function detachReservationFromCart(Request $request): void
    {
        $request->session()->forget(self::RESERVATION_KEY);
    }

    public function index(Request $request): View
    {
        $this->authorize('viewAny', Sale::class);

        $productQuery = Product::query()->where('is_active', true);
        $hasProductCategory = $this->hasColumn('products', 'category');
        if ($search = trim((string) $request->get('q'))) {
            $productQuery->where(function ($query) use ($search, $hasProductCategory): void {
                $query->where('name', 'like', "%{$search}%")
                    ->orWhere('sku', 'like', "%{$search}%")
                    ->orWhere('barcode', 'like', "%{$search}%");
                if ($hasProductCategory) {
                    $query->orWhere('category', 'like', "%{$search}%");
                }
            });
        }
        if ($hasProductCategory && ($category = trim((string) $request->get('category')))) {
            $productQuery->where('category', $category);
        }

        $products = $productQuery->orderBy('name')->paginate(10)->withQueryString();
        $categories = collect();
        if ($hasProductCategory) {
            $categories = Product::query()
                ->where('is_active', true)
                ->whereNotNull('category')
                ->where('category', '!=', '')
                ->orderBy('category')
                ->select('category')
                ->distinct()
                ->get()
                ->pluck('category')
                ->filter()
                ->values();
        }
        $cart = $request->session()->get(self::CART_KEY, []);
        $subtotal = round((float) collect($cart)->sum(fn ($l) => round((float) $l['unit_price'] * (int) $l['qty'], 2)), 2);
        $discountData = $request->session()->get(self::DISCOUNT_KEY, []);
        $discountAmount = min(round((float) ($discountData['amount'] ?? 0), 2), $subtotal);
        $discountCode = (string) ($discountData['approval_code'] ?? '');
        $total = round(max(0, $subtotal - $discountAmount), 2);
        $selectedClient = null;
        if ($request->session()->has(self::CLIENT_KEY)) {
            $selectedClient = Client::query()->find($request->session()->get(self::CLIENT_KEY));
        }
        $clients = Client::query()->orderBy('name')->limit(200)->get(['id', 'name', 'document_number', 'phone']);
        $recentSales = Sale::query()->with('cashier')->latest()->limit(8)->get();

        $pendingReservations = collect();
        if ($request->user()->can('reservations.view')) {
            $pendingReservations = ProductReservation::query()
                ->where('status', ProductReservation::STATUS_PENDING)
                ->with(['patient.client', 'author'])
                ->latest()
                ->limit(25)
                ->get();
        }

        $activeReservation = null;
        if ($request->session()->has(self::RESERVATION_KEY)) {
            $activeReservation = ProductReservation::query()
                ->with(['patient.client'])
                ->find($request->session()->get(self::RESERVATION_KEY));
        }
        $currencySettings = $this->currencySettings();

        return view('pos.index', compact(
            'products',
            'categories',
            'cart',
            'subtotal',
            'discountAmount',
            'discountCode',
            'total',
            'selectedClient',
            'clients',
            'recentSales',
            'pendingReservations',
            'activeReservation',
            'currencySettings'
        ));
    }

    public function loadFromReservation(Request $request, ProductReservation $reservation): RedirectResponse
    {
        $this->authorize('view', $reservation);
        $this->authorize('viewAny', Sale::class);

        if (! $reservation->isPending()) {
            return redirect()->route('pos.index')->with('error', __('La reserva ya no está pendiente.'));
        }

        $reservation->load('items.product');
        $cart = [];
        foreach ($reservation->items as $item) {
            $product = $item->product;
            if (! $product || ! $product->is_active) {
                return redirect()->route('pos.index')->with('error', __('Un producto de la reserva ya no está disponible.'));
            }
            $cart[] = [
                'product_id' => $product->id,
                'qty' => (int) $item->quantity,
                'name' => $product->name,
                'unit_price' => number_format((float) $item->unit_price, 2, '.', ''),
            ];
        }

        $request->session()->put(self::CART_KEY, $cart);
        $request->session()->put(self::RESERVATION_KEY, $reservation->id);
        $request->session()->put(self::CLIENT_KEY, $reservation->client_id);

        return redirect()->route('pos.index')->with('success', __('Carrito cargado desde reserva clínica.'));
    }

    public function addLine(Request $request): RedirectResponse
    {
        $this->authorize('viewAny', Sale::class);
        $this->detachReservationFromCart($request);

        $data = $request->validate([
            'product_id' => ['required', 'exists:products,id'],
            'quantity' => ['required', 'integer', 'min:1', 'max:9999'],
        ]);

        $product = Product::query()->whereKey($data['product_id'])->where('is_active', true)->firstOrFail();
        $qty = $data['quantity'];

        $cart = $request->session()->get(self::CART_KEY, []);
        $found = false;
        foreach ($cart as $i => $line) {
            if ((int) $line['product_id'] === (int) $product->id) {
                $cart[$i]['qty'] = (int) $cart[$i]['qty'] + $qty;
                $found = true;
                break;
            }
        }
        if (! $found) {
            $cart[] = [
                'product_id' => $product->id,
                'qty' => $qty,
                'name' => $product->name,
                'unit_price' => number_format((float) $product->sale_price, 2, '.', ''),
            ];
        }
        $request->session()->put(self::CART_KEY, $cart);

        return redirect()->route('pos.index', ['q' => $request->get('q')]);
    }

    public function updateLine(Request $request): RedirectResponse
    {
        $this->authorize('viewAny', Sale::class);
        $this->detachReservationFromCart($request);

        $data = $request->validate([
            'index' => ['required', 'integer', 'min:0'],
            'quantity' => ['required', 'integer', 'min:1', 'max:9999'],
        ]);

        $cart = $request->session()->get(self::CART_KEY, []);
        if (! isset($cart[$data['index']])) {
            return redirect()->route('pos.index')->with('error', __('Carrito no válido.'));
        }
        $cart[$data['index']]['qty'] = $data['quantity'];
        $request->session()->put(self::CART_KEY, $cart);

        return redirect()->route('pos.index');
    }

    public function removeLine(Request $request): RedirectResponse
    {
        $this->authorize('viewAny', Sale::class);
        $this->detachReservationFromCart($request);

        $data = $request->validate([
            'index' => ['required', 'integer', 'min:0'],
        ]);

        $cart = $request->session()->get(self::CART_KEY, []);
        if (isset($cart[$data['index']])) {
            array_splice($cart, $data['index'], 1);
            $request->session()->put(self::CART_KEY, array_values($cart));
        }

        return redirect()->route('pos.index');
    }

    public function clearCart(Request $request): RedirectResponse
    {
        $this->authorize('viewAny', Sale::class);
        $request->session()->forget(self::CART_KEY);
        $this->detachReservationFromCart($request);
        $request->session()->forget(self::CLIENT_KEY);
        $request->session()->forget(self::DISCOUNT_KEY);

        return redirect()->route('pos.index')->with('success', __('Carrito vaciado.'));
    }

    public function setClient(Request $request): RedirectResponse
    {
        $this->authorize('viewAny', Sale::class);

        $data = $request->validate([
            'client_id' => ['required', 'exists:clients,id'],
        ]);
        $request->session()->put(self::CLIENT_KEY, (int) $data['client_id']);

        return redirect()->route('pos.index')->with('success', __('Cliente asignado.'));
    }

    public function clearClient(Request $request): RedirectResponse
    {
        $this->authorize('viewAny', Sale::class);
        $request->session()->forget(self::CLIENT_KEY);

        return redirect()->route('pos.index')->with('success', __('Cliente removido del carrito.'));
    }

    public function setDiscount(Request $request): RedirectResponse
    {
        $this->authorize('viewAny', Sale::class);

        $data = $request->validate([
            'amount' => ['required', 'numeric', 'min:0'],
            'approval_code' => ['required', 'string', 'max:64'],
        ]);

        $expectedCode = trim((string) env('POS_DISCOUNT_APPROVAL_CODE', '1234'));
        if ($expectedCode !== '' && (string) $data['approval_code'] !== $expectedCode) {
            return redirect()->route('pos.index')->with('error', __('Código de aprobación inválido.'));
        }

        $request->session()->put(self::DISCOUNT_KEY, [
            'amount' => number_format((float) $data['amount'], 2, '.', ''),
            'approval_code' => (string) $data['approval_code'],
        ]);

        return redirect()->route('pos.index')->with('success', __('Descuento aplicado.'));
    }

    public function clearDiscount(Request $request): RedirectResponse
    {
        $this->authorize('viewAny', Sale::class);
        $request->session()->forget(self::DISCOUNT_KEY);

        return redirect()->route('pos.index')->with('success', __('Descuento removido.'));
    }

    public function checkout(CheckoutSaleRequest $request): RedirectResponse
    {
        $cart = $request->session()->get(self::CART_KEY, []);
        if ($cart === []) {
            return redirect()->route('pos.index')->with('error', __('El carrito está vacío.'));
        }

        $notes = $request->validated('notes');
        $paymentMethod = $request->validated('payment_method');
        $clientId = $request->session()->get(self::CLIENT_KEY);
        $discountData = $request->session()->get(self::DISCOUNT_KEY, []);
        $reservationId = $request->session()->get(self::RESERVATION_KEY);

        try {
            $sale = DB::transaction(function () use ($cart, $request, $notes, $paymentMethod, $clientId, $discountData, $reservationId) {
                $reservation = null;
                if ($reservationId) {
                    $reservation = ProductReservation::query()->lockForUpdate()->find($reservationId);
                    if (! $reservation || ! $reservation->isPending()) {
                        throw new \RuntimeException(__('La reserva ya no es válida. Vacíe el carrito o cargue la reserva de nuevo.'));
                    }
                    if (! $this->cartMatchesReservation($reservation, $cart)) {
                        throw new \RuntimeException(__('El carrito no coincide con la reserva. Vacíe y vuelva a cargar desde la reserva.'));
                    }
                }

                $lines = [];
                $subtotal = 0.0;

                foreach ($cart as $line) {
                    $product = Product::query()->lockForUpdate()->findOrFail($line['product_id']);
                    if (! $product->is_active) {
                        throw new \RuntimeException(__('El producto «:name» no está activo.', ['name' => $product->name]));
                    }
                    $qty = (int) $line['qty'];
                    if ($product->track_inventory && $product->stock_quantity < $qty) {
                        throw new \RuntimeException(__('Stock insuficiente para «:name».', ['name' => $product->name]));
                    }
                    $unit = (string) $line['unit_price'];
                    $lineTotal = round((float) $unit * $qty, 2);
                    $subtotal += $lineTotal;

                    $lines[] = compact('product', 'qty', 'unit', 'lineTotal');
                }

                $subtotal = round($subtotal, 2);
                $discountAmount = min(round((float) ($discountData['amount'] ?? 0), 2), $subtotal);
                $total = round(max(0, $subtotal - $discountAmount), 2);
                $discountCode = $discountAmount > 0 ? (string) ($discountData['approval_code'] ?? '') : null;
                $saleClientId = $reservation?->client_id ?? $clientId;

                $saleData = [
                    'user_id' => $request->user()->id,
                    'product_reservation_id' => $reservation?->id,
                    'total' => $total,
                    'status' => 'completed',
                    'notes' => $notes,
                ];
                if ($this->hasColumn('sales', 'client_id')) {
                    $saleData['client_id'] = $saleClientId;
                }
                if ($this->hasColumn('sales', 'subtotal')) {
                    $saleData['subtotal'] = $subtotal;
                }
                if ($this->hasColumn('sales', 'discount_amount')) {
                    $saleData['discount_amount'] = $discountAmount;
                }
                if ($this->hasColumn('sales', 'discount_approval_code')) {
                    $saleData['discount_approval_code'] = $discountCode;
                }
                if ($this->hasColumn('sales', 'payment_method')) {
                    $saleData['payment_method'] = $paymentMethod;
                }
                $sale = Sale::query()->create($saleData);

                foreach ($lines as $row) {
                    /** @var Product $product */
                    $product = $row['product'];
                    $qty = $row['qty'];
                    $unit = $row['unit'];
                    $lineTotal = $row['lineTotal'];

                    SaleItem::query()->create([
                        'sale_id' => $sale->id,
                        'product_id' => $product->id,
                        'product_name' => $product->name,
                        'quantity' => $qty,
                        'unit_price' => $unit,
                        'line_total' => $lineTotal,
                    ]);

                    if ($product->track_inventory) {
                        $product->stock_quantity -= $qty;
                        $product->save();

                        StockMovement::query()->create([
                            'product_id' => $product->id,
                            'user_id' => $request->user()->id,
                            'quantity_change' => -$qty,
                            'balance_after' => $product->stock_quantity,
                            'reason' => 'sale',
                            'reference' => 'sale:'.$sale->id,
                            'notes' => null,
                        ]);
                    }
                }

                if ($reservation) {
                    $reservation->update([
                        'status' => ProductReservation::STATUS_BILLED,
                        'sale_id' => $sale->id,
                    ]);
                }

                return $sale;
            });
        } catch (\RuntimeException $e) {
            return redirect()->route('pos.index')->with('error', $e->getMessage());
        }

        $request->session()->forget(self::CART_KEY);
        $this->detachReservationFromCart($request);
        $request->session()->forget(self::CLIENT_KEY);
        $request->session()->forget(self::DISCOUNT_KEY);

        return redirect()->route('pos.ticket', $sale)
            ->with('success', __('Venta registrada.'));
    }

    public function show(Sale $sale): View
    {
        $this->authorize('view', $sale);
        $sale->load(['items', 'cashier', 'client', 'productReservation.patient.client']);
        $currencySettings = $this->currencySettings();

        return view('pos.show', compact('sale', 'currencySettings'));
    }

    /**
     * @param  array<int, array{product_id: int, qty: int, unit_price: string, name?: string}>  $cart
     */
    protected function cartMatchesReservation(ProductReservation $reservation, array $cart): bool
    {
        $reservation->loadMissing('items');

        $a = $reservation->items->map(fn ($i) => [
            'product_id' => (int) $i->product_id,
            'qty' => (int) $i->quantity,
            'unit_price' => number_format((float) $i->unit_price, 2, '.', ''),
        ])->sortBy('product_id')->values()->all();

        $b = collect($cart)->map(fn ($line) => [
            'product_id' => (int) $line['product_id'],
            'qty' => (int) $line['qty'],
            'unit_price' => number_format((float) $line['unit_price'], 2, '.', ''),
        ])->sortBy('product_id')->values()->all();

        return $a === $b;
    }
}
