<?php
/**
 * ----------------------------------------------------------------------------
 * ARCHIVO: tienda/jerarquia-publica.php
 * ----------------------------------------------------------------------------
 * Centraliza la navegación jerárquica pública de la tienda industrial y
 * resuelve la visualización de categorías, catálogos, familias, líneas,
 * series y grupos con consultas preparadas MySQLi, conteos agregados de
 * referencias, breadcrumb contextual, URLs seguras e imágenes con fallback
 * visual cuando la ruta no existe o el campo está vacío.
 *
 * Responsabilidades:
 * - Definir la configuración real de cada nivel jerárquico.
 * - Validar y normalizar parámetros GET como `via`.
 * - Reconstruir el contexto de navegación aunque el acceso sea directo.
 * - Consultar listados públicos sin consultas dentro de loops.
 * - Renderizar tarjetas y breadcrumb consistentes para toda la jerarquía.
 *
 * Dependencias:
 * - tienda/funciones.php
 *
 * Parámetros externos consumidos por las vistas:
 * - via
 * - categoria
 * - catalogo
 * - familia
 * - linea
 * - serie
 * - grupo
 * ----------------------------------------------------------------------------
 */

declare(strict_types=1);

require_once __DIR__ . '/funciones.php';

function tiendaJerarquiaOrden(): array
{
    return ['categoria', 'catalogo', 'familia', 'linea', 'serie', 'grupo'];
}

function tiendaJerarquiaConfiguracion(): array
{
    return [
        'categoria' => [
            'key' => 'categoria',
            'page' => 'tienda-categoria.php',
            'table' => 'pro1_categoria',
            'alias' => 'c',
            'id_field' => 'ID_categoria',
            'name_expr' => "COALESCE(NULLIF(TRIM(c.categoria), ''), CONCAT('Categoría ', c.ID_categoria))",
            'description_field' => 'descripcion_categoria',
            'image_field' => 'imagen_categoria',
            'article_field' => 'ID_categoria',
            'parent_key' => null,
            'parent_db_field' => null,
            'context_fields' => [],
            'title' => 'Categorías',
            'subtitle' => 'Explora el portafolio industrial desde las categorías principales disponibles.',
            'kicker' => 'CATEGORÍAS',
            'empty_title' => 'No hay categorías disponibles',
            'empty_text' => 'No encontramos categorías públicas activas para mostrar en este momento.',
            'card_action' => 'Ver catálogos',
            'next_key' => 'catalogo',
            'next_page' => 'tienda-catalogos.php',
            'product_filter' => 'categoria',
            'icon' => 'fa-diagram-project',
            'context_label' => 'Categoría seleccionada',
        ],
        'catalogo' => [
            'key' => 'catalogo',
            'page' => 'tienda-catalogos.php',
            'table' => 'pro2_catalogo',
            'alias' => 'ca',
            'id_field' => 'ID_catalogo',
            'name_expr' => "COALESCE(NULLIF(TRIM(ca.catalogo), ''), CONCAT('Catálogo ', ca.ID_catalogo))",
            'description_field' => 'descripcion_catalogo',
            'image_field' => 'imagen_catalogo',
            'article_field' => 'ID_catalogo',
            'parent_key' => 'categoria',
            'parent_db_field' => 'categoria_id',
            'context_fields' => [
                'categoria' => 'categoria_id',
            ],
            'title' => 'Catálogos',
            'subtitle' => 'Consulta los catálogos industriales disponibles y continúa el recorrido técnico por familia.',
            'kicker' => 'CATÁLOGOS',
            'empty_title' => 'No hay catálogos para este filtro',
            'empty_text' => 'No encontramos catálogos activos para la selección recibida.',
            'card_action' => 'Ver familias',
            'next_key' => 'familia',
            'next_page' => 'tienda-familias.php',
            'product_filter' => 'catalogo',
            'icon' => 'fa-boxes-stacked',
            'context_label' => 'Categoría base',
        ],
        'familia' => [
            'key' => 'familia',
            'page' => 'tienda-familias.php',
            'table' => 'pro3_familia',
            'alias' => 'f',
            'id_field' => 'ID_familia',
            'name_expr' => "COALESCE(NULLIF(TRIM(f.nombre_familia), ''), NULLIF(TRIM(f.familia), ''), CONCAT('Familia ', f.ID_familia))",
            'description_field' => 'descripcion_familia',
            'image_field' => 'imagen_familia',
            'article_field' => 'ID_familia',
            'parent_key' => 'catalogo',
            'parent_db_field' => 'catalogo_id',
            'context_fields' => [
                'categoria' => 'categoria_id',
                'catalogo' => 'catalogo_id',
            ],
            'title' => 'Familias',
            'subtitle' => 'Agrupa las referencias por familia y acelera la navegación técnica hacia las líneas disponibles.',
            'kicker' => 'FAMILIAS',
            'empty_title' => 'No hay familias disponibles',
            'empty_text' => 'No encontramos familias públicas para el contexto seleccionado.',
            'card_action' => 'Ver líneas',
            'next_key' => 'linea',
            'next_page' => 'tienda-lineas.php',
            'product_filter' => 'familia',
            'icon' => 'fa-layer-group',
            'context_label' => 'Catálogo base',
        ],
        'linea' => [
            'key' => 'linea',
            'page' => 'tienda-lineas.php',
            'table' => 'pro4_linea',
            'alias' => 'l',
            'id_field' => 'linea',
            'name_expr' => "COALESCE(NULLIF(TRIM(l.nombre_linea), ''), CONCAT('Línea ', l.linea))",
            'description_field' => 'descripcion_linea',
            'image_field' => 'imagen_linea',
            'article_field' => 'ID_linea',
            'parent_key' => 'familia',
            'parent_db_field' => 'familia_id',
            'context_fields' => [
                'categoria' => 'categoria_id',
                'catalogo' => 'catalogo_id',
                'familia' => 'familia_id',
            ],
            'title' => 'Líneas',
            'subtitle' => 'Ordena la oferta industrial por línea para descender hacia series y grupos de producto.',
            'kicker' => 'LÍNEAS',
            'empty_title' => 'No hay líneas disponibles',
            'empty_text' => 'No encontramos líneas activas para la selección recibida.',
            'card_action' => 'Ver series',
            'next_key' => 'serie',
            'next_page' => 'tienda-series.php',
            'product_filter' => 'linea',
            'icon' => 'fa-sliders',
            'context_label' => 'Familia base',
        ],
        'serie' => [
            'key' => 'serie',
            'page' => 'tienda-series.php',
            'table' => 'pro5_series',
            'alias' => 's',
            'id_field' => 'id_serie',
            'name_expr' => "COALESCE(NULLIF(TRIM(s.nombre_serie), ''), NULLIF(TRIM(s.serie), ''), CONCAT('Serie ', s.id_serie))",
            'description_field' => 'descripcion_serie',
            'image_field' => 'imagen_serie',
            'article_field' => 'ID_serie',
            'parent_key' => 'linea',
            'parent_db_field' => 'id_linea',
            'context_fields' => [
                'categoria' => 'categoria',
                'catalogo' => 'catalogo',
                'familia' => 'id_familia',
                'linea' => 'id_linea',
            ],
            'title' => 'Series',
            'subtitle' => 'Visualiza series activas y continúa hasta los grupos finales que aterrizan el listado de productos.',
            'kicker' => 'SERIES',
            'empty_title' => 'No hay series disponibles',
            'empty_text' => 'No encontramos series activas para el filtro aplicado.',
            'card_action' => 'Ver grupos',
            'next_key' => 'grupo',
            'next_page' => 'tienda-grupos.php',
            'product_filter' => 'serie',
            'icon' => 'fa-bezier-curve',
            'context_label' => 'Línea base',
        ],
        'grupo' => [
            'key' => 'grupo',
            'page' => 'tienda-grupos.php',
            'table' => 'pro6_grupos',
            'alias' => 'g',
            'id_field' => 'id_grupo',
            'name_expr' => "COALESCE(NULLIF(TRIM(g.nombre_grupo), ''), NULLIF(TRIM(g.grupo), ''), CONCAT('Grupo ', g.id_grupo))",
            'description_field' => 'descripcion_grupo',
            'image_field' => 'imagen_grupo',
            'secondary_value_field' => 'grupo',
            'secondary_value_label' => 'Grupo',
            'article_field' => 'ID_grupo',
            'parent_key' => 'serie',
            'parent_db_field' => 'id_serie',
            'context_fields' => [
                'categoria' => 'id_categoria',
                'catalogo' => 'id_catalogo',
                'familia' => 'id_familia',
                'linea' => 'id_linea',
                'serie' => 'id_serie',
            ],
            'title' => 'Grupos',
            'subtitle' => 'Selecciona el grupo final y salta al catálogo público con el filtro seguro ya aplicado.',
            'kicker' => 'GRUPOS',
            'empty_title' => 'No hay grupos disponibles',
            'empty_text' => 'No encontramos grupos activos para el contexto seleccionado.',
            'card_action' => 'Ver productos',
            'next_key' => null,
            'next_page' => 'tienda-productos.php',
            'product_filter' => 'grupo',
            'icon' => 'fa-cubes-stacked',
            'context_label' => 'Serie base',
        ],
    ];
}

