<?php
/**
 * ----------------------------------------------------------------------------
 * ARCHIVO: tienda/funciones.php
 * ----------------------------------------------------------------------------
 * Reúne la lógica transversal del catálogo público: saneamiento, sesiones de
 * cliente web, construcción segura de consultas con MySQLi prepared
 * statements, resolución de imágenes, formateo de especificaciones, facetas
 * del catálogo, render HTML reusable y generación centralizada de enlaces de
 * producto bajo la regla codigo_proveedor + slug con fallback por
 * codigo_interno.
 *
 * Entradas principales:
 * - $mysqli: conexión MySQLi disponible desde tienda/config/db.php.
 * - $request: arreglo tipo GET para filtros, búsqueda, ordenamiento y paginado.
 *
 * Salidas principales:
 * - Estructuras de catálogo listas para vista y API JSON.
 * - HTML renderizado para grid, paginación, resumen y sidebar de filtros.
 * ----------------------------------------------------------------------------
 */

declare(strict_types=1);

require_once __DIR__ . '/config/db.php';

if (session_status() === PHP_SESSION_ACTIVE && session_name() !== 'REDDEAIRE_PUBLIC') {
    session_write_close();
}

if (session_status() !== PHP_SESSION_ACTIVE) {
    $secure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';

    session_name('REDDEAIRE_PUBLIC');
    session_set_cookie_params([
        'lifetime' => 0,
        'path' => '/',
        'domain' => '',
        'secure' => $secure,
        'httponly' => true,
        'samesite' => 'Lax',
    ]);
    session_start();
}

const TIENDA_POR_PAGINA = 60;
const TIENDA_POR_PAGINA_MAX = 48;
const TIENDA_SLUG_MAX = 120;
const TIENDA_ETIQUETAS_MAX_GRUPOS = 30;
const TIENDA_ETIQUETAS_MAX_OPCIONES = 10;

function e(mixed $valor): string
{
    return htmlspecialchars((string) $valor, ENT_QUOTES, 'UTF-8');
}

function limpiarTexto(mixed $texto, int $maximo = 255): string
{
    $texto = trim((string) $texto);
    $texto = strip_tags($texto);
    $texto = preg_replace('/\s+/u', ' ', $texto) ?? '';

    if ($maximo > 0) {
        $texto = mb_substr($texto, 0, $maximo, 'UTF-8');
    }

    return trim($texto);
}

function truncarTexto(string $texto, int $maximo = 140): string
{
    $texto = limpiarTexto($texto, 0);

    if ($texto === '' || mb_strlen($texto, 'UTF-8') <= $maximo) {
        return $texto;
    }

    $truncado = mb_substr($texto, 0, $maximo, 'UTF-8');
    $ultimoEspacio = mb_strrpos($truncado, ' ', 0, 'UTF-8');

    if ($ultimoEspacio !== false && $ultimoEspacio > (int) floor($maximo * 0.6)) {
        $truncado = mb_substr($truncado, 0, $ultimoEspacio, 'UTF-8');
    }

    return rtrim($truncado, " ,.;:-") . '...';
}

function generarSlug(string $texto): string
{
    $texto = limpiarTexto($texto, 0);

    if ($texto === '') {
        return '';
    }

    $texto = mb_strtolower($texto, 'UTF-8');

    $reemplazos = [
        'á' => 'a', 'à' => 'a', 'ä' => 'a', 'â' => 'a', 'ã' => 'a',
        'é' => 'e', 'è' => 'e', 'ë' => 'e', 'ê' => 'e',
        'í' => 'i', 'ì' => 'i', 'ï' => 'i', 'î' => 'i',
        'ó' => 'o', 'ò' => 'o', 'ö' => 'o', 'ô' => 'o', 'õ' => 'o',
        'ú' => 'u', 'ù' => 'u', 'ü' => 'u', 'û' => 'u',
        'ñ' => 'n', 'ç' => 'c',
    ];

    $texto = strtr($texto, $reemplazos);
    $texto = preg_replace('/[^a-z0-9\s-]/', '', $texto) ?? '';
    $texto = preg_replace('/[\s-]+/', '-', $texto) ?? '';
    $texto = trim($texto, '-');

    if ($texto === '') {
        return '';
    }

    if (mb_strlen($texto, 'UTF-8') <= TIENDA_SLUG_MAX) {
        return $texto;
    }

    $truncado = mb_substr($texto, 0, TIENDA_SLUG_MAX, 'UTF-8');
    $corte = mb_strrpos($truncado, '-', 0, 'UTF-8');

    if ($corte !== false && $corte > 30) {
        $truncado = mb_substr($truncado, 0, $corte, 'UTF-8');
    }

    return trim($truncado, '-');
}

function formatearEspecificaciones(?string $etiquetas, ?string $valores): array
{
    $listaEtiquetas = array_map('trim', explode(',', (string) $etiquetas));
    $listaValores = array_map('trim', explode(',', (string) $valores));
    $especificaciones = [];

    foreach ($listaEtiquetas as $indice => $etiqueta) {
        if ($etiqueta === '') {
            continue;
        }

        $valor = $listaValores[$indice] ?? '';

        $especificaciones[] = [
            'label' => limpiarTexto($etiqueta, 160),
            'value' => limpiarTexto($valor, 220),
        ];
    }

    return $especificaciones;
}

function usuarioClienteLogueado(): bool
{
    return isset(
        $_SESSION['cliente_web_id'],
        $_SESSION['cliente_usuario_id'],
        $_SESSION['cliente_email'],
        $_SESSION['cliente_login_ok']
    ) && $_SESSION['cliente_login_ok'] === true;
}

function puedeVerPreciosStock(): bool
{
    return usuarioClienteLogueado();
}

function tiendaUrlAbsoluta(string $ruta = ''): string
{
    $ruta = '/' . ltrim($ruta, '/');
    return $ruta;
}

function tiendaConstruirUrlLoginCliente(?string $destino = null): string
{
    $url = tiendaUrlAbsoluta('profile-web/login.php');

    if ($destino === null || $destino === '') {
        return $url;
    }

    return $url . '?redirect=' . rawurlencode($destino);
}

function tiendaObtenerImagenPorDefecto(): string
{
    $candidatos = [
        '/proyecto-productos/proyecto-productos/imgsystem/default_articulo.jpg',
        '/proyecto-productos/imgsystem/default_articulo.jpg',
    ];

    foreach ($candidatos as $candidato) {
        $rutaFisica = dirname(__DIR__) . $candidato;
        if (is_file($rutaFisica)) {
            return $candidato;
        }
    }

    return 'https://via.placeholder.com/640x640/f5f5f5/4D3881?text=Producto';
}

function tiendaEsImagenValida(string $ruta): bool
{
    if ($ruta === '') {
        return false;
    }

    if (preg_match('#^(javascript:|data:)#i', $ruta)) {
        return false;
    }

    if (filter_var($ruta, FILTER_VALIDATE_URL)) {
        $esquema = strtolower((string) parse_url($ruta, PHP_URL_SCHEME));
        return in_array($esquema, ['http', 'https'], true);
    }

    $extension = strtolower((string) pathinfo(parse_url($ruta, PHP_URL_PATH) ?: $ruta, PATHINFO_EXTENSION));
    return in_array($extension, ['jpg', 'jpeg', 'png', 'webp', 'gif', 'svg', 'avif'], true);
}

function tiendaResolverRutaImagen(?string $ruta): string
{
    $ruta = trim((string) $ruta);

    if (!tiendaEsImagenValida($ruta)) {
        return tiendaObtenerImagenPorDefecto();
    }

    if (filter_var($ruta, FILTER_VALIDATE_URL)) {
        return $ruta;
    }

    $ruta = ltrim($ruta, '/');
    $candidatos = [
        $ruta,
        'proyecto-productos/proyecto-productos/' . $ruta,
        'proyecto-productos/' . $ruta,
        'proyecto-productos/proyecto-productos/uploads/' . basename($ruta),
        'proyecto-productos/proyecto-productos/uploads/articulos/' . basename($ruta),
        'proyecto-productos/proyecto-productos/imgproductos/' . basename($ruta),
    ];

    foreach ($candidatos as $candidato) {
        $candidato = preg_replace('#/+#', '/', $candidato) ?? $candidato;
        $rutaFisica = dirname(__DIR__) . '/' . $candidato;

        if (is_file($rutaFisica)) {
            return '/' . ltrim($candidato, '/');
        }
    }

    return tiendaObtenerImagenPorDefecto();
}

