<?php

declare(strict_types=1);

namespace App\Controllers;

use App\Core\Controller;
use App\Core\Request;
use App\Core\Session;
use App\Core\Tenant;
use App\Models\Category;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\Supplier;
use App\Models\Tax;
use App\Models\UnitMeasure;
use App\Services\AuditService;
use PDOException;

class ProductController extends Controller
{
    public function index(Request $request): void
    {
        $companyId = (int) Tenant::companyId();
        $unitModel = new UnitMeasure();
        $productModel = new Product();
        $variantModel = new ProductVariant();
        $products = $productModel->listByCompany($companyId);
        $productVariantsMap = $variantModel->listGroupedByProduct($companyId);

        foreach ($products as &$product) {
            $productId = (int) ($product['id'] ?? 0);
            if ($productId <= 0) {
                continue;
            }

            $variants = $productVariantsMap[$productId] ?? [];
            if ($variants === []) {
                continue;
            }

            $stockActualSum = 0.0;
            $stockMinimoSum = 0.0;
            $hasQuantityValues = false;
            $hasMinimumValues = false;

            foreach ($variants as $variant) {
                $isActive = (int) ($variant['activo'] ?? 1) === 1;
                if (!$isActive) {
                    continue;
                }

                if (array_key_exists('cantidad', $variant) && $variant['cantidad'] !== null) {
                    $hasQuantityValues = true;
                    $stockActualSum += max(0, (float) $variant['cantidad']);
                }

                if (array_key_exists('stock_minimo', $variant) && $variant['stock_minimo'] !== null) {
                    $hasMinimumValues = true;
                    $stockMinimoSum += max(0, (float) $variant['stock_minimo']);
                }
            }

            if ($hasQuantityValues) {
                $product['stock_actual'] = $stockActualSum;
            }

            if ($hasMinimumValues) {
                $product['stock_minimo'] = $stockMinimoSum;
            }
        }
        unset($product);

        $this->render('productos/index', [
            'title' => 'Productos',
            'products' => $products,
            'productVariantsMap' => $productVariantsMap,
            'categories' => (new Category())->all([], $companyId),
            'suppliers' => (new Supplier())->all(['activo' => 1], $companyId, 'nombre ASC'),
            'taxes' => (new Tax())->listActiveByCompany($companyId),
            'units' => $unitModel->listByCompany($companyId, true),
            'units_all' => $unitModel->listByCompany($companyId),
        ]);
    }