function tiendaJerarquiaConfiguracionNivel(string $nivel): array
{
    $configuracion = tiendaJerarquiaConfiguracion();
    return $configuracion[$nivel] ?? $configuracion['categoria'];
}

function tiendaJerarquiaPlaceholderSvg(string $nivel, string $titulo): string
{
    $titulo = limpiarTexto($titulo, 40);
    $abreviatura = mb_strtoupper(mb_substr($titulo !== '' ? $titulo : $nivel, 0, 2, 'UTF-8'), 'UTF-8');
    $svg = <<<SVG
<svg xmlns="http://www.w3.org/2000/svg" width="320" height="220" viewBox="0 0 320 220" fill="none">
    <rect width="320" height="220" rx="28" fill="#F4F6F8"/>
    <rect x="24" y="24" width="272" height="172" rx="24" fill="#FFFFFF" stroke="#D6DEE6"/>
    <circle cx="160" cy="100" r="42" fill="#E7EEF5"/>
    <path d="M122 100h76" stroke="#0B3A53" stroke-width="10" stroke-linecap="round"/>
    <path d="M160 62v76" stroke="#0B3A53" stroke-width="10" stroke-linecap="round"/>
    <text x="160" y="176" text-anchor="middle" fill="#0B3A53" font-family="Arial, sans-serif" font-size="22" font-weight="700">{$abreviatura}</text>
</svg>
SVG;

    return 'data:image/svg+xml;charset=UTF-8,' . rawurlencode($svg);
}

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

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

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

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

    $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),
        'proyecto-productos/proyecto-productos/imgsystem/' . basename($ruta),
    ];

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

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

    return '';
}

function tiendaJerarquiaObtenerRegistroNivel(mysqli $mysqli, string $nivel, int $id): ?array
{
    static $cache = [];

    if ($id <= 0) {
        return null;
    }

    $cacheKey = $nivel . ':' . $id;
    if (array_key_exists($cacheKey, $cache)) {
        return $cache[$cacheKey];
    }

    $config = tiendaJerarquiaConfiguracionNivel($nivel);
    $camposExtras = [];

    foreach ($config['context_fields'] as $campoContexto => $campoDb) {
        $camposExtras[] = "{$config['alias']}.{$campoDb} AS {$campoContexto}_id";
    }

    $selectExtras = $camposExtras !== [] ? ', ' . implode(', ', $camposExtras) : '';
    $sql = "
        SELECT
            {$config['alias']}.{$config['id_field']} AS id,
            {$config['name_expr']} AS nombre,
            COALESCE(NULLIF(TRIM({$config['alias']}.{$config['description_field']}), ''), '') AS descripcion,
            COALESCE(NULLIF(TRIM({$config['alias']}.{$config['image_field']}), ''), '') AS imagen
            {$selectExtras}
        FROM {$config['table']} {$config['alias']}
        WHERE {$config['alias']}.estatus_eliminado = 0
          AND COALESCE({$config['alias']}.estado, 1) = 1
          AND {$config['alias']}.{$config['id_field']} = ?
        LIMIT 1
    ";

    $fila = tiendaFetchOne($mysqli, $sql, [$id]);
    if ($fila === null) {
        $cache[$cacheKey] = null;
        return null;
    }

    $fila['id'] = (int) ($fila['id'] ?? 0);
    $fila['nombre'] = limpiarTexto($fila['nombre'] ?? '', 160);
    $fila['descripcion'] = limpiarTexto($fila['descripcion'] ?? '', 260);
    $fila['imagen'] = trim((string) ($fila['imagen'] ?? ''));

    foreach (array_keys($config['context_fields']) as $campoContexto) {
        $fila[$campoContexto . '_id'] = (int) ($fila[$campoContexto . '_id'] ?? 0);
    }

    $cache[$cacheKey] = $fila;
    return $fila;
}

