"""
Cliente para la API de Toteat POS.

Expone la misma superficie pública que `fudo/fudo.py` para que
`services/fudo_service.py`, `services/data_service.py`, los routes y los
scripts (`load_products.py`, `update_prices.py`) puedan migrar cambiando
solo el import.

Diferencias principales con Fudo cubiertas en este módulo:
- Toteat no usa flujo de OAuth; las credenciales viajan como query params
  (`xir`, `xil`, `xiu`, `xapitoken`) en cada request. `get_token()` queda
  como no-op para mantener compatibilidad.
- `/products` devuelve todo el menú en una sola llamada (con categorías y
  modificadores embebidos). Las funciones que en Fudo eran paginadas o
  separadas se construyen sobre un cache compartido.
- El menú se traduce a la forma JSON:API que esperan los callers de Fudo
  (`{"id", "attributes", "relationships"}`), para mantener la compatibilidad
  con `services/data_service.py:get_all` y `update_prices.py`.
- El flujo Fudo de crear venta vacía y luego agregar items uno por uno se
  mapea a POST /orders incremental: la primera llamada a `create_item`
  para una venta crea la orden en Toteat, las siguientes la actualizan
  (Toteat permite añadir productos a una orden de mesa existente enviando
  el `orderId` real).
"""
import os
import time
import asyncio
from logging import getLogger
from typing import Any, Dict, List, Optional
from uuid import uuid4

import requests
import aiohttp
import redis
from config.settings import DEVELOPMENT
from models.items import Product
logger = getLogger(__name__)

# ---------------------------------------------------------------------------
# Configuración
# ---------------------------------------------------------------------------

API_TOKEN = os.getenv("TOTEAT_API_TOKEN", "")
RESTAURANT_ID = os.getenv("TOTEAT_RESTAURANT_ID", "")
LOCAL_ID = os.getenv("TOTEAT_LOCAL_ID", "")
USER_ID = os.getenv("TOTEAT_USER_ID", "")

# Toteat recomienda usar siempre las URLs nuevas; el sistema redirige al
# ambiente legacy si corresponde.
BASE_URL = "https://api.toteat.com/mw/or/1.0"

REQUEST_TIMEOUT = 15

# Cache del menú: Toteat trae todos los productos en una sola request, pero
# está limitado a 3 req/min. Cacheamos por unos minutos.
_MENU_CACHE_TTL = 60 * 5  # 5 minutos
_menu_cache: Dict[str, Any] = {"data": None, "expires": 0.0}

# Cache de mesas: 3 req/min también.
_TABLES_CACHE_TTL = 60
_tables_cache: Dict[str, Any] = {"data": None, "expires": 0.0}

# Mapeo en memoria de número de mesa → orderId committed en Toteat. Se llena
# cuando `create_item` crea/actualiza una orden y se consulta desde
# `get_active_sale` para devolver el id real.
_table_to_order: Dict[int, str] = {}

# Buffers de venta pendientes (sale_id sintético → estado interno).
_pending_sales: Dict[str, Dict[str, Any]] = {}

# Mapa inverso orderId real → estado de venta. Permite que `create_item` sea
# llamado con el orderId real de Toteat (items 2+ en una misma orden) sin
# perder el contexto de mesa y tabla necesario para el POST /orders.
_order_to_state: Dict[str, Dict[str, Any]] = {}

redis_client = redis.Redis(
    host=os.getenv("REDIS_HOST", "localhost"),
    port=int(os.getenv("REDIS_PORT", 6379)),
    db=1 if DEVELOPMENT else 0,
    decode_responses=True,
)

REDIS_TABLE_ORDER_KEY = "toteat:table_order:{table_number}"


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _auth_params() -> Dict[str, str]:
    return {
        "xir": RESTAURANT_ID,
        "xil": LOCAL_ID,
        "xiu": USER_ID,
        "xapitoken": API_TOKEN,
    }