    public function store(Request $request): never
    {
        $companyId = (int) Tenant::companyId();
        $productModel = new Product();
        $name = trim((string) $request->input('nombre', ''));
        $price = (float) $request->input('precio_venta', 0);
        $categoryId = (int) $request->input('categoria_id', 0);
        $supplierId = (int) $request->input('proveedor_id', 0);
        $unitId = (int) $request->input('unidad_medida_id', 0);
        $skuValue = trim((string) $request->input('sku', ''));
        $sku = $skuValue !== '' ? $skuValue : null;

        Session::setOldInput($request->all());

        if ($name === '' || $price <= 0 || $categoryId <= 0 || $supplierId <= 0 || $unitId <= 0) {
            flash('error', 'Nombre, categoría, proveedor, unidad de medida y precio de venta son obligatorios.');
            redirect('productos');
        }

        $category = (new Category())->findById($categoryId, $companyId);
        if ($category === null || (int) ($category['activo'] ?? 0) !== 1) {
            flash('error', 'La categoría seleccionada no es válida o está inactiva.');
            redirect('productos');
        }

        $supplier = (new Supplier())->findById($supplierId, $companyId);
        if ($supplier === null || (int) ($supplier['activo'] ?? 0) !== 1) {
            flash('error', 'El proveedor seleccionado no es válido o está inactivo.');
            redirect('productos');
        }

        $unit = (new UnitMeasure())->findById($unitId, $companyId);
        if ($unit === null || (int) ($unit['activo'] ?? 0) !== 1) {
            flash('error', 'La unidad de medida seleccionada no es válida o está inactiva.');
            redirect('productos');
        }

        if ($sku !== null && $productModel->skuExists($companyId, $sku)) {
            flash('error', 'Ya existe un producto con ese SKU en esta empresa.');
            redirect('productos');
        }

        try {
            $variantPayload = $this->parseVariantPayload($request, $price);
        } catch (\Throwable $e) {
            flash('error', $e->getMessage());
            redirect('productos');
        }
        $resolvedStock = $this->resolveProductStockValues($request, $variantPayload);

        try {
            $selectedTax = $this->resolveSelectedTax($request, $companyId);
        } catch (\Throwable $e) {
            flash('error', $e->getMessage());
            redirect('productos');
        }

        try {
            $imagePath = $this->storeProductImage($request->file('imagen'), true) ?? $this->defaultProductImagePath();
        } catch (\Throwable $e) {
            flash('error', $e->getMessage());
            redirect('productos');
        }

        try {
            $id = $productModel->create([
                'empresa_id' => $companyId,
                'categoria_id' => $categoryId,
                'proveedor_id' => $supplierId,
                'unidad_medida_id' => $unitId,
                'nombre' => $name,
                'codigo_barras' => $request->input('codigo_barras') ?: null,
                'sku' => $sku,
                'descripcion' => trim((string) $request->input('descripcion', '')) ?: null,
                'imagen' => $imagePath,
                'precio_compra' => (float) $request->input('precio_compra', 0),
                'precio_venta' => $price,
                'stock_actual' => (float) ($resolvedStock['stock_actual'] ?? 0),
                'stock_minimo' => (float) ($resolvedStock['stock_minimo'] ?? 0),
                'impuesto_porcentaje' => (float) ($selectedTax['rate'] ?? 0),
                'activo' => 1,
                'created_at' => date('Y-m-d H:i:s'),
                'updated_at' => date('Y-m-d H:i:s'),
            ]);
        } catch (\Throwable $e) {
            if ($this->isDuplicateSkuException($e)) {
                flash('error', 'El SKU ya está en uso para otro producto de esta empresa.');
                redirect('productos');
            }

            flash('error', 'No fue posible registrar el producto: ' . $e->getMessage());
            redirect('productos');
        }

        $this->syncProductVariants($companyId, $id, $variantPayload);

        Session::clearOldInput();
        AuditService::track('productos.crear', 'Producto creado ID: ' . $id);
        flash('success', 'Producto registrado correctamente.');
        redirect('productos');
    }