function tiendaJerarquiaCompletarContexto(mysqli $mysqli, array $contexto): array
{
    $orden = array_reverse(tiendaJerarquiaOrden());

    foreach ($orden as $nivel) {
        $id = (int) ($contexto[$nivel] ?? 0);
        if ($id <= 0) {
            continue;
        }

        $registro = tiendaJerarquiaObtenerRegistroNivel($mysqli, $nivel, $id);
        if ($registro === null) {
            $contexto[$nivel] = 0;
            continue;
        }

        $contexto[$nivel] = (int) ($registro['id'] ?? 0);
        foreach (tiendaJerarquiaConfiguracionNivel($nivel)['context_fields'] as $claveContexto => $campoDb) {
            $contexto[$claveContexto] = (int) ($registro[$claveContexto . '_id'] ?? 0);
        }

        break;
    }

    return $contexto;
}

function tiendaJerarquiaContextoDesdeRequest(mysqli $mysqli, string $nivel, array $request): array
{
    $contexto = [];
    foreach (tiendaJerarquiaOrden() as $clave) {
        $contexto[$clave] = tiendaNormalizarEntero($request[$clave] ?? 0);
    }

    $via = tiendaNormalizarEntero($request['via'] ?? 0);
    $config = tiendaJerarquiaConfiguracionNivel($nivel);

    if ($via > 0) {
        if ($nivel === 'categoria') {
            $contexto['categoria'] = $via;
        } elseif (($config['parent_key'] ?? null) !== null) {
            $contexto[$config['parent_key']] = $via;
        }
    }

    return tiendaJerarquiaCompletarContexto($mysqli, $contexto);
}

function tiendaJerarquiaViaValidada(string $nivel, array $contexto): int
{
    $config = tiendaJerarquiaConfiguracionNivel($nivel);
    if ($nivel === 'categoria') {
        return (int) ($contexto['categoria'] ?? 0);
    }

    $parentKey = (string) ($config['parent_key'] ?? '');
    return $parentKey !== '' ? (int) ($contexto[$parentKey] ?? 0) : 0;
}

function tiendaJerarquiaObtenerItems(mysqli $mysqli, string $nivel, int $via = 0): array
{
    $config = tiendaJerarquiaConfiguracionNivel($nivel);
    $params = [];
    $selectExtras = '';
    $groupByExtras = '';
    $orderBy = "
            COUNT(*) DESC,
            nombre ASC";
    $where = [
        'a.estatus_eliminado = 0',
        'a.activo = 1',
        "a.{$config['article_field']} IS NOT NULL",
        "a.{$config['article_field']} <> 0",
    ];

    if (!empty($config['secondary_value_field'])) {
        $selectExtras .= ",
            COALESCE(NULLIF(TRIM({$config['alias']}.{$config['secondary_value_field']}), ''), '') AS secondary_value";
        $groupByExtras .= ",
            secondary_value";
    }

    if ($nivel === 'grupo') {
        $selectExtras .= ",
            CASE
                WHEN NULLIF(TRIM({$config['alias']}.item_grupo), '') IS NULL THEN 1
                WHEN TRIM({$config['alias']}.item_grupo) REGEXP '^[0-9]+$' THEN 0
                ELSE 1
            END AS sort_item_grupo_vacio,
            CASE
                WHEN TRIM({$config['alias']}.item_grupo) REGEXP '^[0-9]+$' THEN CAST(TRIM({$config['alias']}.item_grupo) AS UNSIGNED)
                ELSE 999999999
            END AS sort_item_grupo_numero,
            COALESCE(
                NULLIF(TRIM({$config['alias']}.nombre_grupo), ''),
                NULLIF(TRIM({$config['alias']}.grupo), ''),
                CONCAT('Grupo ', {$config['alias']}.id_grupo)
            ) AS sort_nombre_grupo";
        $groupByExtras .= ",
            sort_item_grupo_vacio,
            sort_item_grupo_numero,
            sort_nombre_grupo";
        $orderBy = "
            sort_item_grupo_vacio ASC,
            sort_item_grupo_numero ASC,
            sort_nombre_grupo ASC";
    }

    if ($via > 0) {
        if ($nivel === 'categoria') {
            $where[] = "a.{$config['article_field']} = ?";
        } elseif (($config['parent_key'] ?? null) !== null) {
            $configPadre = tiendaJerarquiaConfiguracionNivel((string) $config['parent_key']);
            $where[] = 'a.' . $configPadre['article_field'] . ' = ?';
        }
        $params[] = $via;
    }

    $sql = "
        SELECT
            a.{$config['article_field']} AS id,
            COALESCE(
                {$config['name_expr']},
                CONCAT('" . ucfirst($nivel) . " ', a.{$config['article_field']})
            ) AS nombre,
            COALESCE(NULLIF(TRIM({$config['alias']}.{$config['description_field']}), ''), '') AS descripcion,
            COALESCE(NULLIF(TRIM({$config['alias']}.{$config['image_field']}), ''), '') AS imagen{$selectExtras},
            COUNT(*) AS total
        FROM pro_articulos a
        LEFT JOIN {$config['table']} {$config['alias']}
            ON {$config['alias']}.{$config['id_field']} = a.{$config['article_field']}
           AND {$config['alias']}.estatus_eliminado = 0
        WHERE " . implode(' AND ', $where) . "
        GROUP BY
            a.{$config['article_field']},
            nombre,
            descripcion,
            imagen{$groupByExtras}
        ORDER BY
            {$orderBy}
        LIMIT 400
    ";

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

    foreach ($items as &$item) {
        $item['id'] = (int) ($item['id'] ?? 0);
        $item['nombre'] = limpiarTexto($item['nombre'] ?? '', 160);
        $item['nombre_corto'] = truncarTexto($item['nombre'], 42);
        $item['descripcion'] = limpiarTexto($item['descripcion'] ?? '', 260);
        $item['descripcion_corta'] = truncarTexto($item['descripcion'], 130);
        $item['imagen'] = trim((string) ($item['imagen'] ?? ''));
        $item['imagen_url'] = tiendaJerarquiaResolverImagenUrl($item['imagen']);
        $item['secondary_value'] = trim((string) ($item['secondary_value'] ?? ''));
        $item['total'] = (int) ($item['total'] ?? 0);
    }
    unset($item);

    return $items;
}