function obtenerImagenProducto(array $producto): string
{
    $imagenes = [
        $producto['imagen1'] ?? '',
        $producto['imagen2'] ?? '',
        $producto['imagen3'] ?? '',
        $producto['imagen_simbolo'] ?? '',
    ];

    foreach ($imagenes as $imagen) {
        $url = tiendaResolverRutaImagen((string) $imagen);
        if ($url !== tiendaObtenerImagenPorDefecto()) {
            return $url;
        }
    }

    return tiendaObtenerImagenPorDefecto();
}

function generarEnlaceProducto(array $producto): string
{
    $codigoProveedor = limpiarTexto($producto['codigo_proveedor'] ?? '', 255);
    $slug = limpiarTexto($producto['slug'] ?? '', TIENDA_SLUG_MAX + 10);
    $codigoInterno = limpiarTexto($producto['codigo_interno'] ?? '', 120);

    if ($codigoProveedor !== '' && $slug !== '') {
        return tiendaUrlAbsoluta('producto/' . rawurlencode($codigoProveedor . '-' . $slug));
    }

    return 'articulo.php?ci=' . rawurlencode($codigoInterno);
}

function tiendaNormalizarEntero(mixed $valor, int $minimo = 0, int $defecto = 0, int $maximo = 1000000): int
{
    if (filter_var($valor, FILTER_VALIDATE_INT) === false) {
        return $defecto;
    }

    $valor = (int) $valor;

    if ($valor < $minimo || $valor > $maximo) {
        return $defecto;
    }

    return $valor;
}

function tiendaNormalizarTextoArray(mixed $valores, int $maximo = 180): array
{
    if (!is_array($valores)) {
        $valores = [$valores];
    }

    $salida = [];

    foreach ($valores as $valor) {
        $valor = limpiarTexto($valor, $maximo);
        if ($valor === '') {
            continue;
        }
        $salida[$valor] = $valor;
    }

    return array_values($salida);
}

function tiendaEscapeLike(string $texto): string
{
    return strtr($texto, [
        '\\' => '\\\\',
        '%' => '\%',
        '_' => '\_',
    ]);
}

function tiendaBindParams(mysqli_stmt $stmt, array $params): void
{
    if ($params === []) {
        return;
    }

    $tipos = '';
    $referencias = [];

    foreach ($params as $indice => $valor) {
        if (is_int($valor)) {
            $tipos .= 'i';
        } elseif (is_float($valor)) {
            $tipos .= 'd';
        } else {
            $tipos .= 's';
        }

        $params[$indice] = $valor;
        $referencias[] = &$params[$indice];
    }

    array_unshift($referencias, $tipos);
    $stmt->bind_param(...$referencias);
}

function tiendaFetchAll(mysqli $mysqli, string $sql, array $params = []): array
{
    $stmt = $mysqli->prepare($sql);
    tiendaBindParams($stmt, $params);
    $stmt->execute();
    $resultado = $stmt->get_result();
    $filas = $resultado instanceof mysqli_result ? $resultado->fetch_all(MYSQLI_ASSOC) : [];

    if ($resultado instanceof mysqli_result) {
        $resultado->free();
    }

    $stmt->close();
    return $filas;
}

function tiendaFetchOne(mysqli $mysqli, string $sql, array $params = []): ?array
{
    $filas = tiendaFetchAll($mysqli, $sql, $params);
    return $filas[0] ?? null;
}

function tiendaTablaTieneColumna(mysqli $mysqli, string $tabla, string $columna): bool
{
    static $cache = [];

    $clave = $tabla . '.' . $columna;

    if (array_key_exists($clave, $cache)) {
        return $cache[$clave];
    }

    $sql = "
        SELECT COUNT(*) AS total
        FROM information_schema.COLUMNS
        WHERE TABLE_SCHEMA = DATABASE()
          AND TABLE_NAME = ?
          AND COLUMN_NAME = ?
        LIMIT 1
    ";

    $fila = tiendaFetchOne($mysqli, $sql, [$tabla, $columna]);
    $cache[$clave] = ((int) ($fila['total'] ?? 0)) > 0;

    return $cache[$clave];
}

function tiendaObtenerJoinPrecios(): string
{
    return "
        LEFT JOIN (
            SELECT
                pi1.codigo_interno,
                pi1.precio_web,
                pi1.precio_normal1,
                pi1.moneda_precio_web,
                pi1.stock,
                pi1.disponibilidad,
                pi1.en_inventario,
                pi1.estatus_activo
            FROM pro_precios_inventario pi1
            INNER JOIN (
                SELECT codigo_interno, MAX(id) AS max_id
                FROM pro_precios_inventario
                WHERE IFNULL(estatus_eliminado, 0) = 0
                  AND COALESCE(estatus_activo, 'si') = 'si'
                GROUP BY codigo_interno
            ) ult ON ult.max_id = pi1.id
        ) pi ON pi.codigo_interno = a.codigo_interno
    ";
}

function tiendaObtenerMapaJerarquia(): array
{
    return [
        'categoria' => [
            'field' => 'a.ID_categoria',
            'join' => "LEFT JOIN pro1_categoria c ON c.ID_categoria = a.ID_categoria AND c.estatus_eliminado = 0",
            'label' => "COALESCE(NULLIF(TRIM(c.categoria), ''), CONCAT('Categoría ', a.ID_categoria))",
            'id_alias' => 'id',
            'name_alias' => 'nombre',
        ],
        'catalogo' => [
            'field' => 'a.ID_catalogo',
            'join' => "LEFT JOIN pro2_catalogo ca ON ca.ID_catalogo = a.ID_catalogo AND ca.estatus_eliminado = 0",
            'label' => "COALESCE(NULLIF(TRIM(ca.catalogo), ''), CONCAT('Catálogo ', a.ID_catalogo))",
            'id_alias' => 'id',
            'name_alias' => 'nombre',
        ],
        'familia' => [
            'field' => 'a.ID_familia',
            'join' => "LEFT JOIN pro3_familia f ON f.ID_familia = a.ID_familia AND f.estatus_eliminado = 0",
            'label' => "COALESCE(NULLIF(TRIM(f.nombre_familia), ''), NULLIF(TRIM(f.familia), ''), CONCAT('Familia ', a.ID_familia))",
            'id_alias' => 'id',
            'name_alias' => 'nombre',
        ],
        'linea' => [
            'field' => 'a.ID_linea',
            'join' => "LEFT JOIN pro4_linea l ON l.linea = a.ID_linea AND l.estatus_eliminado = 0",
            'label' => "COALESCE(NULLIF(TRIM(l.nombre_linea), ''), NULLIF(TRIM(l.linea), ''), CONCAT('Línea ', a.ID_linea))",
            'id_alias' => 'id',
            'name_alias' => 'nombre',
        ],
        'serie' => [
            'field' => 'a.ID_serie',
            'join' => "LEFT JOIN pro5_series s ON s.id_serie = a.ID_serie AND s.estatus_eliminado = 0",
            'label' => "COALESCE(NULLIF(TRIM(s.nombre_serie), ''), NULLIF(TRIM(s.serie), ''), CONCAT('Serie ', a.ID_serie))",
            'id_alias' => 'id',
            'name_alias' => 'nombre',
        ],
        'grupo' => [
            'field' => 'a.ID_grupo',
            'join' => "LEFT JOIN pro6_grupos g ON g.id_grupo = a.ID_grupo AND g.estatus_eliminado = 0",
            'label' => "COALESCE(NULLIF(TRIM(g.nombre_grupo), ''), NULLIF(TRIM(g.grupo), ''), CONCAT('Grupo ', a.ID_grupo))",
            'id_alias' => 'id',
            'name_alias' => 'nombre',
        ],
        'marca' => [
            'field' => 'a.ID_marca',
            'join' => "LEFT JOIN pro7_marca m ON m.ID_marca = a.ID_marca AND m.estatus_eliminado = 0",
            'label' => "COALESCE(NULLIF(TRIM(m.marca), ''), CONCAT('Marca ', a.ID_marca))",
            'id_alias' => 'id',
            'name_alias' => 'nombre',
        ],
    ];
}