    public function update(Request $request): never
    {
        $companyId = (int) Tenant::companyId();
        $productId = (int) $request->param('id', 0);

        if ($companyId <= 0 || $productId <= 0) {
            flash('error', 'No fue posible identificar el producto a editar.');
            redirect('productos');
        }

        $model = new Product();
        $product = $model->findById($productId, $companyId);
        if ($product === null) {
            flash('error', 'El producto no existe para esta empresa.');
            redirect('productos');
        }

        Session::setOldInput($request->all());
        flash('product_edit_modal', (string) $productId);

        $name = trim((string) $request->input('nombre', ''));
        $price = (float) $request->input('precio_venta', 0);
        $categoryId = (int) $request->input('categoria_id', 0);
        $supplierId = (int) $request->input('proveedor_id', 0);
        $unitId = (int) $request->input('unidad_medida_id', 0);
        $skuValue = trim((string) $request->input('sku', ''));
        $sku = $skuValue !== '' ? $skuValue : null;

        if ($name === '' || $price <= 0 || $categoryId <= 0 || $supplierId <= 0 || $unitId <= 0) {
            flash('error', 'Nombre, categoría, proveedor, unidad de medida y precio de venta son obligatorios.');
            redirect('productos');
        }

        $category = (new Category())->findById($categoryId, $companyId);
        if ($category === null || (int) ($category['activo'] ?? 0) !== 1) {
            flash('error', 'La categoría seleccionada no es válida o está inactiva.');
            redirect('productos');
        }

        $supplier = (new Supplier())->findById($supplierId, $companyId);
        if ($supplier === null || (int) ($supplier['activo'] ?? 0) !== 1) {
            flash('error', 'El proveedor seleccionado no es válido o está inactivo.');
            redirect('productos');
        }

        $unit = (new UnitMeasure())->findById($unitId, $companyId);
        if ($unit === null || (int) ($unit['activo'] ?? 0) !== 1) {
            flash('error', 'La unidad de medida seleccionada no existe o está inactiva para esta empresa.');
            redirect('productos');
        }

        if ($sku !== null && $model->skuExists($companyId, $sku, $productId)) {
            flash('error', 'Ya existe otro producto con ese SKU en esta empresa.');
            redirect('productos');
        }

        try {
            $variantPayload = $this->parseVariantPayload($request, $price);
        } catch (\Throwable $e) {
            flash('error', $e->getMessage());
            redirect('productos');
        }
        $resolvedStock = $this->resolveProductStockValues($request, $variantPayload);

        try {
            $selectedTax = $this->resolveSelectedTax($request, $companyId);
        } catch (\Throwable $e) {
            flash('error', $e->getMessage());
            redirect('productos');
        }

        $applyDefaultImage = (int) $request->input('usar_imagen_defecto', 0) === 1;
        $newUploadedImage = null;
        try {
            $newUploadedImage = $this->storeProductImage($request->file('imagen'), false);
        } catch (\Throwable $e) {
            flash('error', $e->getMessage());
            redirect('productos');
        }

        if ($applyDefaultImage && $newUploadedImage !== null) {
            $this->deleteProductImage($newUploadedImage);
            $newUploadedImage = null;
        }

        $updateData = [
            'categoria_id' => $categoryId,
            'proveedor_id' => $supplierId,
            'unidad_medida_id' => $unitId,
            'nombre' => $name,
            'codigo_barras' => $request->input('codigo_barras') ?: null,
            'sku' => $sku,
            'descripcion' => trim((string) $request->input('descripcion', '')) ?: null,
            'precio_compra' => (float) $request->input('precio_compra', 0),
            'precio_venta' => $price,
            'stock_actual' => (float) ($resolvedStock['stock_actual'] ?? 0),
            'stock_minimo' => (float) ($resolvedStock['stock_minimo'] ?? 0),
            'impuesto_porcentaje' => (float) ($selectedTax['rate'] ?? 0),
            'updated_at' => date('Y-m-d H:i:s'),
        ];

        if ($applyDefaultImage) {
            $updateData['imagen'] = $this->defaultProductImagePath();
        } elseif ($newUploadedImage !== null) {
            $updateData['imagen'] = $newUploadedImage;
        }

        $previousImagePath = trim((string) ($product['imagen'] ?? ''));
        $nextImagePath = trim((string) ($updateData['imagen'] ?? $previousImagePath));

        try {
            $model->updateById($productId, $updateData, $companyId);
        } catch (\Throwable $e) {
            if ($newUploadedImage !== null) {
                $this->deleteProductImage($newUploadedImage);
            }

            if ($this->isDuplicateSkuException($e)) {
                flash('error', 'El SKU ya está en uso para otro producto de esta empresa.');
                redirect('productos');
            }

            flash('error', 'No fue posible actualizar el producto: ' . $e->getMessage());
            redirect('productos');
        }

        if ($nextImagePath !== '' && $previousImagePath !== '' && $nextImagePath !== $previousImagePath) {
            $this->deleteProductImage($previousImagePath);
        }

        $this->syncProductVariants($companyId, $productId, $variantPayload);

        Session::clearOldInput();
        flash('success', 'Producto actualizado correctamente.');
        AuditService::track('productos.actualizar', 'Producto actualizado ID: ' . $productId);
        redirect('productos');
    }

    public function downloadTemplate(Request $request): never
    {
        $headers = [
            'nombre',
            'sku',
            'codigo_barras',
            'descripcion',
            'precio_compra',
            'precio_venta',
            'impuesto_porcentaje',
            'stock_actual',
            'stock_minimo',
            'categoria_nombre',
            'proveedor_nombre',
            'unidad_nombre',
        ];

        $example = [
            'Camiseta Básica',
            'CAM-001',
            '7701234567890',
            'Camiseta de algodón talla M',
            '15000',
            '30000',
            '19',
            '50',
            '5',
            'Ropa',
            'Proveedor Ejemplo',
            'Unidad',
        ];

        header('Content-Type: text/csv; charset=utf-8');
        header('Content-Disposition: attachment; filename="plantilla_productos.csv"');
        header('Cache-Control: no-cache, no-store, must-revalidate');

        $out = fopen('php://output', 'w');
        fprintf($out, chr(0xEF) . chr(0xBB) . chr(0xBF)); // BOM UTF-8 para Excel
        fputcsv($out, $headers, ',');
        fputcsv($out, $example, ',');
        fclose($out);
        exit;
    }

