<?php

declare(strict_types=1);

namespace App\Services;

use App\Core\Auth;
use App\Core\Database;
use App\Core\Tenant;
use App\Models\CompanySetting;
use PDO;
use RuntimeException;
use App\Models\PaymentMethod;
use App\Services\FactusService;
use App\Services\SecretCipherService;

class PosService
{
    private ?bool $variantQuantityColumnAvailable = null;

    public function registerSale(array $payload): int
    {
        $companyId = Tenant::companyId();
        $userId = Auth::id();

        if ($companyId === null || $userId === null) {
            throw new RuntimeException('Sesión inválida para registrar la venta.');
        }

        $branchId = (int) ($payload['sucursal_id'] ?? 0);
        $cashBoxId = (int) ($payload['caja_id'] ?? 0);
        $openingId = (int) ($payload['apertura_caja_id'] ?? 0);

        if ($branchId <= 0) {
            throw new RuntimeException('Debes tener una sucursal asignada para vender en POS.');
        }

        if ($cashBoxId <= 0 || $openingId <= 0) {
            throw new RuntimeException('Debes abrir una caja válida para registrar la venta.');
        }

        $items = $payload['items'] ?? [];
        if (!is_array($items) || $items === []) {
            throw new RuntimeException('Debes agregar al menos un producto al carrito.');
        }

        $globalDiscountMode = strtolower(trim((string) ($payload['descuento_modo'] ?? 'valor')));
        if (!in_array($globalDiscountMode, ['valor', 'porcentaje'], true)) {
            $globalDiscountMode = 'valor';
        }
        $globalDiscountRaw = max(0, (float) ($payload['descuento'] ?? 0));

        $saleType = strtolower(trim((string) ($payload['tipo_venta'] ?? 'contado')));
        if (!in_array($saleType, ['contado', 'credito'], true)) {
            $saleType = 'contado';
        }

        $customerIdRaw = $payload['cliente_id'] ?? '';
        $customerId = $customerIdRaw !== '' ? (int) $customerIdRaw : null;
        if ($saleType === 'credito' && ($customerId === null || $customerId <= 0)) {
            throw new RuntimeException('Para venta a credito debes seleccionar un cliente.');
        }

        $paymentMethodCode = strtolower(trim((string) ($payload['metodo_pago'] ?? 'efectivo')));
        if ($paymentMethodCode === '') {
            $paymentMethodCode = 'efectivo';
        }
        $paymentReference = trim((string) ($payload['referencia_pago'] ?? ''));

        $allowedMethods = [];
        foreach ((new PaymentMethod())->effectiveForCompany((int) $companyId) as $method) {
            if ((int) ($method['activo'] ?? 0) !== 1) {
                continue;
            }

            $code = strtolower(trim((string) ($method['codigo'] ?? '')));
            if ($code === '') {
                continue;
            }
            $allowedMethods[$code] = true;
        }

        if ($allowedMethods === []) {
            $allowedMethods = [
                'efectivo' => true,
                'tarjeta' => true,
                'transferencia' => true,
            ];
        }

        if (!isset($allowedMethods[$paymentMethodCode])) {
            throw new RuntimeException('El método de pago seleccionado no está activo para esta empresa.');
        }

        $factusRuntimeSettings = $this->resolveFactusRuntimeSettings((int) $companyId);

        return Database::transaction(function () use (
            $payload,
            $companyId,
            $userId,
            $branchId,
            $cashBoxId,
            $openingId,
            $items,
            $saleType,
            $customerId,
            $globalDiscountMode,
            $globalDiscountRaw,
            $paymentMethodCode,
            $paymentReference,
            $factusRuntimeSettings
        ): int {
            $pdo = Database::connection();

            $userStmt = $pdo->prepare(
                'SELECT sucursal_id
                 FROM usuarios
                 WHERE id = :usuario_id
                   AND empresa_id = :empresa_id
                 LIMIT 1
                 FOR UPDATE'
            );
            $userStmt->execute([
                ':usuario_id' => $userId,
                ':empresa_id' => $companyId,
            ]);
            $user = $userStmt->fetch();

            if (!$user) {
                throw new RuntimeException('No se encontró el usuario para registrar la venta.');
            }

            $assignedBranchId = (int) ($user['sucursal_id'] ?? 0);
            if ($assignedBranchId <= 0 || $assignedBranchId !== $branchId) {
                throw new RuntimeException('Tu usuario no tiene acceso a la sucursal seleccionada para POS.');
            }

            $openingStmt = $pdo->prepare(
                'SELECT
                    ac.id,
                    ac.usuario_id,
                    ac.estado,
                    ac.caja_id,
                    c.sucursal_id AS caja_sucursal_id
                 FROM aperturas_caja ac
                 LEFT JOIN cajas c ON c.id = ac.caja_id
                 WHERE ac.id = :apertura_id
                   AND ac.empresa_id = :empresa_id
                 LIMIT 1
                 FOR UPDATE'
            );
            $openingStmt->execute([
                ':apertura_id' => $openingId,
                ':empresa_id' => $companyId,
            ]);
            $opening = $openingStmt->fetch();

            if (!$opening) {
                throw new RuntimeException('La apertura de caja no existe para la empresa activa.');
            }

            if ((int) ($opening['usuario_id'] ?? 0) !== $userId) {
                throw new RuntimeException('La apertura de caja no pertenece al usuario autenticado.');
            }

            if (($opening['estado'] ?? '') !== 'abierta') {
                throw new RuntimeException('La caja ya está cerrada.');
            }

            if ((int) ($opening['caja_id'] ?? 0) !== $cashBoxId) {
                throw new RuntimeException('La caja activa no coincide con la apertura enviada.');
            }

            if ((int) ($opening['caja_sucursal_id'] ?? 0) !== $branchId) {
                throw new RuntimeException('La caja abierta no pertenece a la sucursal asignada.');
            }

            $saleInsertStmt = $pdo->prepare(
                'INSERT INTO ventas (
                    empresa_id, sucursal_id, usuario_id, cliente_id, apertura_caja_id,
                    subtotal, descuento, impuestos, total, estado, fecha, created_at
                ) VALUES (
                    :empresa_id, :sucursal_id, :usuario_id, :cliente_id, :apertura_caja_id,
                    0, 0, 0, 0, "pendiente", NOW(), NOW()
                )'
            );
            $saleInsertStmt->execute([
                ':empresa_id' => $companyId,
                ':sucursal_id' => $branchId,
                ':usuario_id' => $userId,
                ':cliente_id' => $customerId,
                ':apertura_caja_id' => $openingId,
            ]);
            $saleId = (int) $pdo->lastInsertId();

            $detailStmt = $pdo->prepare(
                'INSERT INTO venta_detalles (
                    venta_id, empresa_id, producto_id, cantidad, precio_unitario, impuesto, descuento, total_linea
                ) VALUES (
                    :venta_id, :empresa_id, :producto_id, :cantidad, :precio_unitario, :impuesto, :descuento, :total_linea
                )'
            );

            $productStmt = $pdo->prepare(
                'SELECT id, nombre, sku, activo, sucursal_id, precio_compra, stock_actual, impuesto_porcentaje
                 FROM productos
                 WHERE id = :producto_id
                   AND empresa_id = :empresa_id
                 LIMIT 1
                 FOR UPDATE'
            );

            $hasVariantTable = $this->hasVariantTable($pdo);
            $hasVariantQty = $hasVariantTable ? $this->hasVariantQuantityColumn($pdo) : false;
            $variantSelectStmt = null;
            $variantStockStmt = null;
            $syncProductStockFromVariantsStmt = null;
            if ($hasVariantTable) {
                $variantSelectStmt = $pdo->prepare(
                    $hasVariantQty
                        ? 'SELECT id, producto_id, nombre, activo, precio_venta, cantidad
                           FROM producto_variantes
                           WHERE id = :variante_id
                             AND empresa_id = :empresa_id
                             AND producto_id = :producto_id
                           LIMIT 1
                           FOR UPDATE'
                        : 'SELECT id, producto_id, nombre, activo, precio_venta, 0 AS cantidad
                           FROM producto_variantes
                           WHERE id = :variante_id
                             AND empresa_id = :empresa_id
                             AND producto_id = :producto_id
                           LIMIT 1
                           FOR UPDATE'
                );
                $variantStockStmt = $hasVariantQty
                    ? $pdo->prepare(
                        'UPDATE producto_variantes
                         SET cantidad = cantidad - :cantidad_descuento,
                             updated_at = NOW()
                         WHERE id = :variante_id
                           AND empresa_id = :empresa_id
                           AND producto_id = :producto_id
                           AND cantidad >= :cantidad_validacion'
                    )
                    : null;

                if ($hasVariantQty) {
                    $syncProductStockFromVariantsStmt = $pdo->prepare(
                        'UPDATE productos p
                         SET p.stock_actual = (
                             SELECT COALESCE(SUM(v.cantidad), 0)
                             FROM producto_variantes v
                             WHERE v.empresa_id = p.empresa_id
                               AND v.producto_id = p.id
                               AND v.activo = 1
                         ),
                         p.updated_at = NOW()
                         WHERE p.id = :producto_id
                           AND p.empresa_id = :empresa_id'
                    );
                }
            }

            $stockStmt = $pdo->prepare(
                'UPDATE productos
                 SET stock_actual = stock_actual - :cantidad
                 WHERE id = :producto_id AND empresa_id = :empresa_id'
            );

            $branchStockStmt = $pdo->prepare(
                'SELECT id, stock
                 FROM inventario
                 WHERE empresa_id = :empresa_id
                   AND sucursal_id = :sucursal_id
                   AND producto_id = :producto_id
                 LIMIT 1
                 FOR UPDATE'
            );

            $branchRowsCountStmt = $pdo->prepare(
                'SELECT COUNT(*)
                 FROM inventario
                 WHERE empresa_id = :empresa_id
                   AND producto_id = :producto_id
                   AND sucursal_id IS NOT NULL'
            );

            $seedBranchStockStmt = $pdo->prepare(
                'INSERT INTO inventario (
                    empresa_id, sucursal_id, producto_id, stock, costo_promedio, updated_at
                ) VALUES (
                    :empresa_id, :sucursal_id, :producto_id, :stock, :costo_promedio, NOW()
                )
                ON DUPLICATE KEY UPDATE
                    updated_at = VALUES(updated_at)'
            );