function tiendaNormalizarFiltrosDinamicos(mixed $tokens): array
{
    if (!is_array($tokens)) {
        $tokens = [$tokens];
    }

    $resultado = [];

    foreach ($tokens as $token) {
        $token = trim((string) $token);
        if ($token === '') {
            continue;
        }

        $decodificado = base64_decode($token, true);
        if ($decodificado === false || !str_contains($decodificado, '||')) {
            continue;
        }

        [$label, $value] = explode('||', $decodificado, 2);
        $label = limpiarTexto($label, 120);
        $value = limpiarTexto($value, 160);

        if ($label === '' || $value === '') {
            continue;
        }

        $clave = mb_strtolower($label . '||' . $value, 'UTF-8');

        $resultado[$clave] = [
            'token' => base64_encode($label . '||' . $value),
            'label' => $label,
            'value' => $value,
        ];
    }

    return array_values($resultado);
}

function tiendaConstruirTokenEtiqueta(string $label, string $value): string
{
    return base64_encode($label . '||' . $value);
}

function tiendaObtenerFiltrosNormalizados(array $request): array
{
    $orden = limpiarTexto($request['orden'] ?? 'tienda', 30);
    $ordenesPermitidos = [
        'tienda',
        'random',
        'a_z',
        'z_a',
        'precio',
        'min_max',
        'max_min',
    ];

    if (!in_array($orden, $ordenesPermitidos, true)) {
        $orden = 'tienda';
    }

    return [
        'q' => limpiarTexto($request['q'] ?? '', 120),
        'pagina' => tiendaNormalizarEntero($request['pagina'] ?? $request['page'] ?? 1, 1, 1, 100000),
        'por_pagina' => tiendaNormalizarEntero($request['por_pagina'] ?? TIENDA_POR_PAGINA, 1, TIENDA_POR_PAGINA, TIENDA_POR_PAGINA_MAX),
        'orden' => $orden,
        'categoria' => tiendaNormalizarEntero($request['categoria'] ?? 0),
        'catalogo' => tiendaNormalizarEntero($request['catalogo'] ?? 0),
        'familia' => tiendaNormalizarEntero($request['familia'] ?? 0),
        'linea' => tiendaNormalizarEntero($request['linea'] ?? 0),
        'serie' => tiendaNormalizarEntero($request['serie'] ?? 0),
        'grupo' => tiendaNormalizarEntero($request['grupo'] ?? 0),
        'marca' => tiendaNormalizarEntero($request['marca'] ?? 0),
        'etiquetas' => tiendaNormalizarFiltrosDinamicos($request['etiqueta'] ?? []),
    ];
}

function tiendaConstruirWhereCatalogo(array $filtros, array &$params, array $dimensionesExcluir = []): string
{
    $where = [
        'a.estatus_eliminado = 0',
        'a.activo = 1',
    ];

    $mapa = tiendaObtenerMapaJerarquia();

    if ($filtros['q'] !== '') {
        $like = '%' . tiendaEscapeLike($filtros['q']) . '%';
        $where[] = '('
            . 'a.codigo_interno LIKE ?'
            . ' OR a.codigo_proveedor LIKE ?'
            . ' OR a.modelo LIKE ?'
            . ' OR a.descripcion_corta LIKE ?'
            . ' OR a.descripcion_larga LIKE ?'
            . ' OR a.descripcion_muy_larga LIKE ?'
            . ')';
        $params[] = $like;
        $params[] = $like;
        $params[] = $like;
        $params[] = $like;
        $params[] = $like;
        $params[] = $like;
    }

    foreach ($mapa as $dimension => $config) {
        if (in_array($dimension, $dimensionesExcluir, true)) {
            continue;
        }

        $valor = (int) ($filtros[$dimension] ?? 0);
        if ($valor > 0) {
            $where[] = $config['field'] . ' = ?';
            $params[] = $valor;
        }
    }

    foreach ($filtros['etiquetas'] as $par) {
        $where[] = 'LOWER(COALESCE(a.etiquetas_datos, \'\')) LIKE ?';
        $where[] = 'LOWER(COALESCE(a.valores_datos, \'\')) LIKE ?';
        $params[] = '%' . tiendaEscapeLike(mb_strtolower($par['label'], 'UTF-8')) . '%';
        $params[] = '%' . tiendaEscapeLike(mb_strtolower($par['value'], 'UTF-8')) . '%';
    }

    return implode(' AND ', $where);
}

function tiendaFormatearMoneda(mixed $valor, string $moneda = 'COP'): string
{
    if ($valor === null || $valor === '' || !is_numeric((string) $valor)) {
        return '';
    }

    $moneda = limpiarTexto($moneda, 10);
    if ($moneda === '') {
        $moneda = 'COP';
    }

    return $moneda . ' ' . number_format((float) $valor, 0, ',', '.');
}

function tiendaFormatearStock(array $producto): string
{
    $disponibilidad = limpiarTexto($producto['disponibilidad'] ?? '', 80);
    $stock = $producto['stock'] ?? null;

    if ($stock !== null && $stock !== '' && is_numeric((string) $stock)) {
        return ((int) $stock) . ' disponibles';
    }

    if ($disponibilidad !== '') {
        return $disponibilidad;
    }

    return 'Consulta disponibilidad';
}

function tiendaNormalizarProductoFila(array $fila): array
{
    $titulo = limpiarTexto($fila['descripcion_corta'] ?? '', 0);
    if ($titulo === '') {
        $titulo = limpiarTexto($fila['modelo'] ?? '', 0);
    }
    if ($titulo === '') {
        $titulo = limpiarTexto($fila['codigo_interno'] ?? 'Producto', 0);
    }

    $fila['titulo'] = $titulo;
    $fila['resumen'] = truncarTexto((string) ($fila['descripcion_larga'] ?: $fila['descripcion_corta'] ?: $fila['descripcion_muy_larga'] ?: ''), 110);
    $fila['imagen_url'] = obtenerImagenProducto($fila);
    $fila['codigo_interno_label'] = limpiarTexto($fila['codigo_interno'] ?? '', 100);
    $fila['codigo_proveedor_label'] = limpiarTexto($fila['codigo_proveedor'] ?? '', 150);
    $fila['modelo_label'] = limpiarTexto($fila['modelo'] ?? '', 120);
    $fila['marca_label'] = limpiarTexto($fila['marca_nombre'] ?? '', 120);
    $fila['categoria_label'] = limpiarTexto($fila['categoria_nombre'] ?? '', 120);
    $fila['familia_label'] = limpiarTexto($fila['familia_nombre'] ?? '', 120);
    $fila['grupo_label'] = limpiarTexto($fila['grupo_nombre'] ?? '', 120);
    $fila['precio_formateado'] = tiendaFormatearMoneda($fila['precio_web'] ?? null, (string) ($fila['moneda_precio_web'] ?? 'COP'));
    $fila['stock_formateado'] = tiendaFormatearStock($fila);
    $fila['especificaciones'] = array_slice(
        formatearEspecificaciones((string) ($fila['etiquetas_datos'] ?? ''), (string) ($fila['valores_datos'] ?? '')),
        0,
        2
    );
    $fila['enlace'] = generarEnlaceProducto($fila);
    $fila['requiere_login_url'] = tiendaConstruirUrlLoginCliente($fila['enlace']);

    return $fila;
}

function tiendaObtenerOrderBy(string $orden): string
{
    $ordenes = [
        'tienda' => "CASE WHEN NULLIF(TRIM(a.item1), '') IS NULL THEN 1 ELSE 0 END ASC, CAST(COALESCE(NULLIF(TRIM(a.item1), ''), '0') AS UNSIGNED) ASC, COALESCE(NULLIF(TRIM(a.descripcion_corta), ''), NULLIF(TRIM(a.modelo), ''), NULLIF(TRIM(a.codigo_proveedor), ''), a.codigo_interno) ASC, a.id DESC",
        'random' => 'RAND()',
        'a_z' => 'COALESCE(NULLIF(TRIM(a.descripcion_corta), \'\'), NULLIF(TRIM(a.modelo), \'\'), NULLIF(TRIM(a.codigo_proveedor), \'\'), a.codigo_interno) ASC, a.id DESC',
        'z_a' => 'COALESCE(NULLIF(TRIM(a.descripcion_corta), \'\'), NULLIF(TRIM(a.modelo), \'\'), NULLIF(TRIM(a.codigo_proveedor), \'\'), a.codigo_interno) DESC, a.id DESC',
        'precio' => 'CASE WHEN pi.precio_web IS NULL OR pi.precio_web = 0 THEN 1 ELSE 0 END, COALESCE(NULLIF(TRIM(a.descripcion_corta), \'\'), NULLIF(TRIM(a.modelo), \'\'), NULLIF(TRIM(a.codigo_proveedor), \'\'), a.codigo_interno) ASC, a.id DESC',
        'min_max' => 'CASE WHEN pi.precio_web IS NULL OR pi.precio_web = 0 THEN 1 ELSE 0 END, pi.precio_web ASC, a.id DESC',
        'max_min' => 'CASE WHEN pi.precio_web IS NULL OR pi.precio_web = 0 THEN 1 ELSE 0 END, pi.precio_web DESC, a.id DESC',
    ];

    return $ordenes[$orden] ?? $ordenes['tienda'];
}