    public function import(Request $request): never
    {
        $companyId = (int) Tenant::companyId();
        $file = $request->file('archivo_csv');

        if (!is_array($file) || (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
            flash('error', 'Debes seleccionar un archivo CSV válido.');
            redirect('productos');
        }

        $tmpPath = (string) ($file['tmp_name'] ?? '');
        if ($tmpPath === '' || !is_file($tmpPath)) {
            flash('error', 'No se pudo leer el archivo subido.');
            redirect('productos');
        }

        $originalName = strtolower((string) ($file['name'] ?? ''));
        if (!str_ends_with($originalName, '.csv')) {
            flash('error', 'El archivo debe ser un CSV (.csv).');
            redirect('productos');
        }

        // Cargar catálogos de la empresa
        $categoryModel = new Category();
        $supplierModel = new Supplier();
        $unitModel = new UnitMeasure();
        $productModel = new Product();

        $categoriesByName = [];
        foreach ($categoryModel->all([], $companyId) as $cat) {
            $key = mb_strtolower(trim((string) ($cat['nombre'] ?? '')));
            if ($key !== '') {
                $categoriesByName[$key] = $cat;
            }
        }

        $suppliersByName = [];
        foreach ($supplierModel->all(['activo' => 1], $companyId, 'nombre ASC') as $sup) {
            $key = mb_strtolower(trim((string) ($sup['nombre'] ?? '')));
            if ($key !== '') {
                $suppliersByName[$key] = $sup;
            }
        }

        $unitsByName = [];
        foreach ($unitModel->listByCompany($companyId, true) as $unit) {
            $keyName = mb_strtolower(trim((string) ($unit['nombre'] ?? '')));
            $keyAbrev = mb_strtolower(trim((string) ($unit['abreviatura'] ?? '')));
            if ($keyName !== '') {
                $unitsByName[$keyName] = $unit;
            }
            if ($keyAbrev !== '' && !isset($unitsByName[$keyAbrev])) {
                $unitsByName[$keyAbrev] = $unit;
            }
        }

        $handle = fopen($tmpPath, 'r');
        if ($handle === false) {
            flash('error', 'No se pudo abrir el archivo.');
            redirect('productos');
        }

        // Detectar BOM UTF-8
        $bom = fread($handle, 3);
        if ($bom !== chr(0xEF) . chr(0xBB) . chr(0xBF)) {
            rewind($handle);
        }

        $headerRow = fgetcsv($handle, 0, ',');
        if (!is_array($headerRow)) {
            fclose($handle);
            flash('error', 'El archivo CSV no tiene encabezados válidos.');
            redirect('productos');
        }

        // Normalizar encabezados
        $headers = array_map(static fn($h) => mb_strtolower(trim((string) $h)), $headerRow);
        $colIndex = array_flip($headers);

        $requiredCols = ['nombre', 'sku', 'precio_venta', 'categoria_nombre', 'proveedor_nombre', 'unidad_nombre'];
        $missingCols = array_diff($requiredCols, array_keys($colIndex));
        if ($missingCols !== []) {
            fclose($handle);
            flash('error', 'Faltan columnas obligatorias: ' . implode(', ', $missingCols) . '. Descarga la plantilla correcta.');
            redirect('productos');
        }

        // accion_duplicado: 'omitir' (default) | 'actualizar'
        $duplicateAction = trim((string) $request->input('accion_duplicado', 'omitir'));
        if (!in_array($duplicateAction, ['omitir', 'actualizar'], true)) {
            $duplicateAction = 'omitir';
        }

        $imported = 0;
        $updated = 0;
        $skipped = 0;
        $errors = [];
        $rowNumber = 1;

        while (($row = fgetcsv($handle, 0, ',')) !== false) {
            $rowNumber++;
            if (array_filter($row, static fn($v) => trim((string) $v) !== '') === []) {
                continue; // fila vacía
            }

            $get = static function (string $col) use ($row, $colIndex): string {
                $idx = $colIndex[$col] ?? -1;
                return $idx >= 0 ? trim((string) ($row[$idx] ?? '')) : '';
            };

            $name = $get('nombre');
            $sku = $get('sku');
            $priceStr = $get('precio_venta');
            $catName = $get('categoria_nombre');
            $supName = $get('proveedor_nombre');
            $unitName = $get('unidad_nombre');

            if ($name === '' || $sku === '' || $priceStr === '' || $catName === '' || $supName === '' || $unitName === '') {
                $errors[] = "Fila {$rowNumber}: nombre, sku, precio_venta, categoria_nombre, proveedor_nombre y unidad_nombre son obligatorios.";
                continue;
            }

            $price = (float) str_replace(',', '.', $priceStr);
            if ($price <= 0) {
                $errors[] = "Fila {$rowNumber}: precio_venta debe ser mayor a cero.";
                continue;
            }

            $catKey = mb_strtolower($catName);
            if (!isset($categoriesByName[$catKey])) {
                $errors[] = "Fila {$rowNumber}: categoría '{$catName}' no encontrada en la empresa.";
                continue;
            }
            $category = $categoriesByName[$catKey];
            if ((int) ($category['activo'] ?? 0) !== 1) {
                $errors[] = "Fila {$rowNumber}: categoría '{$catName}' está inactiva.";
                continue;
            }

            $supKey = mb_strtolower($supName);
            if (!isset($suppliersByName[$supKey])) {
                $errors[] = "Fila {$rowNumber}: proveedor '{$supName}' no encontrado o inactivo.";
                continue;
            }

            $unitKey = mb_strtolower($unitName);
            if (!isset($unitsByName[$unitKey])) {
                $errors[] = "Fila {$rowNumber}: unidad de medida '{$unitName}' no encontrada o inactiva.";
                continue;
            }

            $categoryId = (int) $category['id'];
            $supplierId = (int) $suppliersByName[$supKey]['id'];
            $unitId = (int) $unitsByName[$unitKey]['id'];

            $taxRate = (float) str_replace(',', '.', $get('impuesto_porcentaje'));
            if ($taxRate < 0 || $taxRate > 100) {
                $taxRate = 0;
            }

            $productData = [
                'categoria_id' => $categoryId,
                'proveedor_id' => $supplierId,
                'unidad_medida_id' => $unitId,
                'nombre' => $name,
                'sku' => $sku,
                'codigo_barras' => $get('codigo_barras') !== '' ? $get('codigo_barras') : null,
                'descripcion' => $get('descripcion') !== '' ? $get('descripcion') : null,
                'precio_compra' => (float) str_replace(',', '.', $get('precio_compra')),
                'precio_venta' => $price,
                'impuesto_porcentaje' => $taxRate,
                'stock_actual' => max(0, (float) str_replace(',', '.', $get('stock_actual'))),
                'stock_minimo' => max(0, (float) str_replace(',', '.', $get('stock_minimo'))),
            ];

            // Verificar si el SKU ya existe
            $existing = $productModel->findBySku($companyId, $sku);

            if ($existing !== null) {
                if ($duplicateAction === 'omitir') {
                    $skipped++;
                    continue;
                }

                // actualizar
                try {
                    $productData['updated_at'] = date('Y-m-d H:i:s');
                    $productModel->updateById((int) $existing['id'], $productData, $companyId);
                    $updated++;
                } catch (\Throwable $e) {
                    $errors[] = "Fila {$rowNumber} (SKU '{$sku}'): " . $e->getMessage();
                }
                continue;
            }

            // Crear nuevo
            try {
                $productModel->create(array_merge($productData, [
                    'empresa_id' => $companyId,
                    'imagen' => $this->defaultProductImagePath(),
                    'activo' => 1,
                    'created_at' => date('Y-m-d H:i:s'),
                    'updated_at' => date('Y-m-d H:i:s'),
                ]));
                $imported++;
            } catch (\Throwable $e) {
                $errors[] = "Fila {$rowNumber}: " . $e->getMessage();
            }
        }

        fclose($handle);

        $summary = "Importación masiva: {$imported} creados, {$updated} actualizados, {$skipped} omitidos.";
        AuditService::track('productos.importar', $summary);

        $parts = [];
        if ($imported > 0) {
            $parts[] = "{$imported} creado(s)";
        }
        if ($updated > 0) {
            $parts[] = "{$updated} actualizado(s)";
        }
        if ($skipped > 0) {
            $parts[] = "{$skipped} omitido(s) por SKU duplicado";
        }

        $resultMsg = $parts !== [] ? implode(', ', $parts) . '.' : 'Ningún producto procesado.';

        if ($errors === [] && ($imported > 0 || $updated > 0)) {
            flash('success', "Importación completada: {$resultMsg}");
        } elseif ($errors !== [] && ($imported > 0 || $updated > 0)) {
            flash('warning', "Importación parcial: {$resultMsg} " . count($errors) . ' error(es): ' . implode(' | ', array_slice($errors, 0, 5)));
        } else {
            flash('error', 'No se procesó ningún producto. Errores: ' . implode(' | ', array_slice($errors, 0, 5)));
        }

        redirect('productos');
    }

    private function defaultProductImagePath(): string
    {
        return 'assets/img/default-producto.jpg';
    }

    private function storeProductImage(mixed $imageFile, bool $fallbackToDefault = true): ?string
    {
        if (!is_array($imageFile)) {
            return $fallbackToDefault ? $this->defaultProductImagePath() : null;
        }

        $errorCode = (int) ($imageFile['error'] ?? UPLOAD_ERR_NO_FILE);
        if ($errorCode === UPLOAD_ERR_NO_FILE) {
            return $fallbackToDefault ? $this->defaultProductImagePath() : null;
        }

        if ($errorCode !== UPLOAD_ERR_OK) {
            throw new \RuntimeException('No fue posible cargar la imagen del producto.');
        }

        $tmpPath = (string) ($imageFile['tmp_name'] ?? '');
        $size = (int) ($imageFile['size'] ?? 0);
        $originalName = (string) ($imageFile['name'] ?? 'producto');

        if ($tmpPath === '' || !is_file($tmpPath) || $size <= 0) {
            throw new \RuntimeException('La imagen del producto no es válida.');
        }

        $maxSize = 4 * 1024 * 1024;
        if ($size > $maxSize) {
            throw new \RuntimeException('La imagen supera el tamaño máximo de 4MB.');
        }

        $extension = strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION));
        $allowedExtensions = ['jpg', 'jpeg', 'png', 'webp'];
        if (!in_array($extension, $allowedExtensions, true)) {
            throw new \RuntimeException('Formato no permitido. Usa JPG, PNG o WEBP.');
        }