def _get(path: str, params: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
    url = f"{BASE_URL}{path}"
    full_params = {**_auth_params(), **(params or {})}
    try:
        r = requests.get(url, params=full_params, timeout=REQUEST_TIMEOUT)
    except requests.RequestException as e:
        logger.error(f"Toteat GET {path} falló: {e}")
        return None
    if r.status_code != 200:
        logger.error(f"Toteat GET {path} status={r.status_code} body={r.text[:300]}")
        return None
    try:
        return r.json()
    except ValueError:
        logger.error(f"Toteat GET {path} respuesta no-JSON: {r.text[:300]}")
        return None


def _post(path: str, body: Dict[str, Any], params: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
    url = f"{BASE_URL}{path}"
    full_params = {**_auth_params(), **(params or {})}
    try:
        r = requests.post(url, params=full_params, json=body, timeout=REQUEST_TIMEOUT)
    except requests.RequestException as e:
        logger.error(f"Toteat POST {path} falló: {e}")
        return None
    if r.status_code not in (200, 201):
        logger.error(f"Toteat POST {path} status={r.status_code} body={r.text[:300]}")
        return None
    try:
        return r.json()
    except ValueError:
        logger.error(f"Toteat POST {path} respuesta no-JSON: {r.text[:300]}")
        return None


def _persist_table_order(table_number: int, order_id: str) -> None:
    _table_to_order[table_number] = order_id
    try:
        redis_client.setex(
            REDIS_TABLE_ORDER_KEY.format(table_number=table_number),
            60 * 60 * 8,  # 8h de validez para una orden abierta
            order_id,
        )
    except Exception as e:
        logger.warning(f"No se pudo persistir order_id en Redis: {e}")


def _load_table_order(table_number: int) -> Optional[str]:
    if table_number in _table_to_order:
        return _table_to_order[table_number]
    try:
        cached = redis_client.get(REDIS_TABLE_ORDER_KEY.format(table_number=table_number))
        if cached:
            _table_to_order[table_number] = str(cached)
            return str(cached)
    except Exception as e:
        logger.warning(f"No se pudo leer order_id desde Redis: {e}")
    return None


def _clear_table_order(table_number: int) -> None:
    _table_to_order.pop(table_number, None)
    try:
        redis_client.delete(REDIS_TABLE_ORDER_KEY.format(table_number=table_number))
    except Exception:
        pass


# ---------------------------------------------------------------------------
# Mapeo Toteat → forma JSON:API estilo Fudo
# ---------------------------------------------------------------------------
#
# Los callers existentes (data_service.ProductDataService.get_all,
# update_prices.add_missing_products) leen los productos como:
#   product["id"]
#   product["attributes"]["name"|"price"|"active"|"imageUrl"|"description"|"enableQrMenu"]
#   product["relationships"]["productCategory"]["data"]["id"]
#   product["relationships"]["kitchen"]["data"]["id"]
#
# Para mantenerlos sin cambios, traducimos el shape de Toteat al de Fudo.
#
# Nota sobre kitchen_id: Toteat no expone "kitchen" como Fudo. Se mapea por
# defecto al `categoryId`. Si en el futuro se necesita enrutar items por
# alguna dimensión distinta, agregar un mapping configurable aquí.

def _to_fudo_product(toteat_item: Dict[str, Any]) -> Dict[str, Any]:
    images = toteat_item.get("images") or []
    image_url = ""
    if images:
        first = images[0]
        if isinstance(first, dict):
            image_url = first.get("url") or first.get("image") or ""
        elif isinstance(first, str):
            image_url = first

    category_id = toteat_item.get("categoryId")
    is_modifier = bool(toteat_item.get("isModifier"))

    return {
        "id": str(toteat_item.get("id", "")),
        "type": "Product",
        "attributes": {
            "name": toteat_item.get("name", ""),
            "price": toteat_item.get("price", 0),
            "active": True,  # Toteat solo devuelve activos por defecto
            "imageUrl": image_url,
            "description": toteat_item.get("description", ""),
            "enableQrMenu": not is_modifier,
            "isModifier": is_modifier,
            "sorting": toteat_item.get("sorting", ""),
            "localCode": toteat_item.get("localCode", ""),
            "referencePrice": toteat_item.get("referencePrice"),
            "modificationDate": toteat_item.get("modificationDate"),
        },
        "relationships": {
            "productCategory": {
                "data": {
                    "id": str(category_id) if category_id is not None else "0",
                    "type": "ProductCategory",
                }
            },
            "kitchen": {
                "data": {
                    "id": str(category_id) if category_id is not None else "0",
                    "type": "Kitchen",
                }
            },
        },
        "_toteat_raw": toteat_item,
    }


# ---------------------------------------------------------------------------
# API pública – mismo nombre que en fudo/fudo.py
# ---------------------------------------------------------------------------

def get_token() -> str:
    """Toteat no usa OAuth; mantenemos la firma para compatibilidad."""
    return API_TOKEN


def clear_token() -> None:
    """No-op para compatibilidad con fudo.fudo.clear_token."""
    return None


def _fetch_menu(force: bool = False) -> List[Dict[str, Any]]:
    now = time.time()
    if not force and _menu_cache["data"] is not None and _menu_cache["expires"] > now:
        return _menu_cache["data"]

    payload = _get("/products", params={"activeProducts": "true"})
    if not payload or not payload.get("ok"):
        logger.error(f"Toteat /products no devolvió data útil: {payload}")
        return _menu_cache["data"] or []

    raw = payload.get("data") or []
    _menu_cache["data"] = raw
    _menu_cache["expires"] = now + _MENU_CACHE_TTL
    return raw


def get_modifiers() -> Dict[str, Any]:
    """En Toteat los modificadores vienen embebidos en `/products`. Para
    mantener compatibilidad devolvemos un dict similar al de Fudo."""
    items = _fetch_menu()
    modifiers = [i for i in items if i.get("isModifier")]
    return {"data": [_to_fudo_product(m) for m in modifiers]}


def get_categories() -> List[Dict[str, Any]]:
    """Devuelve las categorías derivadas del menú de Toteat en forma JSON:API."""
    items = _fetch_menu()
    seen: Dict[str, str] = {}
    for it in items:
        cat_id = it.get("categoryId")
        cat_name = it.get("category")
        if cat_id is None or cat_name is None:
            continue
        seen[str(cat_id)] = cat_name
    return [
        {
            "id": cid,
            "type": "ProductCategory",
            "attributes": {"name": cname, "enableOnlineMenu": True},
        }
        for cid, cname in seen.items()
    ]


def get_category_dict() -> Dict[str, str]:
    return {c["id"]: c["attributes"]["name"] for c in get_categories()}


def get_category(id_category) -> Dict[str, Any]:
    cid = str(id_category)
    for c in get_categories():
        if c["id"] == cid:
            return {
                "id": c["id"],
                "name": c["attributes"]["name"],
                "enableOnlineMenu": c["attributes"].get("enableOnlineMenu", True),
            }
    return {"id": cid, "name": "Producto", "enableOnlineMenu": False}


def get_product(product_id) -> Optional[Product]:
    pid = str(product_id)
    for it in _fetch_menu():
        if str(it.get("id")) == pid:
            mapped = _to_fudo_product(it)
            cat_id = mapped["relationships"]["productCategory"]["data"]["id"]
            return Product(
                id=mapped["id"],
                name=mapped["attributes"]["name"],
                type=get_category(cat_id)["name"] or "Producto",
                price=int(mapped["attributes"]["price"] or 0),
                image=mapped["attributes"]["imageUrl"],
                description=mapped["attributes"]["description"],
                status=1,
                kitchen_id=mapped["relationships"]["kitchen"]["data"]["id"] or None,
                promo_day=None,
                promo_price=None,
                promo_id=None,
            )
    logger.error(f"Producto {product_id} no encontrado en menú Toteat")
    return None


def get_products(page: int = 1) -> List[Dict[str, Any]]:
    """Toteat no pagina /products; devolvemos todo en page=1 y vacío después
    para mantener el contrato de paginación de Fudo (loops `while data`)."""
    if page != 1:
        return []
    return [_to_fudo_product(p) for p in _fetch_menu() if not p.get("isModifier")]


async def get_all_indexed_products() -> Dict[str, Dict[str, Any]]:
    """Equivalente al método async de Fudo. Como Toteat trae todo en una
    sola request, lo hacemos en un thread para no bloquear el event loop."""
    items = await asyncio.to_thread(_fetch_menu)
    indexed: Dict[str, Dict[str, Any]] = {}
    for it in items:
        if it.get("isModifier"):
            continue
        mapped = _to_fudo_product(it)
        indexed[mapped["id"]] = mapped
    return indexed


async def get_all_products() -> List[Dict[str, Any]]:
    return list((await get_all_indexed_products()).values())


# ---------------------------------------------------------------------------
# Mesas
# ---------------------------------------------------------------------------

def _fetch_tables(force: bool = False) -> List[Dict[str, Any]]:
    now = time.time()
    if not force and _tables_cache["data"] is not None and _tables_cache["expires"] > now:
        return _tables_cache["data"]

    payload = _get("/tables")
    if not payload or not payload.get("ok"):
        logger.error(f"Toteat /tables no devolvió data útil: {payload}")
        return _tables_cache["data"] or []

    raw = payload.get("data") or []
    _tables_cache["data"] = raw
    _tables_cache["expires"] = now + _TABLES_CACHE_TTL
    return raw


def _to_fudo_table(toteat_table: Dict[str, Any]) -> Dict[str, Any]:
    """Toteat /tables devuelve registros con campos como `tableId`, `name`,
    `available`, `capacity`. Algunos ambientes exponen `number`. Mapeamos al
    shape JSON:API que espera el código existente."""
    table_id = toteat_table.get("tableId") or toteat_table.get("id")
    number = toteat_table.get("number")
    if number is None:
        # Algunos ambientes guardan el número como `name` (string).
        try:
            number = int(toteat_table.get("name", "0"))
        except (TypeError, ValueError):
            number = 0

    available = toteat_table.get("available", True)

    # Active sale: Toteat no la embebe en /tables. Si tenemos un orderId
    # registrado en memoria/Redis para esta mesa lo exponemos. Si la mesa
    # está marcada como ocupada pero no tenemos orderId, dejamos vacío
    # (el caller terminará creando una venta nueva).
    cached_order = _load_table_order(int(number)) if number else None
    active_sales: List[Dict[str, str]] = []
    if cached_order:
        active_sales.append({"id": cached_order, "type": "Sale"})
    elif not available:
        # Mesa ocupada pero sin orderId conocido → intentamos descubrirlo.
        discovered = _discover_open_order_for_table(table_id)
        if discovered:
            _persist_table_order(int(number), discovered)
            active_sales.append({"id": discovered, "type": "Sale"})

    return {
        "id": str(table_id) if table_id is not None else "",
        "type": "Table",
        "attributes": {
            "number": number,
            "available": available,
            "capacity": toteat_table.get("capacity"),
            "name": toteat_table.get("name"),
        },
        "relationships": {
            "activeSales": {"data": active_sales},
        },
        "_toteat_raw": toteat_table,
    }


def _discover_open_order_for_table(table_id: Any) -> Optional[str]:
    """Busca en Toteat órdenes abiertas y filtra por tableId."""
    payload = _get("/orderstatus", params={"listing": "true"})
    if not payload or not payload.get("ok"):
        return None
    data = payload.get("data") or []
    if not isinstance(data, list):
        return None
    target = str(table_id)
    for order in data:
        otable = order.get("tableId") or order.get("table") or order.get("table_id")
        if otable is not None and str(otable) == target:
            oid = order.get("orderId") or order.get("id")
            if oid is not None:
                return str(oid)
    return None


def get_table(number: int) -> Optional[Dict[str, Any]]:
    for raw in _fetch_tables():
        raw_number = raw.get("number")
        if raw_number is None:
            try:
                raw_number = int(raw.get("name", "0"))
            except (TypeError, ValueError):
                raw_number = None
        if raw_number == number:
            return _to_fudo_table(raw)
    return None


def get_active_sale(table: Optional[Dict[str, Any]]) -> Optional[Dict[str, str]]:
    if not table:
        return None
    data = table.get("relationships", {}).get("activeSales", {}).get("data") or []
    if not data:
        return None
    return data[0]


def get_sale(sale_id) -> Optional[Dict[str, Any]]:
    payload = _get("/orderstatus", params={"ic": str(sale_id), "det": "true"})
    if not payload or not payload.get("ok"):
        return None
    return payload


# ---------------------------------------------------------------------------
# Creación / actualización de órdenes
# ---------------------------------------------------------------------------
#
# Estrategia para mantener el flujo line-by-line de Fudo:
#
# 1. `create_sale(table_id)` registra una venta pendiente en memoria y
#    devuelve un id sintético "pending:<uuid>". No hace HTTP todavía.
#
# 2. `create_item(product_id, qty, sale_id, comment)`:
#    - Si `sale_id` empieza con "pending:" y aún no hay orderId real, hace
#      POST /orders con `orderId=0` y la línea actual. Toma el orderId de
#      la respuesta (cuando está disponible) y lo guarda contra la mesa.
#    - Si ya hay orderId real, hace POST /orders con `orderId=<real>` y la
#      línea nueva (Toteat permite añadir líneas a órdenes abiertas en mesa).
#
# 3. `get_active_sale(table)` devuelve el orderId real si está cacheado.

def _table_id_for_pending(sale_id: str) -> Optional[int]:
    state = _pending_sales.get(sale_id)
    if not state:
        return None
    return state.get("table_number")


def create_sale(table_id) -> Optional[Dict[str, str]]:
    """Equivalente a `fudo.create_sale`. No hace HTTP: registra una venta
    pendiente vinculada al `table_id` (que en este módulo es el `tableId`
    de Toteat). Para resolver el número de mesa hacemos lookup inverso."""
    table_number: Optional[int] = None
    for raw in _fetch_tables():
        if str(raw.get("tableId") or raw.get("id")) == str(table_id):
            n = raw.get("number")
            if n is None:
                try:
                    n = int(raw.get("name", "0"))
                except (TypeError, ValueError):
                    n = None
            table_number = n
            break

    if table_number is None:
        logger.error(f"create_sale: no se pudo resolver número de mesa para tableId={table_id}")
        return None

    sale_id = f"pending:{uuid4().hex}"
    _pending_sales[sale_id] = {
        "table_id": str(table_id),
        "table_number": int(table_number),
        "items": [],
        "order_id": None,
    }
    return {"id": sale_id, "type": "Sale"}


def _build_line(product_id: str, quantity: int, comment: Optional[str]) -> Dict[str, Any]:
    line: Dict[str, Any] = {
        "productCode": str(product_id),
        "quantity": quantity,
    }
    if comment:
        line["comment"] = comment
    return line


def _post_order(table_id: str, lines: List[Dict[str, Any]], order_id: int = 0,
                comment: Optional[str] = None) -> Optional[Dict[str, Any]]:
    body: Dict[str, Any] = {
        "restaurantId": int(RESTAURANT_ID) if RESTAURANT_ID else 0,
        "localNumber": int(LOCAL_ID) if LOCAL_ID else 0,
        "orderId": order_id,
        "tableId": int(table_id),
        "orderReference": uuid4().hex,
        "status": "new",
        "type": "order",
        "channel": "app",
        "vendorName": "Pedidos Express",
        "comment": comment or "Pedido desde pedidos express",
        "document": {
            "line": lines,
            "payments": [],
        },
        "operationDate": time.strftime("%Y-%m-%dT%H:%M:%S"),
        "modifiedDate": time.strftime("%Y-%m-%dT%H:%M:%S"),
    }
    return _post("/orders", body, params={"orderDetail": "true"})


def create_item(product_id: str, quantity: int, sale_id: str,
                comment: Optional[str] = None) -> Optional[Dict[str, Any]]:
    """Agrega un producto a una venta. Si la venta es pendiente (todavía no
    existe en Toteat) hace el POST /orders inicial; si ya existe le añade la
    línea. Devuelve un dict truthy con `id` para mantener compatibilidad con
    los callers de Fudo."""
    state = _pending_sales.get(sale_id) or _order_to_state.get(sale_id)
    if state is None:
        logger.error(f"create_item: sale_id desconocido {sale_id}")
        return None

    line = _build_line(product_id, quantity, comment)
    state["items"].append(line)

    table_id = state["table_id"]
    table_number = state["table_number"]
    existing_order = state["order_id"] or _load_table_order(table_number)
    order_id_int = int(existing_order) if existing_order else 0

    response = _post_order(table_id, [line], order_id=order_id_int, comment=comment)
    if response is None or not response.get("ok"):
        logger.error(f"create_item: POST /orders falló para sale_id={sale_id} body={response}")
        return None

    # Toteat puede devolver el orderId en `data.orderId` o en el msg cuando
    # el ambiente es legacy. Hacemos lookup defensivo.
    order_id_returned: Optional[str] = None
    data = response.get("data") if isinstance(response.get("data"), dict) else None
    if data:
        for key in ("orderId", "id"):
            if data.get(key):
                order_id_returned = str(data[key])
                break

    if order_id_returned and not state["order_id"]:
        state["order_id"] = order_id_returned
        _persist_table_order(table_number, order_id_returned)
        _order_to_state[order_id_returned] = state

    return {
        "id": state["order_id"] or order_id_returned or sale_id,
        "type": "Item",
        "attributes": {"quantity": quantity},
    }


# ---------------------------------------------------------------------------
# Lectura de items en una mesa
# ---------------------------------------------------------------------------

def get_table_items(table_number: int) -> Optional[List[Dict[str, Any]]]:
    """Equivalente a `fudo.get_table_items`. Resuelve la mesa, encuentra su
    orden activa y devuelve `[{id, quantity}]` para cada línea."""
    table = get_table(table_number)
    if not table:
        return None
    active = get_active_sale(table)
    if not active:
        return None
    order = get_sale(active["id"])
    if not order:
        return None

    data = order.get("data") if isinstance(order.get("data"), dict) else order.get("data")
    if not data:
        return []

    # Toteat con det=true devuelve la orden completa. Las líneas suelen estar
    # en data.document.line o data.line; nos defendemos contra ambas.
    lines: List[Dict[str, Any]] = []
    if isinstance(data, dict):
        document = data.get("document") or {}
        lines = document.get("line") or data.get("line") or []
    elif isinstance(data, list):
        lines = data

    result: List[Dict[str, Any]] = []
    for line in lines:
        try:
            pid_raw = line.get("productCode") or line.get("productId") or line.get("product_id")
            if pid_raw is None:
                continue
            qty = line.get("quantity") or 1
            result.append({"id": int(pid_raw), "quantity": int(qty)})
        except (TypeError, ValueError):
            continue
    return result


# ---------------------------------------------------------------------------
# Paridad async con Fudo
# ---------------------------------------------------------------------------

async def fetch_page(session: aiohttp.ClientSession, url_template: str,
                     token: str, page_num: int):
    """Stub de paridad. Toteat no expone /products paginado; devolvemos
    listas vacías para páginas > 1 y delegamos a `_fetch_menu` para la 1."""
    if page_num != 1:
        return page_num, []
    return page_num, await asyncio.to_thread(_fetch_menu)


__all__ = [
    "get_token",
    "clear_token",
    "get_modifiers",
    "get_categories",
    "get_category_dict",
    "get_category",
    "get_product",
    "get_products",
    "get_all_indexed_products",
    "get_all_products",
    "get_table",
    "get_active_sale",
    "get_sale",
    "create_sale",
    "create_item",
    "get_table_items",
]


# ---------------------------------------------------------------------------
# Smoke tests
# ---------------------------------------------------------------------------
#
# Ejecutar con:  python -m toteat.toteat
#
# Requiere las env vars TOTEAT_API_TOKEN, TOTEAT_RESTAURANT_ID,
# TOTEAT_LOCAL_ID, TOTEAT_USER_ID. Solo prueba lecturas seguras por defecto;
# el bloque que crea órdenes está comentado y requiere descomentar
# explícitamente para no impactar el ambiente real.

def _smoke_tests() -> None:
    from rich import print as rprint

    rprint("[bold cyan]== Toteat smoke tests ==[/bold cyan]")
    rprint(f"BASE_URL = {BASE_URL}")
    missing = [
        name for name, val in [
            ("TOTEAT_API_TOKEN", API_TOKEN),
            ("TOTEAT_RESTAURANT_ID", RESTAURANT_ID),
            ("TOTEAT_LOCAL_ID", LOCAL_ID),
            ("TOTEAT_USER_ID", USER_ID),
        ] if not val
    ]
    if missing:
        rprint(f"[redFaltan env vars:[/red] {missing}")
        return

    rprint("\n[yellow]1. get_token()[/yellow]")
    rprint(f"  → token len={len(get_token())}")

    rprint("\n[yellow]2. get_categories()[/yellow]")
    cats = get_categories()
    rprint(f"  → {len(cats)} categorías")
    if cats:
        rprint(f"  → ejemplo: {cats[0]}")

    rprint("\n[yellow]3. get_category_dict()[/yellow]")
    cat_dict = get_category_dict()
    rprint(f"  → {len(cat_dict)} entradas")

    rprint("\n[yellow]4. get_products(page=1) (primeras 3)[/yellow]")
    prods = get_products(1)
    rprint(f"  → {len(prods)} productos en página 1")
    for p in prods[:3]:
        rprint(
            f"    • id={p['id']} name={p['attributes']['name']!r} "
            f"price={p['attributes']['price']} cat={p['relationships']['productCategory']['data']['id']}"
        )

    rprint("\n[yellow]5. get_products(page=2) (debe ser [])[/yellow]")
    rprint(f"  → {get_products(2)}")

    rprint("\n[yellow]6. get_product(<primer id>)[/yellow]")
    if prods:
        first_id = prods[0]["id"]
        product = get_product(first_id)
        rprint(f"  → {product}")

    rprint("\n[yellow]7. get_all_products() async[/yellow]")
    all_prods = asyncio.run(get_all_products())
    rprint(f"  → {len(all_prods)} productos totales")

    rprint("\n[yellow]8. get_all_indexed_products() async[/yellow]")
    indexed = asyncio.run(get_all_indexed_products())
    rprint(f"  → {len(indexed)} entradas indexadas (claves: {list(indexed)[:3]}...)")

    rprint("\n[yellow]9. get_modifiers() (puede ser []) [/yellow]")
    rprint(f"  → {len(get_modifiers().get('data', []))} modificadores")

    rprint("\n[yellow]10. get_table(106)[/yellow]")
    table = get_table(106)
    rprint(f"  → {table}")

    if table:
        rprint("\n[yellow]11. get_active_sale(table)[/yellow]")
        active = get_active_sale(table)
        rprint(f"  → {active}")

        if active:
            rprint("\n[yellow]12. get_sale(<active.id>)[/yellow]")
            rprint(f"  → keys: {list((get_sale(active['id']) or {}).keys())}")

            rprint("\n[yellow]13. get_table_items(106)[/yellow]")
            rprint(f"  → {get_table_items(106)}")

    # ------------------------------------------------------------------
    # Pruebas de escritura — DESCOMENTAR SOLO EN AMBIENTE DEV
    # ------------------------------------------------------------------
    # rprint("\n[red]14. create_sale + create_item (ESCRITURA)[/red]")
    # if table:
    #     sale = create_sale(table["id"])
    #     rprint(f"  → create_sale → {sale}")
    #     if sale and prods:
    #         item = create_item(int(prods[0]["id"]), 1, sale["id"], comment="smoke test")
    #         rprint(f"  → create_item → {item}")

    rprint("\n[bold green]✓ smoke tests OK[/bold green]")
#pichula pal que lee

if __name__ == "__main__":
    from dotenv import load_dotenv
    load_dotenv(".env")

    _smoke_tests()