function tiendaRenderizarBadgesJerarquia(array $producto): string
{
    $badges = [];

    foreach (['categoria_nombre', 'familia_nombre', 'grupo_nombre'] as $campo) {
        $valor = limpiarTexto($producto[$campo] ?? '', 60);
        if ($valor !== '') {
            $badges[] = '<span class="product-badge">' . e($valor) . '</span>';
        }
    }

    return implode('', array_slice($badges, 0, 2));
}

function tiendaRenderizarPrecioProducto(array $producto): string
{
    if (!puedeVerPreciosStock()) {
        return '';
    }

    $precio = $producto['precio_formateado'] ?? '';
    $stock = $producto['stock_formateado'] ?? '';

    if ($precio === '') {
        $precio = 'Precio bajo consulta';
    }

    return '
        <div class="product-price-box">
            <strong class="product-price">' . e($precio) . '</strong>
            <span class="product-stock">' . e($stock) . '</span>
        </div>
    ';
}

function tiendaRenderizarCardProducto(array $producto): string
{
    $producto = tiendaNormalizarProductoFila($producto);
    $badges = tiendaRenderizarBadgesJerarquia($producto);
    $precioHtml = tiendaRenderizarPrecioProducto($producto);
    $marca = $producto['marca_label'] ?? '';
    $modelo = $producto['modelo_label'] ?? '';
    $codigoProveedor = $producto['codigo_proveedor_label'] ?? '';
    $codigoInterno = $producto['codigo_interno_label'] ?? '';

    $identityHtml = '';

    if ($codigoProveedor !== '' || $codigoInterno !== '') {
        $identityHtml .= '<div class="product-identity-row">';

        if ($codigoProveedor !== '') {
            $identityHtml .= '
                <div class="product-identity-box">
                    <span>Código proveedor</span>
                    <strong>' . e($codigoProveedor) . '</strong>
                </div>
            ';
        }

        if ($codigoInterno !== '') {
            $identityHtml .= '
                <div class="product-identity-box">
                    <span>Código interno</span>
                    <strong>' . e($codigoInterno) . '</strong>
                </div>
            ';
        }

        $identityHtml .= '</div>';
    }

    return '
        <article class="product-card" aria-label="' . e($producto['titulo']) . '">
            <a class="product-card-media" href="' . e($producto['enlace']) . '" aria-label="Ver ' . e($producto['titulo']) . '">
                <img src="' . e($producto['imagen_url']) . '" alt="' . e($producto['titulo']) . '" loading="lazy">
            </a>
            <div class="product-card-body">
                <div class="product-badge-row">' . $badges . '</div>
                <h3 class="product-title"><a href="' . e($producto['enlace']) . '">' . e($producto['titulo']) . '</a></h3>
                ' . $identityHtml . '
                <div class="product-meta">
                ' . ($modelo !== '' ? '<span><strong>Modelo:</strong> ' . e($modelo) . '</span>' : '') . '
                ' . ($marca !== '' ? '<span><strong>Marca:</strong> ' . e($marca) . '</span>' : '') . '
                </div>
                ' . $precioHtml . '
                <div class="product-actions">
                    <a class="btn btn-product-primary" href="' . e($producto['enlace']) . '">
                        <span class="product-action-copy">
                            <strong>Ver producto</strong>
                            <small>Ficha y detalle</small>
                        </span>
                        <span class="product-action-icon" aria-hidden="true">
                            <i class="fa-solid fa-arrow-right"></i>
                        </span>
                    </a>
                    <a class="btn btn-product-secondary" href="' . e($producto['requiere_login_url']) . '">
                        <span class="product-action-copy">
                            <strong>Solicitar cotización</strong>
                            <small>Atención comercial</small>
                        </span>
                        <span class="product-action-icon" aria-hidden="true">
                            <i class="fa-regular fa-file-lines"></i>
                        </span>
                    </a>
                </div>
            </div>
        </article>
    ';
}

function tiendaRenderizarGridProductos(array $productos): string
{
    if ($productos === []) {
        return '
            <div class="empty-state">
                <div class="empty-state-icon">0</div>
                <h3>No encontramos productos con esos filtros</h3>
                <p>Ajusta la búsqueda, cambia la jerarquía o elimina filtros dinámicos para ampliar el catálogo visible.</p>
            </div>
        ';
    }

    $html = '<div class="products-grid">';

    foreach ($productos as $producto) {
        $html .= tiendaRenderizarCardProducto($producto);
    }

    $html .= '</div>';
    return $html;
}

function tiendaConstruirQueryCatalogo(array $filtros, array $dimensionesExcluir = []): array
{
    $params = [];
    $whereSql = tiendaConstruirWhereCatalogo($filtros, $params, $dimensionesExcluir);

    return [
        'where_sql' => $whereSql,
        'params' => $params,
    ];
}

function tiendaObtenerFacetaJerarquia(mysqli $mysqli, string $dimension, array $filtros): array
{
    $mapa = tiendaObtenerMapaJerarquia();

    if (!isset($mapa[$dimension])) {
        return [];
    }

    $config = $mapa[$dimension];
    $whereData = tiendaConstruirQueryCatalogo($filtros, [$dimension]);

    $sql = "
        SELECT
            {$config['field']} AS id,
            {$config['label']} AS nombre,
            COUNT(*) AS total
        FROM pro_articulos a
        {$config['join']}
        WHERE {$whereData['where_sql']}
          AND {$config['field']} IS NOT NULL
          AND {$config['field']} <> 0
        GROUP BY {$config['field']}, nombre
        ORDER BY nombre ASC
        LIMIT 200
    ";

    $filas = tiendaFetchAll($mysqli, $sql, $whereData['params']);

    foreach ($filas as &$fila) {
        $fila['id'] = (int) ($fila['id'] ?? 0);
        $fila['nombre'] = limpiarTexto($fila['nombre'] ?? '', 120);
        $fila['total'] = (int) ($fila['total'] ?? 0);
        $fila['active'] = ((int) ($filtros[$dimension] ?? 0) === $fila['id']);
    }
    unset($fila);

    return $filas;
}

function tiendaObtenerOrdenEtiquetasGrupo(mysqli $mysqli, int $grupoId): array
{
    if ($grupoId <= 0) {
        return [];
    }

    $columnas = [];

    for ($i = 1; $i <= 30; $i++) {
        $columnas[] = 'e.etiqueta' . $i;
    }

    $sql = "
        SELECT " . implode(', ', $columnas) . "
        FROM pro6_grupos g
        INNER JOIN pro8_etiquetas e
            ON e.ID = g.id_etiquetas
        WHERE g.id_grupo = ?
          AND g.estatus_eliminado = 0
          AND IFNULL(e.estatus_eliminado, 0) = 0
        LIMIT 1
    ";

    $fila = tiendaFetchOne($mysqli, $sql, [$grupoId]);

    if ($fila === null) {
        return [];
    }

    $orden = [];
    $posicion = 0;

    for ($i = 1; $i <= 30; $i++) {
        $label = limpiarTexto($fila['etiqueta' . $i] ?? '', 90);

        if ($label === '') {
            continue;
        }

        $clave = mb_strtolower($label, 'UTF-8');

        if (!isset($orden[$clave])) {
            $orden[$clave] = $posicion;
            $posicion++;
        }
    }

    return $orden;
}