function tiendaJerarquiaMacroAgrupacionDef(string $nivel): ?array
{
    if ($nivel === 'serie') {
        return [
            'nivel' => 'serie',
            'main_table' => 'macro_series',
            'detail_table' => 'macro_series_detalle',
            'main_id_field' => 'id',
            'main_name_field' => 'nombre_macroserie',
            'main_description_field' => 'descripcion_macroserie',
            'main_link_field' => 'link_macroserie',
            'main_pdf_field' => 'pdf_macroserie',
            'detail_parent_field' => 'macroserie_id',
            'detail_child_field' => 'serie_id',
            'child_id_field' => 'id_serie',
            'label_singular' => 'macroserie',
            'label_plural' => 'macroseries',
            'label_upper' => 'MACROSERIE',
            'free_label' => 'Series sin macroserie',
        ];
    }

    if ($nivel === 'grupo') {
        return [
            'nivel' => 'grupo',
            'main_table' => 'macro_grupos',
            'detail_table' => 'macro_grupos_detalle',
            'main_id_field' => 'id',
            'main_name_field' => 'nombre_macrogrupo',
            'main_description_field' => 'descripcion_macrogrupo',
            'main_link_field' => 'link_macrogrupo',
            'main_pdf_field' => 'pdf_macrogrupo',
            'detail_parent_field' => 'macrogrupo_id',
            'detail_child_field' => 'grupo_id',
            'child_id_field' => 'id_grupo',
            'label_singular' => 'macrogrupo',
            'label_plural' => 'macrogrupos',
            'label_upper' => 'MACROGRUPO',
            'free_label' => 'Grupos sin macrogrupo',
        ];
    }

    return null;
}

function tiendaJerarquiaMacroAgrupacionTablasDisponibles(mysqli $mysqli, string $nivel): bool
{
    static $cache = [];
    $def = tiendaJerarquiaMacroAgrupacionDef($nivel);

    if ($def === null) {
        return false;
    }

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

    $sql = "
        SELECT COUNT(*) AS total
        FROM information_schema.TABLES
        WHERE TABLE_SCHEMA = DATABASE()
          AND TABLE_NAME IN (?, ?)
    ";

    $fila = tiendaFetchOne($mysqli, $sql, [$def['main_table'], $def['detail_table']]);
    $cache[$nivel] = (int) ($fila['total'] ?? 0) === 2;

    return $cache[$nivel];
}

function tiendaJerarquiaNormalizarUrlMacroAgrupacion(?string $valor): string
{
    $valor = trim((string) $valor);

    if ($valor === '' || !filter_var($valor, FILTER_VALIDATE_URL)) {
        return '';
    }

    $esquema = strtolower((string) parse_url($valor, PHP_URL_SCHEME));

    return in_array($esquema, ['http', 'https'], true) ? $valor : '';
}

function tiendaJerarquiaAgruparItemsMacroNivel(mysqli $mysqli, string $nivel, array $items): array
{
    $def = tiendaJerarquiaMacroAgrupacionDef($nivel);

    if ($def === null || $items === [] || !tiendaJerarquiaMacroAgrupacionTablasDisponibles($mysqli, $nivel)) {
        return [
            'macro_items' => [],
            'items_libres' => $items,
            'label_plural' => '',
            'free_label' => '',
        ];
    }

    $itemsPorId = [];
    $ids = [];

    foreach ($items as $item) {
        $itemId = (int) ($item['id'] ?? 0);

        if ($itemId <= 0) {
            continue;
        }

        $itemsPorId[$itemId] = $item;
        $ids[] = $itemId;
    }

    if ($ids === []) {
        return [
            'macro_items' => [],
            'items_libres' => $items,
            'label_plural' => (string) $def['label_plural'],
            'free_label' => (string) $def['free_label'],
        ];
    }

    $placeholders = implode(',', array_fill(0, count($ids), '?'));
    $sql = "
        SELECT
            m.{$def['main_id_field']} AS macro_id,
            m.{$def['main_name_field']} AS macro_nombre,
            COALESCE(NULLIF(TRIM(m.{$def['main_description_field']}), ''), '') AS macro_descripcion,
            COALESCE(NULLIF(TRIM(m.{$def['main_link_field']}), ''), '') AS macro_link,
            COALESCE(NULLIF(TRIM(m.{$def['main_pdf_field']}), ''), '') AS macro_pdf,
            COALESCE(m.orden_impresion, 0) AS orden_impresion,
            d.{$def['detail_child_field']} AS item_id
        FROM {$def['main_table']} m
        INNER JOIN {$def['detail_table']} d
            ON d.{$def['detail_parent_field']} = m.{$def['main_id_field']}
        WHERE m.estado = 1
          AND d.{$def['detail_child_field']} IN ({$placeholders})
        ORDER BY
            m.orden_impresion ASC,
            m.{$def['main_name_field']} ASC,
            d.id ASC
    ";

    $rows = tiendaFetchAll($mysqli, $sql, $ids);
    $macroItems = [];
    $itemsAsignados = [];

    foreach ($rows as $row) {
        $itemId = (int) ($row['item_id'] ?? 0);
        $macroId = (int) ($row['macro_id'] ?? 0);

        if ($itemId <= 0 || $macroId <= 0) {
            continue;
        }

        if (!isset($itemsPorId[$itemId]) || isset($itemsAsignados[$itemId])) {
            continue;
        }

        if (!isset($macroItems[$macroId])) {
            $macroItems[$macroId] = [
                'id' => $macroId,
                'titulo' => limpiarTexto($row['macro_nombre'] ?? '', 160),
                'descripcion' => limpiarTexto($row['macro_descripcion'] ?? '', 320),
                'link' => tiendaJerarquiaNormalizarUrlMacroAgrupacion($row['macro_link'] ?? ''),
                'pdf' => tiendaJerarquiaNormalizarUrlMacroAgrupacion($row['macro_pdf'] ?? ''),
                'orden_impresion' => (int) ($row['orden_impresion'] ?? 0),
                'items' => [],
                'nivel' => $nivel,
                'label_upper' => (string) $def['label_upper'],
            ];
        }

        $macroItems[$macroId]['items'][] = $itemsPorId[$itemId];
        $itemsAsignados[$itemId] = true;
    }

    $itemsLibres = [];

    foreach ($items as $item) {
        $itemId = (int) ($item['id'] ?? 0);

        if ($itemId > 0 && isset($itemsAsignados[$itemId])) {
            continue;
        }

        $itemsLibres[] = $item;
    }

    $macroItems = array_values(array_filter($macroItems, static function (array $macroItem): bool {
        return $macroItem['items'] !== [];
    }));

    return [
        'macro_items' => $macroItems,
        'items_libres' => $itemsLibres,
        'label_plural' => (string) $def['label_plural'],
        'free_label' => (string) $def['free_label'],
    ];
}