        $mimeType = function_exists('mime_content_type')
            ? strtolower((string) (mime_content_type($tmpPath) ?: ''))
            : '';
        $allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp'];
        if ($mimeType !== '' && !in_array($mimeType, $allowedMimeTypes, true)) {
            throw new \RuntimeException('El archivo subido no parece una imagen válida.');
        }

        $uploadDir = base_path('public/uploads/productos');
        if (!is_dir($uploadDir) && !mkdir($uploadDir, 0775, true) && !is_dir($uploadDir)) {
            throw new \RuntimeException('No se pudo crear la carpeta de imágenes de productos.');
        }

        try {
            $token = bin2hex(random_bytes(8));
        } catch (\Throwable) {
            $token = (string) mt_rand(10000000, 99999999);
        }

        $fileName = 'producto_' . date('Ymd_His') . '_' . $token . '.' . $extension;
        $targetPath = $uploadDir . '/' . $fileName;

        if (!move_uploaded_file($tmpPath, $targetPath)) {
            if (!rename($tmpPath, $targetPath)) {
                throw new \RuntimeException('No fue posible guardar la imagen en el servidor.');
            }
        }

        return 'uploads/productos/' . $fileName;
    }

    private function deleteProductImage(string $path): void
    {
        $normalized = ltrim(str_replace('\\', '/', trim($path)), '/');
        if ($normalized === '' || !str_starts_with($normalized, 'uploads/productos/')) {
            return;
        }

        $fullPath = base_path('public/' . $normalized);
        if (is_file($fullPath)) {
            @unlink($fullPath);
        }
    }

    private function resolveSelectedTax(Request $request, int $companyId): array
    {
        $taxId = (int) $request->input('impuesto_id', 0);
        $fallbackRate = (float) $request->input('impuesto_porcentaje', 0);

        if ($taxId <= 0) {
            if ($fallbackRate < 0 || $fallbackRate > 100) {
                throw new \RuntimeException('El porcentaje de impuesto debe estar entre 0 y 100.');
            }

            return [
                'id' => null,
                'rate' => $fallbackRate,
            ];
        }

        $tax = (new Tax())->findAvailableForCompanyById($companyId, $taxId);
        if ($tax === null) {
            throw new \RuntimeException('El impuesto seleccionado no existe para la empresa actual.');
        }

        if ((int) ($tax['activo'] ?? 0) !== 1) {
            throw new \RuntimeException('El impuesto seleccionado está inactivo.');
        }

        $rate = (float) ($tax['porcentaje'] ?? 0);
        if ($rate < 0 || $rate > 100) {
            throw new \RuntimeException('El impuesto seleccionado tiene un porcentaje inválido.');
        }

        return [
            'id' => $taxId,
            'rate' => $rate,
        ];
    }

    private function parseVariantPayload(Request $request, float $basePrice): array
    {
        $enabled = (int) $request->input('variantes_habilitadas', 0) === 1;
        if (!$enabled) {
            return [
                'enabled' => false,
                'items' => [],
            ];
        }

        $mode = strtolower(trim((string) $request->input('variantes_modo', 'same')));
        if (!in_array($mode, ['same', 'individual'], true)) {
            $mode = 'same';
        }

        $names = (array) $request->input('variante_nombre', []);
        $prices = (array) $request->input('variante_precio', []);
        $quantities = (array) $request->input('variante_cantidad', []);
        $minStocks = (array) $request->input('variante_stock_minimo', []);

        $items = [];
        $seen = [];
        foreach ($names as $index => $variantName) {
            $name = trim((string) $variantName);
            if ($name === '') {
                continue;
            }

            $key = mb_strtolower($name);
            if (isset($seen[$key])) {
                throw new \RuntimeException('No puedes repetir nombres de variantes para el mismo producto.');
            }
            $seen[$key] = true;

            $price = $mode === 'same'
                ? $basePrice
                : (float) ($prices[$index] ?? 0);

            if ($price <= 0) {
                throw new \RuntimeException('Cada variante debe tener un precio de venta mayor a cero.');
            }

            $quantityRaw = (float) ($quantities[$index] ?? 0);
            $quantity = max(0, (int) floor($quantityRaw));
            $minStockRaw = (float) ($minStocks[$index] ?? 0);
            $minStock = max(0, (int) floor($minStockRaw));

            $items[] = [
                'nombre' => $name,
                'precio_venta' => $price,
                'cantidad' => $quantity,
                'stock_minimo' => $minStock,
            ];
        }

        if ($items === []) {
            throw new \RuntimeException('Debes registrar al menos una variante cuando activas variantes en el producto.');
        }

        return [
            'enabled' => true,
            'items' => $items,
        ];
    }

    private function syncProductVariants(int $companyId, int $productId, array $variantPayload): void
    {
        $model = new ProductVariant();
        if (!((bool) ($variantPayload['enabled'] ?? false))) {
            $model->deleteByProduct($companyId, $productId);
            return;
        }

        $model->replaceByProduct(
            $companyId,
            $productId,
            is_array($variantPayload['items'] ?? null) ? $variantPayload['items'] : []
        );
    }

    private function resolveProductStockValues(Request $request, array $variantPayload): array
    {
        $variantsEnabled = (bool) ($variantPayload['enabled'] ?? false);
        if (!$variantsEnabled) {
            return [
                'stock_actual' => (float) $request->input('stock_actual', 0),
                'stock_minimo' => (float) $request->input('stock_minimo', 0),
            ];
        }

        $items = is_array($variantPayload['items'] ?? null) ? $variantPayload['items'] : [];
        $stockActual = 0;
        $stockMinimo = 0;

        foreach ($items as $item) {
            $stockActual += max(0, (int) ($item['cantidad'] ?? 0));
            $stockMinimo += max(0, (int) ($item['stock_minimo'] ?? 0));
        }

        return [
            'stock_actual' => (float) $stockActual,
            'stock_minimo' => (float) $stockMinimo,
        ];
    }

    private function isDuplicateSkuException(\Throwable $exception): bool
    {
        if ($exception instanceof PDOException) {
            $sqlState = (string) ($exception->errorInfo[0] ?? '');
            $driverCode = (int) ($exception->errorInfo[1] ?? 0);

            if ($sqlState === '23000' || $driverCode === 1062) {
                $message = mb_strtolower($exception->getMessage());
                return str_contains($message, 'uk_productos_empresa_sku')
                    || str_contains($message, 'sku');
            }
        }

        return false;
    }
}