function tiendaObtenerFacetasEtiquetas(mysqli $mysqli, array $filtros): array
{
    $whereData = tiendaConstruirQueryCatalogo($filtros, []);

    $sql = "
        SELECT a.etiquetas_datos, a.valores_datos
        FROM pro_articulos a
        WHERE {$whereData['where_sql']}
          AND COALESCE(a.etiquetas_datos, '') <> ''
        LIMIT 600
    ";

    $filas = tiendaFetchAll($mysqli, $sql, $whereData['params']);
    $grupos = [];
    $activos = [];

    foreach ($filtros['etiquetas'] as $activo) {
        $activos[$activo['token']] = true;
    }

    foreach ($filas as $fila) {
        $pares = formatearEspecificaciones((string) ($fila['etiquetas_datos'] ?? ''), (string) ($fila['valores_datos'] ?? ''));

        foreach ($pares as $par) {
            $label = limpiarTexto($par['label'], 90);
            $value = limpiarTexto($par['value'], 120);

            if ($label === '' || $value === '') {
                continue;
            }

            $labelKey = mb_strtolower($label, 'UTF-8');
            $valueKey = mb_strtolower($value, 'UTF-8');
            $token = tiendaConstruirTokenEtiqueta($label, $value);

            if (!isset($grupos[$labelKey])) {
                $grupos[$labelKey] = [
                    'label' => $label,
                    'options' => [],
                ];
            }

            if (!isset($grupos[$labelKey]['options'][$valueKey])) {
                $grupos[$labelKey]['options'][$valueKey] = [
                    'token' => $token,
                    'value' => $value,
                    'total' => 0,
                    'active' => isset($activos[$token]),
                ];
            }

            $grupos[$labelKey]['options'][$valueKey]['total']++;
        }
    }

    $ordenGrupoPlantilla = tiendaObtenerOrdenEtiquetasGrupo($mysqli, (int) ($filtros['grupo'] ?? 0));

    if ($ordenGrupoPlantilla !== []) {
        uksort(
            $grupos,
            static function (string $a, string $b) use ($ordenGrupoPlantilla): int {
                $posA = $ordenGrupoPlantilla[$a] ?? 9999;
                $posB = $ordenGrupoPlantilla[$b] ?? 9999;

                if ($posA === $posB) {
                    return 0;
                }

                return $posA <=> $posB;
            }
        );
    }

    $grupos = array_slice($grupos, 0, TIENDA_ETIQUETAS_MAX_GRUPOS, true);

    $salida = [];

    foreach ($grupos as $grupo) {
        uasort(
            $grupo['options'],
            static fn(array $a, array $b): int => $b['total'] <=> $a['total'] ?: strcasecmp($a['value'], $b['value'])
        );

        $grupo['total_options'] = count($grupo['options']);
        $grupo['options'] = array_slice(array_values($grupo['options']), 0, TIENDA_ETIQUETAS_MAX_OPCIONES);
        $salida[] = $grupo;
    }

    return $salida;
}

function tiendaRenderizarPaginacion(array $catalogo): string
{
    if (($catalogo['total_paginas'] ?? 1) <= 1) {
        return '';
    }

    $pagina = (int) $catalogo['pagina'];
    $totalPaginas = (int) $catalogo['total_paginas'];
    $paginasVisibles = [];

    for ($i = 1; $i <= min(4, $totalPaginas); $i++) {
        $paginasVisibles[$i] = true;
    }

    for ($i = max(1, $pagina - 1); $i <= min($totalPaginas, $pagina + 1); $i++) {
        $paginasVisibles[$i] = true;
    }

    for ($i = max(1, $totalPaginas - 2); $i <= $totalPaginas; $i++) {
        $paginasVisibles[$i] = true;
    }

    $paginas = array_keys($paginasVisibles);
    sort($paginas);

    $html = '<nav class="catalog-pagination" aria-label="Paginación del catálogo"><div class="pagination-list">';

    $html .= '<button type="button" class="pagination-item" data-page="' . max(1, $pagina - 1) . '" ' . ($pagina === 1 ? 'disabled' : '') . '>Anterior</button>';

    $anterior = 0;

    foreach ($paginas as $numeroPagina) {
        if ($anterior > 0 && ($numeroPagina - $anterior) > 1) {
            $html .= '<span class="pagination-ellipsis" aria-hidden="true">...</span>';
        }

        $html .= '<button type="button" class="pagination-item ' . ($numeroPagina === $pagina ? 'is-active' : '') . '" data-page="' . $numeroPagina . '">' . $numeroPagina . '</button>';
        $anterior = $numeroPagina;
    }

    $html .= '<button type="button" class="pagination-item" data-page="' . min($totalPaginas, $pagina + 1) . '" ' . ($pagina === $totalPaginas ? 'disabled' : '') . '>Siguiente</button>';
    $html .= '</div></nav>';

    return $html;
}

function tiendaRenderizarFiltrosActivos(array $filtros, array $facetas): string
{
    $chips = [];
    $mapaNombres = [
        'categoria' => 'Categoría',
        'catalogo' => 'Catálogo',
        'familia' => 'Familia',
        'linea' => 'Línea',
        'serie' => 'Serie',
        'grupo' => 'Grupo',
        'marca' => 'Marca',
    ];

    foreach ($mapaNombres as $clave => $label) {
        $valor = (int) ($filtros[$clave] ?? 0);
        if ($valor <= 0 || !isset($facetas[$clave])) {
            continue;
        }

        foreach ($facetas[$clave] as $opcion) {
            if ((int) $opcion['id'] === $valor) {
                $chips[] = '<span class="active-chip">' . e($label . ': ' . $opcion['nombre']) . '</span>';
                break;
            }
        }
    }

    foreach ($filtros['etiquetas'] as $par) {
        $chips[] = '<span class="active-chip">' . e($par['label'] . ': ' . $par['value']) . '</span>';
    }

    if ($filtros['q'] !== '') {
        $chips[] = '<span class="active-chip">Búsqueda: ' . e($filtros['q']) . '</span>';
    }

    if ($chips === []) {
        return '';
    }

    return '<div class="active-chip-row">' . implode('', $chips) . '</div>';
}

function tiendaRenderizarResultadosCatalogo(array $catalogo): string
{
    $orden = (string) ($catalogo['filtros']['orden'] ?? 'tienda');
    $resumen = '
        <div class="catalog-results-head">
            <div class="catalog-results-summary">
                <strong>' . number_format((int) $catalogo['total'], 0, ',', '.') . '</strong>
                <span> productos encontrados</span>
            </div>
            <div class="catalog-results-tools">
                <label class="catalog-order-inline" aria-label="Ordenar productos">
                    <span>Ordenar</span>
                    <select data-order-toolbar>
                        <option value="tienda" ' . ($orden === 'tienda' ? 'selected' : '') . '>Tienda</option>
                        <option value="random" ' . ($orden === 'random' ? 'selected' : '') . '>Aleatorio</option>
                        <option value="a_z" ' . ($orden === 'a_z' ? 'selected' : '') . '>A - Z</option>
                        <option value="z_a" ' . ($orden === 'z_a' ? 'selected' : '') . '>Z - A</option>
                        <option value="precio" ' . ($orden === 'precio' ? 'selected' : '') . '>Precio</option>
                        <option value="min_max" ' . ($orden === 'min_max' ? 'selected' : '') . '>Min - Max</option>
                        <option value="max_min" ' . ($orden === 'max_min' ? 'selected' : '') . '>Max - Min</option>
                    </select>
                </label>
                <div class="catalog-range">Mostrando ' . (int) $catalogo['desde'] . ' - ' . (int) $catalogo['hasta'] . '</div>
            </div>
        </div>
    ';

    $chips = tiendaRenderizarFiltrosActivos($catalogo['filtros'], $catalogo['facetas']);
    $grid = tiendaRenderizarGridProductos($catalogo['productos']);
    $paginacion = tiendaRenderizarPaginacion($catalogo);

    return $resumen . $chips . $grid . $paginacion;
}

function tiendaRenderizarSelectJerarquia(string $name, string $label, array $options, int $selected): string
{
    $html = '
        <label class="filter-field">
            <span>' . e($label) . '</span>
            <select name="' . e($name) . '">
                <option value="">Todas</option>
    ';

    foreach ($options as $option) {
        $html .= '
            <option value="' . (int) $option['id'] . '" ' . ((int) $option['id'] === $selected ? 'selected' : '') . '>
                ' . e($option['nombre']) . ' (' . number_format((int) $option['total'], 0, ',', '.') . ')
            </option>
        ';
    }

    $html .= '
            </select>
        </label>
    ';

    return $html;
}