function tiendaJerarquiaMetadatosContexto(mysqli $mysqli, array $contexto): array
{
    $metadatos = [];

    foreach (tiendaJerarquiaOrden() as $nivel) {
        $id = (int) ($contexto[$nivel] ?? 0);
        if ($id <= 0) {
            continue;
        }

        $registro = tiendaJerarquiaObtenerRegistroNivel($mysqli, $nivel, $id);
        if ($registro !== null) {
            $metadatos[$nivel] = $registro;
        }
    }

    return $metadatos;
}

function tiendaJerarquiaUrl(string $ruta, array $params = []): string
{
    $ruta = tiendaUrlAbsoluta($ruta);
    if ($params === []) {
        return $ruta;
    }

    return $ruta . '?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986);
}

function tiendaJerarquiaParametrosHasta(array $contexto, string $nivelHasta): array
{
    $params = [];

    foreach (tiendaJerarquiaOrden() as $nivel) {
        $valor = (int) ($contexto[$nivel] ?? 0);
        if ($valor > 0) {
            $params[$nivel] = $valor;
        }

        if ($nivel === $nivelHasta) {
            break;
        }
    }

    return $params;
}

function tiendaJerarquiaConstruirUrlCard(string $nivelActual, array $item, array $contexto): string
{
    $configActual = tiendaJerarquiaConfiguracionNivel($nivelActual);
    $contextoNuevo = $contexto;
    $contextoNuevo[$nivelActual] = (int) ($item['id'] ?? 0);

    if (($configActual['next_key'] ?? null) === null) {
        return tiendaJerarquiaConstruirUrlProductos($contextoNuevo);
    }

    $params = tiendaJerarquiaParametrosHasta($contextoNuevo, $nivelActual);
    $params['via'] = (int) ($item['id'] ?? 0);

    return tiendaJerarquiaUrl((string) $configActual['next_page'], $params);
}

function tiendaJerarquiaConstruirUrlVolver(string $nivelActual, array $contexto): string
{
    $orden = tiendaJerarquiaOrden();
    $indice = array_search($nivelActual, $orden, true);

    if ($indice === false || $indice === 0) {
        if ((int) ($contexto['categoria'] ?? 0) > 0) {
            return tiendaJerarquiaUrl('tienda-categoria.php');
        }

        return tiendaJerarquiaUrl('index-web.php');
    }

    $nivelPadre = $orden[$indice - 1];
    $configPadre = tiendaJerarquiaConfiguracionNivel($nivelPadre);
    $params = [];

    if ($nivelPadre === 'categoria') {
        $categoriaId = (int) ($contexto['categoria'] ?? 0);
        if ($categoriaId > 0) {
            $params['via'] = $categoriaId;
        }
        return tiendaJerarquiaUrl($configPadre['page'], $params);
    }

    $nivelAbuelo = $orden[$indice - 2] ?? null;
    if ($nivelAbuelo !== null) {
        $abueloId = (int) ($contexto[$nivelAbuelo] ?? 0);
        if ($abueloId > 0) {
            $params = tiendaJerarquiaParametrosHasta($contexto, $nivelAbuelo);
            $params['via'] = $abueloId;
        }
    }

    return tiendaJerarquiaUrl($configPadre['page'], $params);
}

function tiendaJerarquiaConstruirUrlProductos(array $contexto): string
{
    foreach (array_reverse(tiendaJerarquiaOrden()) as $nivel) {
        $valor = (int) ($contexto[$nivel] ?? 0);
        if ($valor > 0) {
            return tiendaJerarquiaUrl('tienda-productos.php', [
                $nivel => $valor,
            ]);
        }
    }

    return tiendaJerarquiaUrl('tienda-productos.php');
}

function tiendaJerarquiaConstruirFiltrosProductos(array $contexto): array
{
    $filtros = [];

    foreach (tiendaJerarquiaOrden() as $nivel) {
        $valor = (int) ($contexto[$nivel] ?? 0);
        if ($valor > 0) {
            $filtros[$nivel] = $valor;
        }
    }

    return $filtros;
}

function tiendaJerarquiaContarProductosContexto(mysqli $mysqli, array $contexto): int
{
    $filtros = tiendaJerarquiaConstruirFiltrosProductos($contexto);
    $where = [
        'estatus_eliminado = 0',
        'activo = 1',
    ];
    $params = [];
    $mapaCampos = [
        'categoria' => 'ID_categoria',
        'catalogo' => 'ID_catalogo',
        'familia' => 'ID_familia',
        'linea' => 'ID_linea',
        'serie' => 'ID_serie',
        'grupo' => 'ID_grupo',
    ];

    foreach ($mapaCampos as $nivel => $campo) {
        $valor = (int) ($filtros[$nivel] ?? 0);
        if ($valor > 0) {
            $where[] = $campo . ' = ?';
            $params[] = $valor;
        }
    }

    $sql = "
        SELECT COUNT(*) AS total
        FROM pro_articulos
        WHERE " . implode(' AND ', $where) . "
    ";

    $fila = tiendaFetchOne($mysqli, $sql, $params);
    return (int) ($fila['total'] ?? 0);
}

function tiendaJerarquiaBadgeReferencias(int $total): string
{
    if ($total <= 0) {
        return 'Sin referencias';
    }

    return number_format($total, 0, ',', '.') . ' referencias';
}