            $branchDiscountStmt = $pdo->prepare(
                'UPDATE inventario
                 SET stock = stock - :cantidad,
                     updated_at = NOW()
                 WHERE empresa_id = :empresa_id
                   AND sucursal_id = :sucursal_id
                   AND producto_id = :producto_id'
            );

            $kardexStmt = $pdo->prepare(
                'INSERT INTO movimientos_inventario (
                    empresa_id, sucursal_id, producto_id, tipo, referencia, cantidad, costo_unitario, created_at
                ) VALUES (
                    :empresa_id, :sucursal_id, :producto_id, :tipo, :referencia, :cantidad, :costo_unitario, NOW()
                )'
            );

            $invoiceLines = [];
            $validItems = 0;
            $subtotal = 0.0;
            $lineDiscountTotal = 0.0;
            $tax = 0.0;
            $variantProductsToSync = [];

            foreach ($items as $item) {
                $productId = (int) ($item['producto_id'] ?? 0);
                $variantId = (int) ($item['variante_id'] ?? 0);
                $variantNameInput = trim((string) ($item['variante_nombre'] ?? ''));
                $cantidad = (float) ($item['cantidad'] ?? 0);
                $precio = (float) ($item['precio_unitario'] ?? 0);
                $lineDiscountInput = max(0, (float) ($item['descuento'] ?? 0));
                $lineDiscountType = strtolower(trim((string) ($item['descuento_tipo'] ?? 'valor')));
                if (!in_array($lineDiscountType, ['valor', 'porcentaje'], true)) {
                    $lineDiscountType = 'valor';
                }

                if ($productId <= 0 || $cantidad <= 0 || $precio <= 0) {
                    continue;
                }

                if (!$this->isWholeQuantity($cantidad)) {
                    throw new RuntimeException('La cantidad de cada producto debe ser un número entero.');
                }
                $cantidad = (float) ((int) round($cantidad));

                $productStmt->execute([
                    ':producto_id' => $productId,
                    ':empresa_id' => $companyId,
                ]);
                $product = $productStmt->fetch();

                if (!$product) {
                    throw new RuntimeException('Producto no válido para registrar venta.');
                }

                if ((int) ($product['activo'] ?? 0) !== 1) {
                    throw new RuntimeException('El producto "' . (string) ($product['nombre'] ?? 'N/A') . '" está inactivo.');
                }

                $resolvedVariantName = '';
                $isVariantSale = $variantId > 0 || $variantNameInput !== '';
                if ($isVariantSale) {
                    if (!$hasVariantTable || !$variantSelectStmt) {
                        throw new RuntimeException('El módulo de variaciones no está disponible en la base de datos.');
                    }

                    if ($variantId <= 0) {
                        throw new RuntimeException('La variación enviada no es válida para el producto.');
                    }

                    $variantSelectStmt->execute([
                        ':variante_id' => $variantId,
                        ':empresa_id' => $companyId,
                        ':producto_id' => $productId,
                    ]);
                    $variant = $variantSelectStmt->fetch();

                    if (!$variant) {
                        throw new RuntimeException('La variación seleccionada no existe para el producto.');
                    }

                    if ((int) ($variant['activo'] ?? 0) !== 1) {
                        throw new RuntimeException('La variación seleccionada está inactiva.');
                    }

                    $resolvedVariantName = trim((string) ($variant['nombre'] ?? ''));
                    if ($resolvedVariantName === '') {
                        throw new RuntimeException('La variación seleccionada no tiene nombre válido.');
                    }

                    $variantPrice = (float) ($variant['precio_venta'] ?? 0);
                    if ($variantPrice > 0) {
                        $precio = $variantPrice;
                    }

                    if (!$hasVariantQty) {
                        throw new RuntimeException(
                            'No puedes vender variaciones sin columna de cantidad en la base de datos. Ejecuta la migración de producto_variantes.'
                        );
                    }

                    $availableVariantQty = (float) ($variant['cantidad'] ?? 0);
                    if ($availableVariantQty + 0.0001 < $cantidad) {
                        throw new RuntimeException(
                            'Stock insuficiente en la variación "' . $resolvedVariantName . '".'
                        );
                    }
                }

                if (!$isVariantSale && (float) ($product['stock_actual'] ?? 0) + 0.0001 < $cantidad) {
                    throw new RuntimeException(
                        'Stock global insuficiente para "' . (string) ($product['nombre'] ?? 'N/A') . '".'
                    );
                }

                $productBranchId = $product['sucursal_id'] !== null ? (int) $product['sucursal_id'] : null;

                $branchStockStmt->execute([
                    ':empresa_id' => $companyId,
                    ':sucursal_id' => $branchId,
                    ':producto_id' => $productId,
                ]);
                $branchStock = $branchStockStmt->fetch();

                if (!$branchStock) {
                    $branchRowsCountStmt->execute([
                        ':empresa_id' => $companyId,
                        ':producto_id' => $productId,
                    ]);
                    $hasBranchRows = (int) $branchRowsCountStmt->fetchColumn() > 0;

                    if (!$hasBranchRows && ($productBranchId === null || $productBranchId === $branchId)) {
                        $seedStock = (float) ($product['stock_actual'] ?? 0);
                        $seedBranchStockStmt->execute([
                            ':empresa_id' => $companyId,
                            ':sucursal_id' => $branchId,
                            ':producto_id' => $productId,
                            ':stock' => $seedStock,
                            ':costo_promedio' => (float) ($product['precio_compra'] ?? 0),
                        ]);

                        $branchStockStmt->execute([
                            ':empresa_id' => $companyId,
                            ':sucursal_id' => $branchId,
                            ':producto_id' => $productId,
                        ]);
                        $branchStock = $branchStockStmt->fetch();
                    }
                }

                $availableByBranch = $branchStock ? (float) ($branchStock['stock'] ?? 0) : 0.0;
                if ($availableByBranch + 0.0001 < $cantidad) {
                    throw new RuntimeException(
                        'Stock insuficiente en la sucursal para "' . (string) ($product['nombre'] ?? 'N/A') . '".'
                    );
                }

                $lineSubtotal = $cantidad * $precio;
                $lineDiscount = $this->discountAmount($lineSubtotal, $lineDiscountInput, $lineDiscountType);
                $lineBase = max(0.0, $lineSubtotal - $lineDiscount);
                $taxRate = max(0.0, min(100.0, (float) ($product['impuesto_porcentaje'] ?? 0)));
                $lineTax = round($lineBase * ($taxRate / 100), 2);
                $lineTotal = max(0.0, $lineBase + $lineTax);

                $detailStmt->execute([
                    ':venta_id' => $saleId,
                    ':empresa_id' => $companyId,
                    ':producto_id' => $productId,
                    ':cantidad' => $cantidad,
                    ':precio_unitario' => $precio,
                    ':impuesto' => $lineTax,
                    ':descuento' => $lineDiscount,
                    ':total_linea' => $lineTotal,
                ]);

                $stockStmt->execute([
                    ':cantidad' => $cantidad,
                    ':producto_id' => $productId,
                    ':empresa_id' => $companyId,
                ]);

                $branchDiscountStmt->execute([
                    ':cantidad' => $cantidad,
                    ':empresa_id' => $companyId,
                    ':sucursal_id' => $branchId,
                    ':producto_id' => $productId,
                ]);

                $kardexStmt->execute([
                    ':empresa_id' => $companyId,
                    ':sucursal_id' => $branchId,
                    ':producto_id' => $productId,
                    ':tipo' => 'salida',
                    ':referencia' => 'VENTA#' . $saleId,
                    ':cantidad' => $cantidad,
                    ':costo_unitario' => (float) ($product['precio_compra'] ?? 0),
                ]);

                if ($variantStockStmt !== null && $variantId > 0) {
                    $variantStockStmt->execute([
                        ':cantidad_descuento' => $cantidad,
                        ':cantidad_validacion' => $cantidad,
                        ':variante_id' => $variantId,
                        ':empresa_id' => $companyId,
                        ':producto_id' => $productId,
                    ]);

                    if ($variantStockStmt->rowCount() <= 0) {
                        throw new RuntimeException('No fue posible descontar stock de la variación seleccionada.');
                    }

                    $variantProductsToSync[$productId] = true;
                }

                $validItems++;
                $subtotal += $lineSubtotal;
                $lineDiscountTotal += $lineDiscount;
                $tax += $lineTax;

                $invoiceLines[] = [
                    'producto_id' => $productId,
                    'sku' => trim((string) ($product['sku'] ?? '')),
                    'descripcion' => trim((string) ($product['nombre'] ?? ('Producto #' . $productId)))
                        . ($resolvedVariantName !== '' ? (' - ' . $resolvedVariantName) : ''),
                    'cantidad' => $cantidad,
                    'precio_unitario' => $precio,
                    'descuento' => $lineDiscount,
                    'tasa_impuesto' => $taxRate,
                    'impuesto' => $lineTax,
                    'total_linea' => $lineTotal,
                ];
            }

            if ($validItems <= 0) {
                throw new RuntimeException('Debes registrar al menos un producto válido en el carrito.');
            }

            if ($syncProductStockFromVariantsStmt !== null && $variantProductsToSync !== []) {
                foreach (array_keys($variantProductsToSync) as $variantProductId) {
                    $syncProductStockFromVariantsStmt->execute([
                        ':producto_id' => (int) $variantProductId,
                        ':empresa_id' => $companyId,
                    ]);
                }
            }

            $subtotalAfterLineDiscount = max(0.0, $subtotal - $lineDiscountTotal);
            $globalDiscount = $this->discountAmount($subtotalAfterLineDiscount, $globalDiscountRaw, $globalDiscountMode);
            $discount = round($lineDiscountTotal + $globalDiscount, 2);
            $tax = round($tax, 2);
            $total = max(0.0, round($subtotalAfterLineDiscount - $globalDiscount + $tax, 2));

            $paidAmount = $total;
            if ($saleType === 'credito') {
                $paidAmount = max(0.0, (float) ($payload['abono_inicial'] ?? 0));
                if ($paidAmount > $total) {
                    throw new RuntimeException('El abono inicial no puede superar el total de la venta.');
                }
            }
            $saleStatus = $paidAmount >= ($total - 0.0001) ? 'pagada' : 'pendiente';

            if ($this->isCredibancoDataphonePaymentMethod($paymentMethodCode) && $paidAmount > 0.0001) {
                // Credibanco confirma pago por webhook; la venta se mantiene pendiente hasta aprobación remota.
                $saleStatus = 'pendiente';
            }

            if (
                ($this->isBoldPaymentMethod($paymentMethodCode) || $this->isCredibancoDataphonePaymentMethod($paymentMethodCode))
                && $paidAmount > 0.0001
                && $paymentReference === ''
            ) {
                throw new RuntimeException('La referencia de transacción del datáfono es obligatoria para registrar la venta.');
            }

            if ($this->isBoldPaymentMethod($paymentMethodCode) && $paidAmount > 0.0001) {
                $boldSettings = $this->resolveBoldRuntimeSettings((int) $companyId, (string) $paymentMethodCode);
                if ($this->isBoldDataphonePaymentMethod((string) $paymentMethodCode)) {
                    $boldValidation = (new BoldService())->validatePaymentTransaction([
                        'api_url' => (string) ($boldSettings['api_url'] ?? ''),
                        'api_key' => (string) ($boldSettings['api_key'] ?? ''),
                    ], $paymentReference, $paidAmount);

                    if (!(bool) ($boldValidation['ok'] ?? false)) {
                        throw new RuntimeException(
                            'No se pudo validar la transacción Bold: '
                            . (string) ($boldValidation['message'] ?? 'Validación rechazada por Bold.')
                        );
                    }
                }
            }

            $saleUpdateStmt = $pdo->prepare(
                'UPDATE ventas
                 SET subtotal = :subtotal,
                     descuento = :descuento,
                     impuestos = :impuestos,
                     total = :total,
                     estado = :estado,
                     updated_at = NOW()
                 WHERE id = :id
                   AND empresa_id = :empresa_id'
            );
            $saleUpdateStmt->execute([
                ':subtotal' => $subtotal,
                ':descuento' => $discount,
                ':impuestos' => $tax,
                ':total' => $total,
                ':estado' => $saleStatus,
                ':id' => $saleId,
                ':empresa_id' => $companyId,
            ]);

            $invoiceNumber = $this->reserveInvoiceNumber($pdo, $companyId, $branchId, $saleId);
            $invoiceStmt = $pdo->prepare(
                'INSERT INTO facturas (
                    empresa_id, sucursal_id, venta_id, cliente_id, tipo_documento, numero,
                    subtotal, impuestos, total, estado, fecha_emision, created_at, updated_at
                ) VALUES (
                    :empresa_id, :sucursal_id, :venta_id, :cliente_id, :tipo_documento, :numero,
                    :subtotal, :impuestos, :total, :estado, NOW(), NOW(), NOW()
                )'
            );
            $invoiceStmt->execute([
                ':empresa_id' => $companyId,
                ':sucursal_id' => $branchId,
                ':venta_id' => $saleId,
                ':cliente_id' => $customerId,
                ':tipo_documento' => 'factura',
                ':numero' => $invoiceNumber,
                ':subtotal' => $subtotal,
                ':impuestos' => $tax,
                ':total' => $total,
                ':estado' => 'emitida',
            ]);

            $invoiceId = (int) $pdo->lastInsertId();
            $invoiceDetailStmt = $pdo->prepare(
                'INSERT INTO factura_detalles (
                    factura_id, producto_id, descripcion, cantidad, precio_unitario, impuesto, total_linea
                ) VALUES (
                    :factura_id, :producto_id, :descripcion, :cantidad, :precio_unitario, :impuesto, :total_linea
                )'
            );

            foreach ($invoiceLines as $line) {
                $invoiceDetailStmt->execute([
                    ':factura_id' => $invoiceId,
                    ':producto_id' => $line['producto_id'],
                    ':descripcion' => $line['descripcion'],
                    ':cantidad' => $line['cantidad'],
                    ':precio_unitario' => $line['precio_unitario'],
                    ':impuesto' => $line['impuesto'],
                    ':total_linea' => $line['total_linea'],
                ]);
            }

            if ($paidAmount > 0.0001) {
                $paymentStmt = $pdo->prepare(
                    'INSERT INTO venta_pagos (
                        venta_id, empresa_id, metodo_pago, monto, referencia, created_at
                    ) VALUES (
                        :venta_id, :empresa_id, :metodo_pago, :monto, :referencia, NOW()
                    )'
                );

                $paymentStmt->execute([
                    ':venta_id' => $saleId,
                    ':empresa_id' => $companyId,
                    ':metodo_pago' => $paymentMethodCode,
                    ':monto' => $paidAmount,
                    ':referencia' => $paymentReference !== '' ? $paymentReference : null,
                ]);
            }

            if (is_array($factusRuntimeSettings)) {
                $this->issueFactusInvoiceForSale(
                    $pdo,
                    (int) $companyId,
                    (int) $saleId,
                    (int) $invoiceId,
                    $invoiceNumber,
                    $customerId,
                    (string) $saleType,
                    (string) $paymentMethodCode,
                    $invoiceLines,
                    $factusRuntimeSettings
                );
            }

            return $saleId;
        });
    }

    private function isWholeQuantity(float $quantity): bool
    {
        return abs($quantity - round($quantity)) < 0.0001;
    }

    private function discountAmount(float $baseAmount, float $discountValue, string $discountType): float
    {
        $baseAmount = max(0.0, $baseAmount);
        $discountValue = max(0.0, $discountValue);
        $discountType = strtolower(trim($discountType));

        if ($baseAmount <= 0.0001 || $discountValue <= 0.0001) {
            return 0.0;
        }

        if ($discountType === 'porcentaje') {
            $discountValue = min(100.0, $discountValue);
            return round($baseAmount * ($discountValue / 100), 2);
        }

        return round(min($baseAmount, $discountValue), 2);
    }

    private function reserveInvoiceNumber(PDO $pdo, int $companyId, int $branchId, int $saleId): string
    {
        $numberingStmt = $pdo->prepare(
            'SELECT *
             FROM numeraciones_documentos
             WHERE empresa_id = :empresa_id
               AND tipo_documento = :tipo_documento
               AND activo = 1
               AND (sucursal_id = :sucursal_id OR sucursal_id IS NULL)
             ORDER BY
                CASE WHEN sucursal_id = :sucursal_id_priority THEN 0 ELSE 1 END,
                id ASC
             LIMIT 1
             FOR UPDATE'
        );
        $numberingStmt->execute([
            ':empresa_id' => $companyId,
            ':tipo_documento' => 'factura',
            ':sucursal_id' => $branchId,
            ':sucursal_id_priority' => $branchId,
        ]);
        $numbering = $numberingStmt->fetch();

        if (!$numbering) {
            return 'POS-' . $branchId . '-' . $saleId;
        }

        $current = (int) ($numbering['consecutivo_actual'] ?? 0);
        $final = $numbering['consecutivo_final'] !== null ? (int) $numbering['consecutivo_final'] : null;
        $prefix = trim((string) ($numbering['prefijo'] ?? ''));

        if ($current <= 0) {
            throw new RuntimeException('La numeración activa tiene consecutivo inválido.');
        }

        if ($final !== null && $current > $final) {
            throw new RuntimeException('La numeración activa ya alcanzó su consecutivo final.');
        }

        $this->assertResolutionCompatibility($pdo, $companyId, $branchId, $prefix, $current);

        $updateStmt = $pdo->prepare(
            'UPDATE numeraciones_documentos
             SET consecutivo_actual = :siguiente,
                 updated_at = NOW()
             WHERE id = :id
               AND empresa_id = :empresa_id'
        );
        $updateStmt->execute([
            ':siguiente' => $current + 1,
            ':id' => (int) $numbering['id'],
            ':empresa_id' => $companyId,
        ]);

        return substr(($prefix !== '' ? $prefix : '') . (string) $current, 0, 80);
    }

    private function assertResolutionCompatibility(
        PDO $pdo,
        int $companyId,
        int $branchId,
        string $prefix,
        int $currentNumber
    ): void {
        $resolutionStmt = $pdo->prepare(
            'SELECT *
             FROM resoluciones_dian
             WHERE empresa_id = :empresa_id
               AND activo = 1
               AND (sucursal_id = :sucursal_id OR sucursal_id IS NULL)
               AND COALESCE(prefijo, "") = :prefijo
               AND CURDATE() BETWEEN fecha_desde AND fecha_hasta
             ORDER BY
                CASE WHEN sucursal_id = :sucursal_id_priority THEN 0 ELSE 1 END,
                id DESC
             LIMIT 1'
        );
        $resolutionStmt->execute([
            ':empresa_id' => $companyId,
            ':sucursal_id' => $branchId,
            ':sucursal_id_priority' => $branchId,
            ':prefijo' => $prefix,
        ]);
        $resolution = $resolutionStmt->fetch();

        if (!$resolution) {
            return;
        }

        $rangeFrom = (int) ($resolution['rango_desde'] ?? 0);
        $rangeTo = (int) ($resolution['rango_hasta'] ?? 0);

        if ($rangeFrom <= 0 || $rangeTo <= 0 || $rangeFrom > $rangeTo) {
            throw new RuntimeException('La resolución activa tiene rango inválido.');
        }

        if ($currentNumber < $rangeFrom || $currentNumber > $rangeTo) {
            throw new RuntimeException(
                'El consecutivo de numeración no está dentro del rango autorizado por la resolución activa.'
            );
        }
    }

    private function resolveFactusRuntimeSettings(int $companyId): ?array
    {
        $raw = (new CompanySetting())->getMany($companyId, [
            'facturacion_electronica',
            'factus_api_url',
            'factus_client_id',
            'factus_client_secret',
            'factus_username',
            'factus_password',
            'factus_software_id',
            'factus_software_pin',
            'factus_numbering_range_id',
            'factus_municipality_id',
        ]);

        $feEnabled = trim((string) ($raw['facturacion_electronica'] ?? '0')) === '1';
        if (!$feEnabled) {
            return null;
        }

        $resolved = [
            'api_url' => trim((string) ($raw['factus_api_url'] ?? '')),
            'client_id' => SecretCipherService::decrypt((string) ($raw['factus_client_id'] ?? '')),
            'client_secret' => SecretCipherService::decrypt((string) ($raw['factus_client_secret'] ?? '')),
            'username' => SecretCipherService::decrypt((string) ($raw['factus_username'] ?? '')),
            'password' => SecretCipherService::decrypt((string) ($raw['factus_password'] ?? '')),
            'software_id' => SecretCipherService::decrypt((string) ($raw['factus_software_id'] ?? '')),
            'software_pin' => SecretCipherService::decrypt((string) ($raw['factus_software_pin'] ?? '')),
            'numbering_range_id' => trim((string) ($raw['factus_numbering_range_id'] ?? '')),
            'municipality_id' => trim((string) ($raw['factus_municipality_id'] ?? '')),
        ];

        $required = [
            'api_url' => 'URL API Factus',
            'client_id' => 'Client ID',
            'client_secret' => 'Client Secret',
            'username' => 'Usuario Factus',
            'password' => 'Contraseña Factus',
            'software_id' => 'Software ID',
            'software_pin' => 'Software PIN',
            'numbering_range_id' => 'Numbering Range ID',
            'municipality_id' => 'Municipality ID',
        ];

        foreach ($required as $key => $label) {
            if (trim((string) ($resolved[$key] ?? '')) === '') {
                throw new RuntimeException('Facturación electrónica activa, pero falta configurar: ' . $label . '.');
            }
        }

        if (preg_match('/^\d+$/', $resolved['numbering_range_id']) !== 1) {
            throw new RuntimeException('Numbering Range ID de Factus debe ser numérico.');
        }

        if (preg_match('/^\d+$/', $resolved['municipality_id']) !== 1) {
            throw new RuntimeException('Municipality ID de Factus debe ser numérico.');
        }

        return $resolved;
    }

    private function issueFactusInvoiceForSale(
        PDO $pdo,
        int $companyId,
        int $saleId,
        int $invoiceId,
        string $invoiceNumber,
        ?int $customerId,
        string $saleType,
        string $paymentMethodCode,
        array $invoiceLines,
        array $factusSettings
    ): void {
        $companyStmt = $pdo->prepare(
            'SELECT nombre, nit, email, telefono, direccion
             FROM empresas
             WHERE id = :empresa_id
             LIMIT 1'
        );
        $companyStmt->execute([':empresa_id' => $companyId]);
        $company = $companyStmt->fetch() ?: [];

        $customer = [];
        if ($customerId !== null && $customerId > 0) {
            $customerStmt = $pdo->prepare(
                'SELECT nombre, documento, email, telefono, direccion
                 FROM clientes
                 WHERE id = :cliente_id
                   AND empresa_id = :empresa_id
                 LIMIT 1'
            );
            $customerStmt->execute([
                ':cliente_id' => $customerId,
                ':empresa_id' => $companyId,
            ]);
            $customer = $customerStmt->fetch() ?: [];
        }

        $factusItems = [];
        foreach ($invoiceLines as $line) {
            if (!is_array($line)) {
                continue;
            }

            $quantity = (float) ($line['cantidad'] ?? 0);
            $unitPrice = (float) ($line['precio_unitario'] ?? 0);
            if ($quantity <= 0 || $unitPrice <= 0) {
                continue;
            }

            $lineSubtotal = $quantity * $unitPrice;
            $lineDiscount = max(0.0, (float) ($line['descuento'] ?? 0));
            $lineDiscountRate = $lineSubtotal > 0.0001
                ? round(min(100.0, ($lineDiscount * 100) / $lineSubtotal), 2)
                : 0.0;

            $taxRate = max(0.0, min(100.0, (float) ($line['tasa_impuesto'] ?? 0)));
            $sku = trim((string) ($line['sku'] ?? ''));
            $productId = (int) ($line['producto_id'] ?? 0);
            $description = trim((string) ($line['descripcion'] ?? 'Producto'));

            $factusItems[] = [
                'code_reference' => $sku !== '' ? substr($sku, 0, 120) : ('P-' . $productId),
                'name' => substr($description !== '' ? $description : ('Producto #' . $productId), 0, 255),
                'quantity' => round($quantity, 2),
                'price' => round($unitPrice, 2),
                'discount_rate' => $lineDiscountRate,
                'tax_rate' => $taxRate,
                'unit_measure_id' => 70,
                'standard_code_id' => 1,
                'tribute_id' => 1,
                'is_excluded' => $taxRate > 0 ? 0 : 1,
            ];
        }

        if ($factusItems === []) {
            throw new RuntimeException('No hay líneas válidas para emitir factura electrónica en Factus.');
        }

        $paymentMap = $this->factusPaymentConfig($saleType, $paymentMethodCode);

        $context = [
            'reference_code' => $invoiceNumber !== '' ? $invoiceNumber : ('POS-' . $saleId),
            'observation' => 'Venta POS #' . $saleId . ' emitida desde Veltrix.',
            'payment_form' => $paymentMap['payment_form'],
            'payment_method_code' => $paymentMap['payment_method_code'],
            'operation_type' => 10,
            'company' => [
                'name' => (string) ($company['nombre'] ?? 'Veltrix POS'),
                'email' => (string) ($company['email'] ?? ''),
                'phone' => (string) ($company['telefono'] ?? ''),
                'address' => (string) ($company['direccion'] ?? ''),
            ],
            'customer' => [
                'name' => (string) ($customer['nombre'] ?? 'Consumidor final'),
                'identification' => (string) ($customer['documento'] ?? ($company['nit'] ?? '')),
                'email' => (string) ($customer['email'] ?? ''),
                'phone' => (string) ($customer['telefono'] ?? ''),
                'address' => (string) ($customer['direccion'] ?? ''),
            ],
            'items' => $factusItems,
        ];

        $factusResponse = (new FactusService())->issueSaleInvoice($factusSettings, $context);
        if (!(bool) ($factusResponse['ok'] ?? false)) {
            throw new RuntimeException((string) ($factusResponse['message'] ?? 'Factus no permitió emitir la factura.'));
        }

        $invoiceData = is_array($factusResponse['data'] ?? null) ? $factusResponse['data'] : [];
        $factusNumber = trim((string) ($invoiceData['number'] ?? $invoiceData['number_full'] ?? ''));
        $cufe = trim((string) ($invoiceData['cufe'] ?? ''));
        $qrPath = $this->firstNonEmptyString($invoiceData, [
            'qr_path',
            'qr_url',
            'qr_link',
            'qr_code',
            'qr',
            'url',
        ]);
        $statusRaw = strtolower(trim((string) ($invoiceData['status'] ?? $invoiceData['state'] ?? 'aceptada')));
        $status = $this->normalizeFactusStatus($statusRaw);

        $updateInvoiceStmt = $pdo->prepare(
            'UPDATE facturas
             SET numero = :numero,
                 cufe = :cufe,
                 qr_path = :qr_path,
                 estado = :estado,
                 updated_at = NOW()
             WHERE id = :id
               AND empresa_id = :empresa_id'
        );
        $updateInvoiceStmt->execute([
            ':numero' => substr($factusNumber !== '' ? $factusNumber : $invoiceNumber, 0, 80),
            ':cufe' => $cufe !== '' ? substr($cufe, 0, 120) : null,
            ':qr_path' => $qrPath !== '' ? substr($qrPath, 0, 255) : null,
            ':estado' => $status,
            ':id' => $invoiceId,
            ':empresa_id' => $companyId,
        ]);
    }

    private function factusPaymentConfig(string $saleType, string $paymentMethodCode): array
    {
        $saleType = strtolower(trim($saleType));
        $method = strtolower(trim($paymentMethodCode));

        $paymentForm = $saleType === 'credito' ? '2' : '1';
        $paymentMethod = match ($method) {
            'transferencia', 'pse', 'nequi', 'daviplata' => '42',
            'tarjeta', 'tarjeta_credito', 'tarjeta_debito', 'dataphone', 'bold', 'bold_dataphone', 'bold_button', 'credibanco', 'credibanco_dataphone', 'credibanco_datafono' => '48',
            default => '10', // efectivo
        };

        return [
            'payment_form' => $paymentForm,
            'payment_method_code' => $paymentMethod,
        ];
    }

    private function normalizeFactusStatus(string $status): string
    {
        $status = strtolower(trim($status));
        if ($status === '') {
            return 'aceptada';
        }

        if (str_contains($status, 'rechaz')) {
            return 'rechazada';
        }

        if (str_contains($status, 'acept') || str_contains($status, 'valid') || str_contains($status, 'aprob')) {
            return 'aceptada';
        }

        return 'emitida';
    }

    private function firstNonEmptyString(array $source, array $keys): string
    {
        foreach ($keys as $key) {
            $value = trim((string) ($source[$key] ?? ''));
            if ($value !== '') {
                return $value;
            }
        }

        return '';
    }

    private function isBoldPaymentMethod(string $paymentMethodCode): bool
    {
        $code = strtolower(trim($paymentMethodCode));
        return in_array($code, ['bold', 'bold_dataphone', 'bold_button'], true);
    }

    private function isBoldDataphonePaymentMethod(string $paymentMethodCode): bool
    {
        $code = strtolower(trim($paymentMethodCode));
        return in_array($code, ['bold', 'bold_dataphone'], true);
    }

    private function isCredibancoDataphonePaymentMethod(string $paymentMethodCode): bool
    {
        $code = strtolower(trim($paymentMethodCode));
        return in_array($code, ['credibanco', 'credibanco_dataphone', 'credibanco_datafono'], true);
    }

    private function resolveBoldRuntimeSettings(int $companyId, string $selectedPaymentMethodCode = ''): array
    {
        $raw = (new CompanySetting())->getMany($companyId, [
            'bold_enabled',
            'bold_mode',
            'bold_api_url',
            'bold_api_key',
            'bold_button_public_key',
            'bold_button_secret_key',
        ]);

        $enabled = trim((string) ($raw['bold_enabled'] ?? '0')) === '1';
        if (!$enabled) {
            throw new RuntimeException('El método Bold está seleccionado, pero la integración Bold está desactivada.');
        }

        $requestedCode = strtolower(trim($selectedPaymentMethodCode));
        $mode = match ($requestedCode) {
            'bold_dataphone' => 'dataphone',
            'bold_button' => 'button',
            default => strtolower(trim((string) ($raw['bold_mode'] ?? 'dataphone'))),
        };
        if (!in_array($mode, ['dataphone', 'button'], true)) {
            $mode = 'dataphone';
        }

        if ($mode === 'button') {
            $buttonPublicKey = trim(SecretCipherService::decrypt((string) ($raw['bold_button_public_key'] ?? '')));
            $buttonSecretKey = trim(SecretCipherService::decrypt((string) ($raw['bold_button_secret_key'] ?? '')));
            if ($buttonPublicKey === '' || $buttonSecretKey === '') {
                throw new RuntimeException('Faltan llaves de Botón de pagos Bold en Integraciones para registrar la venta.');
            }

            return [
                'mode' => 'button',
                'button_public_key' => $buttonPublicKey,
                'button_secret_key' => $buttonSecretKey,
            ];
        }

        $apiUrl = rtrim(trim((string) ($raw['bold_api_url'] ?? '')), '/');
        $apiKey = trim(SecretCipherService::decrypt((string) ($raw['bold_api_key'] ?? '')));

        if ($apiUrl === '' || $apiKey === '') {
            throw new RuntimeException('Faltan credenciales de Bold en Integraciones para validar la venta.');
        }

        if (!filter_var($apiUrl, FILTER_VALIDATE_URL) || !preg_match('/^https?:\/\//i', $apiUrl)) {
            throw new RuntimeException('La URL API de Bold no es válida. Revisa Integraciones.');
        }

        return [
            'mode' => 'dataphone',
            'api_url' => $apiUrl,
            'api_key' => $apiKey,
        ];
    }

    private function hasVariantQuantityColumn(PDO $pdo): bool
    {
        if ($this->variantQuantityColumnAvailable !== null) {
            return $this->variantQuantityColumnAvailable;
        }

        $stmt = $pdo->prepare(
            "SELECT COUNT(*) FROM information_schema.COLUMNS
             WHERE TABLE_SCHEMA = DATABASE()
               AND TABLE_NAME = 'producto_variantes'
               AND COLUMN_NAME = 'cantidad'"
        );
        $stmt->execute();
        $this->variantQuantityColumnAvailable = (int) $stmt->fetchColumn() > 0;

        return $this->variantQuantityColumnAvailable;
    }

    private function hasVariantTable(PDO $pdo): bool
    {
        $stmt = $pdo->prepare(
            "SELECT COUNT(*) FROM information_schema.TABLES
             WHERE TABLE_SCHEMA = DATABASE()
               AND TABLE_NAME = 'producto_variantes'"
        );
        $stmt->execute();

        return (int) $stmt->fetchColumn() > 0;
    }
}