function tiendaRenderizarFiltrosSidebar(array $catalogo): string
{
    $filtros = $catalogo['filtros'];
    $facetas = $catalogo['facetas'];

    $html = '
        <form id="catalogoFiltrosForm" class="filters-form" autocomplete="off">
            <div class="filters-block">
                <div class="filters-block-head">
                    <h3>Filtros del catálogo</h3>
                    <button type="button" class="filters-reset" data-reset-filters>Limpiar</button>
                </div>
                <label class="filter-field">
                    <span>Buscar producto</span>
                    <input type="search" name="q" value="' . e($filtros['q']) . '" placeholder="Código, modelo, descripción...">
                </label>
                <label class="filter-field">
                    <span>Ordenar por</span>
                    <select name="orden">
                        <option value="tienda" ' . ($filtros['orden'] === 'tienda' ? 'selected' : '') . '>Tienda</option>
                        <option value="random" ' . ($filtros['orden'] === 'random' ? 'selected' : '') . '>Aleatorio</option>
                        <option value="a_z" ' . ($filtros['orden'] === 'a_z' ? 'selected' : '') . '>A - Z</option>
                        <option value="z_a" ' . ($filtros['orden'] === 'z_a' ? 'selected' : '') . '>Z - A</option>
                        <option value="precio" ' . ($filtros['orden'] === 'precio' ? 'selected' : '') . '>Precio</option>
                        <option value="min_max" ' . ($filtros['orden'] === 'min_max' ? 'selected' : '') . '>Min - Max</option>
                        <option value="max_min" ' . ($filtros['orden'] === 'max_min' ? 'selected' : '') . '>Max - Min</option>
                    </select>
                </label>
            </div>
    ';

    $html .= tiendaRenderizarSelectJerarquia('categoria', 'Categoría', $facetas['categoria'], (int) $filtros['categoria']);
    $html .= tiendaRenderizarSelectJerarquia('catalogo', 'Catálogo', $facetas['catalogo'], (int) $filtros['catalogo']);
    $html .= tiendaRenderizarSelectJerarquia('familia', 'Familia', $facetas['familia'], (int) $filtros['familia']);
    $html .= tiendaRenderizarSelectJerarquia('linea', 'Línea', $facetas['linea'], (int) $filtros['linea']);
    $html .= tiendaRenderizarSelectJerarquia('serie', 'Serie', $facetas['serie'], (int) $filtros['serie']);
    $html .= tiendaRenderizarSelectJerarquia('grupo', 'Grupo', $facetas['grupo'], (int) $filtros['grupo']);
    $html .= tiendaRenderizarSelectJerarquia('marca', 'Marca', $facetas['marca'], (int) $filtros['marca']);

    if ($facetas['etiquetas'] !== []) {
        $html .= '<section class="filters-block"><h3>Especificaciones dinámicas</h3>';

        foreach ($facetas['etiquetas'] as $grupo) {
            $hayActiva = false;

            foreach ($grupo['options'] as $option) {
                if (!empty($option['active'])) {
                    $hayActiva = true;
                    break;
                }
            }

            $totalOpcionesGrupo = (int) ($grupo['total_options'] ?? count($grupo['options']));

            $html .= '
                <details class="filter-tag-group" ' . ($hayActiva ? 'open' : '') . '>
                    <summary>
                        <span>' . e($grupo['label']) . '</span>
                    </summary>
                    <div class="filter-tag-options">
            ';

            foreach ($grupo['options'] as $option) {
                $html .= '
                    <label class="filter-check">
                        <input type="checkbox" name="etiqueta[]" value="' . e($option['token']) . '" ' . ($option['active'] ? 'checked' : '') . '>
                        <span>' . e($option['value']) . ' <small>(' . (int) $option['total'] . ')</small></span>
                    </label>
                ';
            }

            $html .= '</div></details>';
        }

        $html .= '</section>';
    }

    $html .= '
            <div class="filters-actions">
                <button type="submit" class="btn btn-filter-submit">Aplicar filtros</button>
            </div>
        </form>
    ';

    return $html;
}

function tiendaConsultarCatalogo(mysqli $mysqli, array $request): array
{
    $filtros = tiendaObtenerFiltrosNormalizados($request);
    $whereData = tiendaConstruirQueryCatalogo($filtros);
    $orderBy = tiendaObtenerOrderBy($filtros['orden']);
    $slugSelect = tiendaTablaTieneColumna($mysqli, 'pro_articulos', 'slug')
        ? 'a.slug'
        : 'NULL AS slug';

    $conteoSql = "
        SELECT COUNT(*) AS total
        FROM pro_articulos a
        WHERE {$whereData['where_sql']}
    ";

    $conteo = tiendaFetchOne($mysqli, $conteoSql, $whereData['params']);
    $total = (int) ($conteo['total'] ?? 0);
    $porPagina = $filtros['por_pagina'];
    $totalPaginas = max(1, (int) ceil($total / $porPagina));
    $pagina = min($filtros['pagina'], $totalPaginas);
    $offset = ($pagina - 1) * $porPagina;

    $sql = "
        SELECT
            a.id,
            a.codigo_interno,
            a.codigo_proveedor,
            a.codigo_aplicacion,
            {$slugSelect},
            a.modelo,
            a.modelo2,
            a.descripcion_corta,
            a.descripcion_larga,
            a.descripcion_muy_larga,
            a.imagen1,
            a.imagen2,
            a.imagen3,
            a.imagen_simbolo,
            a.link_modelo,
            a.link_modelo_pdf,
            a.etiquetas_datos,
            a.valores_datos,
            a.ID_categoria,
            a.ID_catalogo,
            a.ID_familia,
            a.ID_linea,
            a.ID_serie,
            a.ID_grupo,
            a.ID_marca,
            COALESCE(NULLIF(TRIM(c.categoria), ''), '') AS categoria_nombre,
            COALESCE(NULLIF(TRIM(ca.catalogo), ''), '') AS catalogo_nombre,
            COALESCE(NULLIF(TRIM(f.nombre_familia), ''), NULLIF(TRIM(f.familia), ''), '') AS familia_nombre,
            COALESCE(NULLIF(TRIM(l.nombre_linea), ''), NULLIF(TRIM(l.linea), ''), '') AS linea_nombre,
            COALESCE(NULLIF(TRIM(s.nombre_serie), ''), NULLIF(TRIM(s.serie), ''), '') AS serie_nombre,
            COALESCE(NULLIF(TRIM(g.nombre_grupo), ''), NULLIF(TRIM(g.grupo), ''), '') AS grupo_nombre,
            COALESCE(NULLIF(TRIM(m.marca), ''), '') AS marca_nombre,
            pi.precio_web,
            pi.precio_normal1,
            pi.moneda_precio_web,
            pi.stock,
            pi.disponibilidad,
            pi.en_inventario
        FROM pro_articulos a
        LEFT JOIN pro1_categoria c ON c.ID_categoria = a.ID_categoria AND c.estatus_eliminado = 0
        LEFT JOIN pro2_catalogo ca ON ca.ID_catalogo = a.ID_catalogo AND ca.estatus_eliminado = 0
        LEFT JOIN pro3_familia f ON f.ID_familia = a.ID_familia AND f.estatus_eliminado = 0
        LEFT JOIN pro4_linea l ON l.linea = a.ID_linea AND l.estatus_eliminado = 0
        LEFT JOIN pro5_series s ON s.id_serie = a.ID_serie AND s.estatus_eliminado = 0
        LEFT JOIN pro6_grupos g ON g.id_grupo = a.ID_grupo AND g.estatus_eliminado = 0
        LEFT JOIN pro7_marca m ON m.ID_marca = a.ID_marca AND m.estatus_eliminado = 0
        " . tiendaObtenerJoinPrecios() . "
        WHERE {$whereData['where_sql']}
        ORDER BY {$orderBy}
        LIMIT ? OFFSET ?
    ";

    $paramsListado = $whereData['params'];
    $paramsListado[] = $porPagina;
    $paramsListado[] = $offset;

    $productos = tiendaFetchAll($mysqli, $sql, $paramsListado);

    foreach ($productos as &$producto) {
        $producto = tiendaNormalizarProductoFila($producto);
    }
    unset($producto);

    $facetas = [
        'categoria' => tiendaObtenerFacetaJerarquia($mysqli, 'categoria', $filtros),
        'catalogo' => tiendaObtenerFacetaJerarquia($mysqli, 'catalogo', $filtros),
        'familia' => tiendaObtenerFacetaJerarquia($mysqli, 'familia', $filtros),
        'linea' => tiendaObtenerFacetaJerarquia($mysqli, 'linea', $filtros),
        'serie' => tiendaObtenerFacetaJerarquia($mysqli, 'serie', $filtros),
        'grupo' => tiendaObtenerFacetaJerarquia($mysqli, 'grupo', $filtros),
        'marca' => tiendaObtenerFacetaJerarquia($mysqli, 'marca', $filtros),
        'etiquetas' => tiendaObtenerFacetasEtiquetas($mysqli, $filtros),
    ];

    $catalogo = [
        'ok' => true,
        'filtros' => $filtros,
        'productos' => $productos,
        'facetas' => $facetas,
        'total' => $total,
        'pagina' => $pagina,
        'por_pagina' => $porPagina,
        'total_paginas' => $totalPaginas,
        'desde' => $total > 0 ? ($offset + 1) : 0,
        'hasta' => min($offset + $porPagina, $total),
    ];

    $catalogo['html'] = tiendaRenderizarResultadosCatalogo($catalogo);
    $catalogo['sidebar_html'] = tiendaRenderizarFiltrosSidebar($catalogo);

    return $catalogo;
}