function tiendaJerarquiaRenderMedia(array $item, string $nivel, string $contexto = 'grid'): string
{
    $config = tiendaJerarquiaConfiguracionNivel($nivel);
    $titulo = (string) ($item['nombre'] ?? $config['title']);
    $imagen = trim((string) ($item['imagen_url'] ?? ''));

    if ($imagen !== '') {
        return '<img src="' . e($imagen) . '" alt="' . e($titulo) . '" loading="lazy">';
    }

    if ($contexto === 'carousel') {
        return '<span class="catalog-rail-fallback" aria-hidden="true"><i class="fa-solid ' . e((string) $config['icon']) . '"></i></span>';
    }

    $placeholder = tiendaJerarquiaPlaceholderSvg($nivel, $titulo);
    return '<img src="' . e($placeholder) . '" alt="' . e($titulo) . '" loading="lazy">';
}

function tiendaJerarquiaBreadcrumb(string $nivelActual, array $contexto, array $metadatos): array
{
    $items = [
        [
            'label' => 'Inicio',
            'url' => tiendaJerarquiaUrl('index-web.php'),
            'active' => false,
        ],
        [
            'label' => 'Categorías',
            'url' => tiendaJerarquiaUrl('tienda-categoria.php'),
            'active' => false,
        ],
    ];

    if ($nivelActual === 'categoria') {
        if ((int) ($contexto['categoria'] ?? 0) > 0 && isset($metadatos['categoria'])) {
            $items[] = [
                'label' => (string) $metadatos['categoria']['nombre'],
                'url' => '',
                'active' => true,
            ];
        } else {
            $items[1]['active'] = true;
        }

        return $items;
    }

    $orden = tiendaJerarquiaOrden();
    $indiceActual = (int) array_search($nivelActual, $orden, true);

    for ($i = 0; $i < $indiceActual; $i++) {
        $nivel = $orden[$i];
        $valor = (int) ($contexto[$nivel] ?? 0);

        if ($valor <= 0 || !isset($metadatos[$nivel])) {
            continue;
        }

        if ($nivel === 'categoria') {
            $items[] = [
                'label' => (string) $metadatos[$nivel]['nombre'],
                'url' => tiendaJerarquiaUrl('tienda-catalogos.php', ['via' => $valor, 'categoria' => $valor]),
                'active' => false,
            ];
            continue;
        }

        $configNivel = tiendaJerarquiaConfiguracionNivel($nivel);
        $params = tiendaJerarquiaParametrosHasta($contexto, $nivel);
        if (($configNivel['next_page'] ?? null) !== null) {
            $params['via'] = $valor;
            $url = tiendaJerarquiaUrl((string) $configNivel['next_page'], $params);
        } else {
            $url = tiendaJerarquiaUrl((string) $configNivel['page'], $params);
        }
        $items[] = [
            'label' => (string) $metadatos[$nivel]['nombre'],
            'url' => $url,
            'active' => false,
        ];
    }

    $items[] = [
        'label' => (string) tiendaJerarquiaConfiguracionNivel($nivelActual)['title'],
        'url' => '',
        'active' => true,
    ];

    return $items;
}

function tiendaJerarquiaPrepararVista(mysqli $mysqli, string $nivel, array $request): array
{
    $config = tiendaJerarquiaConfiguracionNivel($nivel);
    $contexto = tiendaJerarquiaContextoDesdeRequest($mysqli, $nivel, $request);
    $viaValidado = tiendaJerarquiaViaValidada($nivel, $contexto);
    $items = tiendaJerarquiaObtenerItems($mysqli, $nivel, $viaValidado);
    $metadatos = tiendaJerarquiaMetadatosContexto($mysqli, $contexto);
    $contextoPadre = $config['parent_key'] !== null && isset($metadatos[$config['parent_key']])
        ? $metadatos[$config['parent_key']]
        : null;
    $contextoActual = isset($metadatos[$nivel]) ? $metadatos[$nivel] : null;
    $titulo = $config['title'];
    $descripcion = $config['subtitle'];

    if ($contextoPadre !== null) {
        $titulo = $config['title'] . ' de ' . $contextoPadre['nombre'];
        $descripcion = $config['context_label'] . ': ' . $contextoPadre['nombre'] . '. ' . $config['subtitle'];
    } elseif ($nivel === 'categoria' && $contextoActual !== null) {
        $titulo = $contextoActual['nombre'];
        $descripcion = 'Detalle de la categoría seleccionada dentro del portafolio público.';
    }

    $totalReferencias = 0;
    foreach ($items as $item) {
        $totalReferencias += (int) ($item['total'] ?? 0);
    }

    $productosDirectos = tiendaJerarquiaContarProductosContexto($mysqli, $contexto);
    $autoredirectUrl = '';

    if (
        $viaValidado > 0
        && $nivel !== 'grupo'
        && $items === []
        && $productosDirectos > 0
    ) {
        $autoredirectUrl = tiendaJerarquiaConstruirUrlProductos($contexto);
    }

    $macroAgrupacionData = [
        'macro_items' => [],
        'items_libres' => $items,
        'label_plural' => '',
        'free_label' => '',
    ];

    if (in_array($nivel, ['serie', 'grupo'], true) && $items !== []) {
        $macroAgrupacionData = tiendaJerarquiaAgruparItemsMacroNivel($mysqli, $nivel, $items);
    }

    return [
        'nivel' => $nivel,
        'config' => $config,
        'contexto' => $contexto,
        'via' => $viaValidado,
        'items' => $items,
        'metadatos' => $metadatos,
        'contexto_padre' => $contextoPadre,
        'contexto_actual' => $contextoActual,
        'titulo' => $titulo,
        'descripcion' => $descripcion,
        'breadcrumb' => tiendaJerarquiaBreadcrumb($nivel, $contexto, $metadatos),
        'volver_url' => tiendaJerarquiaConstruirUrlVolver($nivel, $contexto),
        'ver_todo_url' => tiendaJerarquiaUrl($config['page']),
        'productos_url' => tiendaJerarquiaConstruirUrlProductos($contexto),
        'meta_title' => 'Red de Aire | ' . $titulo,
        'meta_description' => truncarTexto($descripcion, 160),
        'total_items' => count($items),
        'total_referencias' => $totalReferencias,
        'productos_directos' => $productosDirectos,
        'autoredirect_url' => $autoredirectUrl,
        'macro_agrupacion_data' => $macroAgrupacionData,
    ];
}