/**
 * ----------------------------------------------------------------------------
 * BLOQUE: FUNCIONES DE DETALLE PÚBLICO
 * ----------------------------------------------------------------------------
 * Resuelve artículos por código interno o por token limpio
 * codigo_proveedor-slug, arma la ficha enriquecida con jerarquía, galería,
 * precio/stock, recursos y relacionados, y centraliza canonical/SEO sin
 * depender del panel administrativo.
 * ----------------------------------------------------------------------------
 */

function tiendaNormalizarCodigoInternoProducto(mixed $valor): string
{
    $valor = trim((string) $valor);

    if ($valor === '' || !preg_match('/^[A-Za-z0-9_-]+$/', $valor)) {
        return '';
    }

    return $valor;
}

function tiendaNormalizarTokenRutaProducto(mixed $valor): string
{
    $valor = trim((string) $valor);
    $valor = preg_replace('/[^A-Za-z0-9\-]/', '', $valor) ?? '';
    $valor = trim($valor, '-');

    if ($valor === '' || !str_contains($valor, '-')) {
        return '';
    }

    return $valor;
}

function tiendaEsUrlRecursoValida(?string $url): bool
{
    $url = trim((string) $url);

    if ($url === '' || preg_match('#^(javascript:|data:)#i', $url)) {
        return false;
    }

    if (filter_var($url, FILTER_VALIDATE_URL)) {
        $scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME));
        return in_array($scheme, ['http', 'https'], true);
    }

    return false;
}

function tiendaObtenerSelectDetalleProducto(mysqli $mysqli): string
{
    $slugSelect = tiendaTablaTieneColumna($mysqli, 'pro_articulos', 'slug')
        ? 'a.slug'
        : 'NULL AS slug';

    return "
        SELECT
            a.id,
            a.codigo_interno,
            a.codigo_proveedor,
            a.codigo_aplicacion,
            {$slugSelect},
            a.modelo,
            a.modelo2,
            a.codigo_proveedor_v,
            a.descripcion_modelo,
            a.descripcion_corta,
            a.descripcion_larga,
            a.descripcion_muy_larga,
            a.link_modelo,
            a.link_modelo_pdf,
            a.link_cad1,
            a.link_cad2,
            a.item1,
            a.item2,
            a.imagen1,
            a.imagen2,
            a.imagen3,
            a.imagen_simbolo,
            a.normalizado,
            a.norma,
            a.fecha_inclusion,
            a.etiquetas_datos,
            a.valores_datos,
            a.ID_categoria,
            a.ID_catalogo,
            a.ID_familia,
            a.ID_linea,
            a.ID_serie,
            a.ID_grupo,
            a.ID_marca,
            COALESCE(NULLIF(TRIM(c.categoria), ''), '') AS categoria_nombre,
            COALESCE(NULLIF(TRIM(ca.catalogo), ''), '') AS catalogo_nombre,
            COALESCE(NULLIF(TRIM(f.nombre_familia), ''), NULLIF(TRIM(f.familia), ''), '') AS familia_nombre,
            COALESCE(NULLIF(TRIM(l.nombre_linea), ''), NULLIF(TRIM(l.linea), ''), '') AS linea_nombre,
            COALESCE(NULLIF(TRIM(s.nombre_serie), ''), NULLIF(TRIM(s.serie), ''), '') AS serie_nombre,
            COALESCE(NULLIF(TRIM(g.nombre_grupo), ''), NULLIF(TRIM(g.grupo), ''), '') AS grupo_nombre,
            COALESCE(NULLIF(TRIM(m.marca), ''), '') AS marca_nombre,
            c.descripcion_categoria,
            c.link_categoria,
            c.link_categoria_pdf,
            ca.descripcion_catalogo,
            ca.link_catalogo,
            ca.link_catalogo_pdf,
            f.descripcion_familia,
            f.link_familia,
            f.link_familia_pdf,
            l.descripcion_linea,
            l.link_linea,
            l.link_linea_pdf,
            s.descripcion_serie,
            s.link_serie,
            s.pdf_serie,
            g.descripcion_grupo,
            g.link_grupo,
            g.pdf_grupo,
            g.certificados,
            m.descripcion_marca,
            m.link_marca,
            m.link_marca_pdf,
            m.logo_marca,
            pi.precio_web,
            pi.precio_normal1,
            pi.moneda_precio_web,
            pi.stock,
            pi.disponibilidad,
            pi.en_inventario
        FROM pro_articulos a
        LEFT JOIN pro1_categoria c ON c.ID_categoria = a.ID_categoria AND c.estatus_eliminado = 0
        LEFT JOIN pro2_catalogo ca ON ca.ID_catalogo = a.ID_catalogo AND ca.estatus_eliminado = 0
        LEFT JOIN pro3_familia f ON f.ID_familia = a.ID_familia AND f.estatus_eliminado = 0
        LEFT JOIN pro4_linea l ON l.linea = a.ID_linea AND l.estatus_eliminado = 0
        LEFT JOIN pro5_series s ON s.id_serie = a.ID_serie AND s.estatus_eliminado = 0
        LEFT JOIN pro6_grupos g ON g.id_grupo = a.ID_grupo AND g.estatus_eliminado = 0
        LEFT JOIN pro7_marca m ON m.ID_marca = a.ID_marca AND m.estatus_eliminado = 0
        " . tiendaObtenerJoinPrecios() . "
        WHERE a.estatus_eliminado = 0
          AND a.activo = 1
    ";
}

function tiendaPrepararGaleriaProducto(array $producto): array
{
    $galeria = [];
    $vistos = [];

    foreach (['imagen1', 'imagen2', 'imagen3', 'imagen_simbolo'] as $campo) {
        $url = tiendaResolverRutaImagen((string) ($producto[$campo] ?? ''));
        if ($url === '' || isset($vistos[$url])) {
            continue;
        }

        $vistos[$url] = true;
        $galeria[] = [
            'url' => $url,
            'label' => match ($campo) {
                'imagen1' => 'Imagen principal',
                'imagen2' => 'Vista secundaria',
                'imagen3' => 'Vista complementaria',
                default => 'Imagen técnica',
            },
        ];
    }

    if ($galeria === []) {
        $galeria[] = [
            'url' => tiendaObtenerImagenPorDefecto(),
            'label' => 'Imagen del producto',
        ];
    }

    return $galeria;
}