function tiendaJerarquiaRenderizarBreadcrumb(array $items): string
{
    $html = '<nav class="taxonomy-breadcrumb" aria-label="Ruta jerárquica"><ol>';

    foreach ($items as $item) {
        $label = e((string) ($item['label'] ?? ''));
        $active = !empty($item['active']);
        $url = trim((string) ($item['url'] ?? ''));

        if ($active || $url === '') {
            $html .= '<li aria-current="page"><span>' . $label . '</span></li>';
            continue;
        }

        $html .= '<li><a href="' . e($url) . '">' . $label . '</a></li>';
    }

    $html .= '</ol></nav>';
    return $html;
}

function tiendaJerarquiaRenderizarTarjeta(string $nivel, array $item, array $contexto): string
{
    $config = tiendaJerarquiaConfiguracionNivel($nivel);
    $titulo = trim((string) ($item['nombre'] ?? ''));
    $descripcion = trim((string) ($item['descripcion_corta'] ?? ''));
    $secondaryValue = trim((string) ($item['secondary_value'] ?? ''));
    $badge = tiendaJerarquiaBadgeReferencias((int) ($item['total'] ?? 0));
    $url = tiendaJerarquiaConstruirUrlCard($nivel, $item, $contexto);
    $media = tiendaJerarquiaRenderMedia($item, $nivel, 'grid');
    $metaHtml = '';

    if ($descripcion === '') {
        $descripcion = $badge;
    }

    if ($secondaryValue !== '') {
        $metaHtml = '<span class="taxonomy-card-meta">' . e($secondaryValue) . '</span>';
    }

    return '
        <article class="taxonomy-card">
            <a class="taxonomy-card-link" href="' . e($url) . '" data-bs-toggle="tooltip" data-bs-placement="top" title="' . e($titulo) . '" aria-label="' . e($titulo) . '">
                <div class="taxonomy-card-media">' . $media . '</div>
                <div class="taxonomy-card-body">
                    <span class="taxonomy-card-kicker">' . e($config['kicker']) . '</span>
                    <h3>' . e((string) ($item['nombre_corto'] ?? $titulo)) . '</h3>
                    ' . $metaHtml . '
                    <p>' . e($descripcion) . '</p>
                    <div class="taxonomy-card-footer">
                        <span class="taxonomy-card-badge">' . e($badge) . '</span>
                        <span class="taxonomy-card-action">' . e((string) $config['card_action']) . '</span>
                    </div>
                </div>
            </a>
        </article>
    ';
}

function tiendaJerarquiaRenderizarBloqueMacroAgrupacion(array $macroItem, array $contexto): string
{
    $titulo = trim((string) ($macroItem['titulo'] ?? ''));
    $descripcion = trim((string) ($macroItem['descripcion'] ?? ''));
    $link = trim((string) ($macroItem['link'] ?? ''));
    $pdf = trim((string) ($macroItem['pdf'] ?? ''));
    $items = $macroItem['items'] ?? [];
    $nivel = (string) ($macroItem['nivel'] ?? 'serie');
    $labelUpper = (string) ($macroItem['label_upper'] ?? 'MACRO');
    $acciones = '';
    $tarjetas = '';

    if ($link !== '') {
        $acciones .= '<a class="taxonomy-btn is-compact" href="' . e($link) . '" target="_blank" rel="noopener noreferrer">Ver más</a>';
    }

    if ($pdf !== '') {
        $acciones .= '<a class="taxonomy-btn is-compact" href="' . e($pdf) . '" target="_blank" rel="noopener noreferrer">PDF técnico</a>';
    }

    foreach ($items as $item) {
        $tarjetas .= tiendaJerarquiaRenderizarTarjeta($nivel, $item, $contexto);
    }

    return '
        <section class="taxonomy-macroseries-block">
            <header class="taxonomy-macroseries-head">
                <div class="taxonomy-macroseries-top">
                    <div>
                        <span class="taxonomy-macroseries-kicker">' . e($labelUpper) . '</span>
                        <h2>' . e($titulo) . '</h2>
                        ' . ($descripcion !== '' ? '<p>' . e($descripcion) . '</p>' : '') . '
                    </div>
                    <span class="taxonomy-card-badge">' . e(tiendaJerarquiaBadgeReferencias(count($items))) . '</span>
                </div>
                ' . ($acciones !== '' ? '<div class="taxonomy-macroseries-actions">' . $acciones . '</div>' : '') . '
            </header>
            <div class="taxonomy-macroseries-grid">' . $tarjetas . '</div>
        </section>
    ';
}

function tiendaJerarquiaRenderizarPagina(array $vista): void
{
    $config = $vista['config'];
    $items = $vista['items'];
    $macroAgrupacionData = $vista['macro_agrupacion_data'] ?? ['macro_items' => [], 'items_libres' => $items, 'label_plural' => '', 'free_label' => ''];
    ?>
    <main class="store-main taxonomy-main">
        <section class="taxonomy-shell">
            <?php echo tiendaJerarquiaRenderizarBreadcrumb($vista['breadcrumb']); ?>

            <header class="taxonomy-hero">
                <div class="taxonomy-hero-copy">
                    <span class="taxonomy-hero-kicker"><?php echo e((string) $config['kicker']); ?></span>
                    <h1><?php echo e((string) $vista['titulo']); ?></h1>
                    <p><?php echo e((string) $vista['descripcion']); ?></p>

                    <div class="taxonomy-hero-stats">
                        <span><?php echo number_format((int) $vista['total_items'], 0, ',', '.'); ?> opciones</span>
                        <span><?php echo number_format((int) $vista['total_referencias'], 0, ',', '.'); ?> referencias relacionadas</span>
                    </div>
                </div>

                <div class="taxonomy-hero-actions">
                    <a class="taxonomy-btn" href="<?php echo e((string) $vista['volver_url']); ?>">Volver</a>
                    <a class="taxonomy-btn" href="<?php echo e((string) $vista['ver_todo_url']); ?>">Ver todo</a>
                    <a class="taxonomy-btn is-primary" href="<?php echo e((string) $vista['productos_url']); ?>">Ver productos</a>
                </div>
            </header>

            <?php if ($vista['contexto_padre'] !== null): ?>
                <div class="taxonomy-context-note">
                    <span><?php echo e((string) $config['context_label']); ?></span>
                    <strong><?php echo e((string) $vista['contexto_padre']['nombre']); ?></strong>
                </div>
            <?php endif; ?>

            <?php if ($items === []): ?>
                <section class="taxonomy-empty">
                    <div class="taxonomy-empty-icon"><i class="fa-solid <?php echo e((string) $config['icon']); ?>"></i></div>
                    <h2><?php echo e((string) $config['empty_title']); ?></h2>
                    <p><?php echo e((string) $config['empty_text']); ?></p>
                </section>
            <?php else: ?>
                <?php if (in_array((string) $vista['nivel'], ['serie', 'grupo'], true) && ($macroAgrupacionData['macro_items'] ?? []) !== []): ?>
                    <section class="taxonomy-macroseries-stack" aria-label="<?php echo e((string) ($macroAgrupacionData['label_plural'] ?? '')); ?>">
                        <?php foreach ($macroAgrupacionData['macro_items'] as $macroItem): ?>
                            <?php echo tiendaJerarquiaRenderizarBloqueMacroAgrupacion($macroItem, $vista['contexto']); ?>
                        <?php endforeach; ?>
                    </section>

                    <?php if (($macroAgrupacionData['items_libres'] ?? []) !== []): ?>
                        <div class="taxonomy-section-label"><?php echo e((string) ($macroAgrupacionData['free_label'] ?? '')); ?></div>
                        <section class="taxonomy-grid" aria-label="<?php echo e((string) ($macroAgrupacionData['free_label'] ?? '')); ?>">
                            <?php foreach ($macroAgrupacionData['items_libres'] as $item): ?>
                                <?php echo tiendaJerarquiaRenderizarTarjeta((string) $vista['nivel'], $item, $vista['contexto']); ?>
                            <?php endforeach; ?>
                        </section>
                    <?php endif; ?>
                <?php else: ?>
                    <section class="taxonomy-grid" aria-label="<?php echo e((string) $config['title']); ?>">
                        <?php foreach ($items as $item): ?>
                            <?php echo tiendaJerarquiaRenderizarTarjeta((string) $vista['nivel'], $item, $vista['contexto']); ?>
                        <?php endforeach; ?>
                    </section>
                <?php endif; ?>
            <?php endif; ?>
        </section>
    </main>
    <?php
}