function tiendaConstruirRecursosProducto(array $producto): array
{
    $recursos = [];

    $mapa = [
        ['label' => 'Ficha del modelo', 'url' => $producto['link_modelo'] ?? '', 'tipo' => 'link'],
        ['label' => 'PDF del modelo', 'url' => $producto['link_modelo_pdf'] ?? '', 'tipo' => 'pdf'],
        ['label' => 'Archivo CAD 1', 'url' => $producto['link_cad1'] ?? '', 'tipo' => 'cad'],
        ['label' => 'Archivo CAD 2', 'url' => $producto['link_cad2'] ?? '', 'tipo' => 'cad'],
        ['label' => 'Ficha de categoría', 'url' => $producto['link_categoria'] ?? '', 'tipo' => 'link'],
        ['label' => 'PDF de categoría', 'url' => $producto['link_categoria_pdf'] ?? '', 'tipo' => 'pdf'],
        ['label' => 'Ficha de catálogo', 'url' => $producto['link_catalogo'] ?? '', 'tipo' => 'link'],
        ['label' => 'PDF de catálogo', 'url' => $producto['link_catalogo_pdf'] ?? '', 'tipo' => 'pdf'],
        ['label' => 'Ficha de familia', 'url' => $producto['link_familia'] ?? '', 'tipo' => 'link'],
        ['label' => 'PDF de familia', 'url' => $producto['link_familia_pdf'] ?? '', 'tipo' => 'pdf'],
        ['label' => 'Ficha de línea', 'url' => $producto['link_linea'] ?? '', 'tipo' => 'link'],
        ['label' => 'PDF de línea', 'url' => $producto['link_linea_pdf'] ?? '', 'tipo' => 'pdf'],
        ['label' => 'Ficha de serie', 'url' => $producto['link_serie'] ?? '', 'tipo' => 'link'],
        ['label' => 'PDF de serie', 'url' => $producto['pdf_serie'] ?? '', 'tipo' => 'pdf'],
        ['label' => 'Ficha de grupo', 'url' => $producto['link_grupo'] ?? '', 'tipo' => 'link'],
        ['label' => 'PDF de grupo', 'url' => $producto['pdf_grupo'] ?? '', 'tipo' => 'pdf'],
        ['label' => 'Certificados', 'url' => $producto['certificados'] ?? '', 'tipo' => 'pdf'],
        ['label' => 'Ficha de marca', 'url' => $producto['link_marca'] ?? '', 'tipo' => 'link'],
        ['label' => 'PDF de marca', 'url' => $producto['link_marca_pdf'] ?? '', 'tipo' => 'pdf'],
    ];

    foreach ($mapa as $item) {
        $url = trim((string) $item['url']);
        if (!tiendaEsUrlRecursoValida($url)) {
            continue;
        }

        $recursos[] = [
            'label' => $item['label'],
            'url' => $url,
            'tipo' => $item['tipo'],
        ];
    }

    return $recursos;
}

function tiendaNormalizarDetalleProducto(array $producto): array
{
    $producto = tiendaNormalizarProductoFila($producto);
    $producto['descripcion_corta_label'] = limpiarTexto($producto['descripcion_corta'] ?? '', 220);
    $producto['descripcion_larga_html'] = nl2br(e(limpiarTexto($producto['descripcion_larga'] ?? '', 0)));
    $producto['descripcion_muy_larga_html'] = nl2br(e(limpiarTexto($producto['descripcion_muy_larga'] ?? '', 0)));
    $producto['descripcion_principal_html'] = $producto['descripcion_muy_larga_html'] !== ''
        ? $producto['descripcion_muy_larga_html']
        : ($producto['descripcion_larga_html'] !== '' ? $producto['descripcion_larga_html'] : nl2br(e($producto['descripcion_corta_label'])));
    $producto['galeria'] = tiendaPrepararGaleriaProducto($producto);
    $producto['marca_logo_url'] = tiendaResolverRutaImagen((string) ($producto['logo_marca'] ?? ''));
    $producto['especificaciones_full'] = formatearEspecificaciones(
        (string) ($producto['etiquetas_datos'] ?? ''),
        (string) ($producto['valores_datos'] ?? '')
    );
    $producto['recursos'] = tiendaConstruirRecursosProducto($producto);

    return $producto;
}

function tiendaObtenerDetalleProductoPorCodigoInterno(mysqli $mysqli, string $codigoInterno): ?array
{
    $codigoInterno = tiendaNormalizarCodigoInternoProducto($codigoInterno);

    if ($codigoInterno === '') {
        return null;
    }

    $sql = tiendaObtenerSelectDetalleProducto($mysqli) . "
          AND a.codigo_interno = ?
        LIMIT 1
    ";

    $producto = tiendaFetchOne($mysqli, $sql, [$codigoInterno]);

    if ($producto === null) {
        return null;
    }

    return tiendaNormalizarDetalleProducto($producto);
}

function tiendaObtenerDetalleProductoPorTokenRuta(mysqli $mysqli, string $tokenRuta): ?array
{
    $tokenRuta = tiendaNormalizarTokenRutaProducto($tokenRuta);

    if ($tokenRuta === '' || !tiendaTablaTieneColumna($mysqli, 'pro_articulos', 'slug')) {
        return null;
    }

    $sql = tiendaObtenerSelectDetalleProducto($mysqli) . "
          AND CONCAT(COALESCE(a.codigo_proveedor, ''), '-', COALESCE(a.slug, '')) = ?
        LIMIT 1
    ";

    $producto = tiendaFetchOne($mysqli, $sql, [$tokenRuta]);

    if ($producto === null) {
        return null;
    }

    if (limpiarTexto(($producto['codigo_proveedor'] ?? '') . '-' . ($producto['slug'] ?? ''), 300) !== $tokenRuta) {
        return null;
    }

    return tiendaNormalizarDetalleProducto($producto);
}

function tiendaConstruirCanonicalProducto(array $producto): string
{
    $enlace = generarEnlaceProducto($producto);

    if (str_starts_with($enlace, '/')) {
        return 'https://reddeaire.com' . $enlace;
    }

    return 'https://reddeaire.com/' . ltrim($enlace, '/');
}

function tiendaObtenerRelacionadosProducto(mysqli $mysqli, array $producto, int $limite = 8): array
{
    $limite = max(1, min($limite, 12));

    $sql = "
        SELECT
            a.id,
            a.codigo_interno,
            a.codigo_proveedor,
            " . (tiendaTablaTieneColumna($mysqli, 'pro_articulos', 'slug') ? "a.slug" : "NULL AS slug") . ",
            a.modelo,
            a.descripcion_corta,
            a.descripcion_larga,
            a.descripcion_muy_larga,
            a.imagen1,
            a.imagen2,
            a.imagen3,
            a.imagen_simbolo,
            COALESCE(NULLIF(TRIM(m.marca), ''), '') AS marca_nombre,
            COALESCE(NULLIF(TRIM(c.categoria), ''), '') AS categoria_nombre,
            COALESCE(NULLIF(TRIM(f.nombre_familia), ''), NULLIF(TRIM(f.familia), ''), '') AS familia_nombre,
            COALESCE(NULLIF(TRIM(g.nombre_grupo), ''), NULLIF(TRIM(g.grupo), ''), '') AS grupo_nombre,
            pi.precio_web,
            pi.precio_normal1,
            pi.moneda_precio_web,
            pi.stock,
            pi.disponibilidad,
            pi.en_inventario
        FROM pro_articulos a
        LEFT JOIN pro1_categoria c ON c.ID_categoria = a.ID_categoria AND c.estatus_eliminado = 0
        LEFT JOIN pro3_familia f ON f.ID_familia = a.ID_familia AND f.estatus_eliminado = 0
        LEFT JOIN pro6_grupos g ON g.id_grupo = a.ID_grupo AND g.estatus_eliminado = 0
        LEFT JOIN pro7_marca m ON m.ID_marca = a.ID_marca AND m.estatus_eliminado = 0
        " . tiendaObtenerJoinPrecios() . "
        WHERE a.estatus_eliminado = 0
          AND a.activo = 1
          AND a.codigo_interno <> ?
          AND (
                (a.ID_familia = ? AND a.ID_familia <> 0)
             OR (a.ID_grupo = ? AND a.ID_grupo <> 0)
             OR (a.ID_marca = ? AND a.ID_marca <> 0)
          )
        ORDER BY
            (a.ID_grupo = ?) DESC,
            (a.ID_familia = ?) DESC,
            (a.ID_marca = ?) DESC,
            a.id DESC
        LIMIT {$limite}
    ";

    $params = [
        $producto['codigo_interno'],
        (int) ($producto['ID_familia'] ?? 0),
        (int) ($producto['ID_grupo'] ?? 0),
        (int) ($producto['ID_marca'] ?? 0),
        (int) ($producto['ID_grupo'] ?? 0),
        (int) ($producto['ID_familia'] ?? 0),
        (int) ($producto['ID_marca'] ?? 0),
    ];

    $relacionados = tiendaFetchAll($mysqli, $sql, $params);

    foreach ($relacionados as &$relacionado) {
        $relacionado = tiendaNormalizarProductoFila($relacionado);
    }
    unset($relacionado);

    return $relacionados;
}