function tiendaJerarquiaObtenerCatalogosCarrusel(mysqli $mysqli, int $limite = 14): array
{
    $limite = max(6, min($limite, 20));

    $sql = "
        SELECT
            ca.ID_catalogo AS id,
            ca.categoria_id AS categoria_id,
            COALESCE(NULLIF(TRIM(ca.catalogo), ''), CONCAT('Catálogo ', ca.ID_catalogo)) AS nombre,
            COALESCE(NULLIF(TRIM(ca.imagen_catalogo), ''), '') AS imagen,
            COALESCE(conteo.total, 0) AS total
        FROM pro2_catalogo ca
        LEFT JOIN (
            SELECT
                ID_catalogo,
                COUNT(*) AS total
            FROM pro_articulos
            WHERE estatus_eliminado = 0
              AND activo = 1
              AND ID_catalogo IS NOT NULL
              AND ID_catalogo <> 0
            GROUP BY ID_catalogo
        ) conteo ON conteo.ID_catalogo = ca.ID_catalogo
        WHERE ca.estatus_eliminado = 0
          AND COALESCE(ca.estado, 1) = 1
          AND ca.ID_catalogo IS NOT NULL
          AND ca.ID_catalogo <> 0
        ORDER BY
            CASE WHEN COALESCE(conteo.total, 0) > 0 THEN 0 ELSE 1 END,
            COALESCE(conteo.total, 0) DESC,
            nombre ASC
        LIMIT {$limite}
    ";

    $items = tiendaFetchAll($mysqli, $sql);

    foreach ($items as &$item) {
        $item['id'] = (int) ($item['id'] ?? 0);
        $item['categoria_id'] = (int) ($item['categoria_id'] ?? 0);
        $item['nombre'] = limpiarTexto($item['nombre'] ?? '', 160);
        $item['imagen'] = trim((string) ($item['imagen'] ?? ''));
        $item['imagen_url'] = tiendaJerarquiaResolverImagenUrl($item['imagen']);
        $item['total'] = (int) ($item['total'] ?? 0);
        $item['url'] = tiendaJerarquiaUrl('tienda-familias.php', array_filter([
            'via' => (int) ($item['id'] ?? 0),
            'categoria' => (int) ($item['categoria_id'] ?? 0),
            'catalogo' => (int) ($item['id'] ?? 0),
        ]));
    }
    unset($item);

    return $items;
}

function tiendaJerarquiaObtenerCategoriasCarrusel(mysqli $mysqli, int $limite = 14): array
{
    $limite = max(6, min($limite, 20));

    $sql = "
        SELECT
            c.ID_categoria AS id,
            COALESCE(NULLIF(TRIM(c.categoria), ''), CONCAT('Categoría ', c.ID_categoria)) AS nombre,
            COALESCE(NULLIF(TRIM(c.imagen_categoria), ''), '') AS imagen,
            COALESCE(conteo.total, 0) AS total
        FROM pro1_categoria c
        LEFT JOIN (
            SELECT
                ID_categoria,
                COUNT(*) AS total
            FROM pro_articulos
            WHERE estatus_eliminado = 0
              AND activo = 1
              AND ID_categoria IS NOT NULL
              AND ID_categoria <> 0
            GROUP BY ID_categoria
        ) conteo ON conteo.ID_categoria = c.ID_categoria
        WHERE c.estatus_eliminado = 0
          AND COALESCE(c.estado, 1) = 1
          AND c.ID_categoria IS NOT NULL
          AND c.ID_categoria <> 0
        ORDER BY
            CASE WHEN COALESCE(conteo.total, 0) > 0 THEN 0 ELSE 1 END,
            COALESCE(conteo.total, 0) DESC,
            nombre ASC
        LIMIT {$limite}
    ";

    $items = tiendaFetchAll($mysqli, $sql);

    foreach ($items as &$item) {
        $item['id'] = (int) ($item['id'] ?? 0);
        $item['nombre'] = limpiarTexto($item['nombre'] ?? '', 160);
        $item['imagen'] = trim((string) ($item['imagen'] ?? ''));
        $item['imagen_url'] = tiendaJerarquiaResolverImagenUrl($item['imagen']);
        $item['total'] = (int) ($item['total'] ?? 0);
        $item['url'] = tiendaJerarquiaUrl('tienda-catalogos.php', [
            'via' => (int) ($item['id'] ?? 0),
            'categoria' => (int) ($item['id'] ?? 0),
        ]);
    }
    unset($item);

    return $items;
}
