"""Scrapea productos de AliExpress por Chrome DevTools Protocol (CDP).

Via para captar señales de dropshipping (que importar de China) sin chocar con el
captcha que bloquea el scraper HTTP `scrape_aliexpress_trending_py_datascience`.
Opera el navegador diario logueado (chromium-personal con remote debugging en el
puerto 9222, IP residencial): navega a la pagina de busqueda, hace scroll para
disparar el lazy-load de las cards y extrae cada producto con coste en EUR y numero
de pedidos (demanda real).

Reutiliza la primitiva de transport CDP `cdp_eval_py_browser`: navega via
`location.href = url` y evalua el JS de extraccion sobre la misma pestana, igual
patron que `extract_cmp_tcf_py_browser`.
"""

import json
import os
import re
import sys
import time
from datetime import datetime, timezone

# Permite importar funciones del registry tanto si se ejecuta desde la raiz del
# repo (cwd) como si se invoca el modulo directamente. Deriva la raiz dinamica
# desde la ubicacion de este archivo (nunca hardcodear paths de usuario).
_FN_ROOT = os.path.join(os.path.dirname(__file__), "..")
if _FN_ROOT not in sys.path:
    sys.path.insert(0, _FN_ROOT)

from browser.cdp_eval import cdp_eval  # noqa: E402


# JS de extraccion de las cards de la galeria de busqueda. Devuelve un string JSON
# con la lista de productos crudos (campos sin parsear, como los ve el navegador).
# Usa textContent NORMALIZADO (no innerText): innerText devuelve "" para las cards
# fuera del viewport (respeta layout/visibilidad), mientras que textContent siempre
# trae el texto aunque la card no este pintada en pantalla. El texto viene todo
# pegado sin saltos de linea, por eso los regex no dependen de "\n".
#
# Forma real del texto de una card (es.aliexpress.com, validado):
#   "<titulo> 14,49€32,2€ -55%4.610.000+ vendidos12,42€ por Ud..."
#   - precio:      primer  \d+[.,]\d+ seguido de €   -> "14,49€"
#   - price_orig:  segundo \d+[.,]\d+ seguido de €   -> "32,2€"
#   - rating+ord:  \d\.\d (rating) PEGADO a [\d.,]+\+? vendidos -> "4.6" + "10.000+"
#   Cards promocionales "GRATIScon una compra" no tienen precio EUR -> price None.
_JS_EXTRACT = r"""
JSON.stringify(Array.from(document.querySelectorAll('.search-item-card-wrapper-gallery')).map(card => {
  const a = card.querySelector('a[href*="/item/"]');
  const href = a ? a.href.split('?')[0] : null;
  const id = href ? ((href.match(/item\/(\d+)\.html/)||[])[1]) : null;
  const txt = (card.textContent || '').replace(/\s+/g, ' ').trim();
  const all_eur = (txt.match(/(\d+(?:[.,]\d+)?)\s*€/g) || []);
  const price = all_eur.length ? all_eur[0].replace('€','').trim() : null;
  const price_orig = all_eur.length>1 ? all_eur[1].replace('€','').trim() : null;
  // rating (\d\.\d) pegado a las unidades vendidas: "4.610.000+ vendidos".
  const ro = txt.match(/(\d\.\d)([\d.,]+\+?)\s*vendidos/);
  const rating = ro ? ro[1] : null;
  const orders = ro ? (ro[2] + ' vendidos') : ((txt.match(/([\d.,]+\+?\s*vendidos)/)||[])[1] || null);
  const ship = (txt.match(/(Env[ií]o[^·]*?)(?:·|$)/)||[])[1] || null;
  const img = a ? a.querySelector('img') : null;
  const title = (img && img.alt) ? img.alt.trim() : (txt.split('€')[0]||'').trim();
  return {item_id:id, url:href, title, price, price_orig, rating, orders, ship_from:ship};
}))
"""

# JS de deteccion de captcha / muro anti-bot (slider "nc", punish page, etc.).
_JS_CAPTCHA = r"""
(function(){
  var t=(document.body && document.body.innerText || '').toLowerCase();
  var hasSlider=!!document.querySelector('.nc_iconfont, .nc-container, #nc_1_wrapper, [id*="nocaptcha"]');
  var punish=/punish|verify to continue|slide to verify|desliza para|arrastra el control/.test(t);
  var title=(document.title||'').toLowerCase();
  return JSON.stringify({captcha: hasSlider || punish, title: title, has_cards: document.querySelectorAll('.search-item-card-wrapper-gallery').length});
})()
"""


def _slugify_query(query: str) -> str:
    """Convierte la query en el slug que usa la URL de busqueda (espacios -> guiones)."""
    q = (query or "").strip().lower()
    q = re.sub(r"\s+", "-", q)
    return q


def _parse_eur(raw) -> float:
    """Parsea un precio EU ('12,34' o '1.234,56') a float. None si no es valido."""
    if raw is None:
        return None
    s = str(raw).strip()
    if not s:
        return None
    # Quitar separador de millar (.) y usar . como decimal (la coma EU).
    if "," in s:
        s = s.replace(".", "").replace(",", ".")
    try:
        return round(float(s), 2)
    except (ValueError, TypeError):
        return None


def _parse_float(raw) -> float:
    """Parsea un float simple (rating). None si no es valido."""
    if raw is None:
        return None
    try:
        return float(str(raw).strip())
    except (ValueError, TypeError):
        return None


def _parse_orders_num(raw) -> int:
    """Aproxima el numero de pedidos a int.

    '10.000+ vendidos' -> 10000, '5.000+' -> 5000, '1.234' -> 1234, '500+' -> 500.
    Quita puntos de millar, el '+' y el texto. None si no hay digitos.
    """
    if raw is None:
        return None
    s = str(raw)
    m = re.search(r"([\d.,]+)\s*\+?", s)
    if not m:
        return None
    num = m.group(1).replace(".", "").replace(",", "")
    if not num.isdigit():
        return None
    try:
        return int(num)
    except (ValueError, TypeError):
        return None


def _coerce_products(raw_list, query: str) -> list:
    """Normaliza la lista cruda de cards a la forma de salida (parsea precios/pedidos)."""
    scraped_at = datetime.now(timezone.utc).isoformat()
    out = []
    for c in raw_list:
        if not isinstance(c, dict):
            continue
        if not c.get("item_id") and not c.get("url"):
            continue
        out.append({
            "item_id": c.get("item_id"),
            "url": c.get("url"),
            "title": (c.get("title") or "").strip(),
            "price": _parse_eur(c.get("price")),
            "price_orig": _parse_eur(c.get("price_orig")),
            "rating": _parse_float(c.get("rating")),
            "orders": c.get("orders"),
            "orders_num": _parse_orders_num(c.get("orders")),
            "ship_from": (c.get("ship_from") or None),
            "scraped_at": scraped_at,
        })
    return out


def _parse_json_value(value) -> object:
    """Convierte el string JSON devuelto por cdp_eval en objeto Python; None si falla."""
    if isinstance(value, (list, dict)):
        return value
    if not isinstance(value, str):
        return None
    try:
        return json.loads(value)
    except (ValueError, TypeError):
        return None


def scrape_aliexpress_cdp(
    query: str,
    sort: str = "total_tranpro_desc",
    limit: int = 40,
    port: int = 9222,
    timeout_s: float = 25.0,
) -> dict:
    """Scrapea la pagina de busqueda de AliExpress por CDP y devuelve los productos.

    Navega el navegador diario (Chrome con remote debugging) a la URL de busqueda
    ordenada por `sort`, hace scroll para disparar el lazy-load hasta acercarse a
    `limit` cards, extrae cada producto y parsea precios (EUR) y numero de pedidos.

    Args:
        query: Termino de busqueda (ej. "organizador maletero coche"). Los espacios
            se convierten en guiones para la URL.
        sort: Orden de resultados. "total_tranpro_desc" = por numero de pedidos
            (demanda real, el default util para dropshipping). Otros: "default",
            "price_asc", "price_desc".
        limit: Numero objetivo de productos a recolectar. El scroll itera hasta
            acercarse a este valor (cap de seguridad en el numero de scrolls).
        port: Puerto de remote debugging de Chrome. Default 9222.
        timeout_s: Timeout (segundos) para cada evaluacion CDP.

    Returns:
        dict autosuficiente:
            {"status": "ok"|"error"|"captcha",
             "source": "aliexpress",
             "query": str,
             "url": str,            # URL navegada
             "count": int,
             "products": [
                {"item_id", "url", "title", "price"(float EUR|None),
                 "price_orig"(float|None), "rating"(float|None),
                 "orders"(str crudo|None), "orders_num"(int|None),
                 "ship_from"(str|None), "scraped_at"(iso)}
             ],
             "error": str            # solo presente si status=="error"
            }
        Nunca inventa datos: sin cards -> status="error" products=[]; captcha
        detectado -> status="captcha" products=[]. Nunca lanza.
    """
    slug = _slugify_query(query)
    url = f"https://es.aliexpress.com/w/wholesale-{slug}.html?SortType={sort}"
    base = {"status": "error", "source": "aliexpress", "query": query, "url": url,
            "count": 0, "products": []}

    try:
        # 1. Navegar la pestana activa a la URL de busqueda (reutiliza transport CDP).
        #    Se prioriza una pestana cuya URL ya contenga "aliexpress" para no pisar
        #    otra pestana del navegador diario; si no hay, cae al primer target page.
        nav_expr = "location.href=" + json.dumps(url) + "; true"
        nav = cdp_eval(nav_expr, port=port, target_url_substr="aliexpress", timeout_s=timeout_s)
        if not nav.get("ok"):
            nav = cdp_eval(nav_expr, port=port, timeout_s=timeout_s)
        if not nav.get("ok"):
            base["error"] = "navigate failed: " + str(nav.get("error", ""))
            return base

        # 2. Esperar la carga inicial de la SPA + primeras cards.
        time.sleep(8.0)

        # 3. Deteccion temprana de captcha / muro anti-bot.
        cap = cdp_eval(_JS_CAPTCHA, port=port, target_url_substr="aliexpress", timeout_s=timeout_s)
        cap_data = _parse_json_value(cap.get("value")) or {}
        if isinstance(cap_data, dict) and cap_data.get("captcha"):
            res = dict(base)
            res["status"] = "captcha"
            return res

        # 4. Scroll para disparar el lazy-load. AliExpress carga ~15 cards iniciales
        #    y va anadiendo mas al bajar. Iteramos hasta acercarnos a `limit` o
        #    hasta que el conteo deje de crecer (cap de seguridad de 8 scrolls).
        last_count = 0
        stable_rounds = 0
        max_scrolls = 8
        for _ in range(max_scrolls):
            cnt = cdp_eval(
                "document.querySelectorAll('.search-item-card-wrapper-gallery').length",
                port=port, target_url_substr="aliexpress", timeout_s=timeout_s,
            )
            current = cnt.get("value") if cnt.get("ok") else 0
            if not isinstance(current, int):
                current = 0
            if current >= limit:
                break
            if current <= last_count:
                stable_rounds += 1
                if stable_rounds >= 2:
                    break
            else:
                stable_rounds = 0
            last_count = current
            cdp_eval(
                "window.scrollTo(0, document.body.scrollHeight); true",
                port=port, target_url_substr="aliexpress", timeout_s=timeout_s,
            )
            time.sleep(1.2)

        # 5. Extraer las cards con el JS validado.
        ext = cdp_eval(_JS_EXTRACT, port=port, target_url_substr="aliexpress", timeout_s=timeout_s)
        if not ext.get("ok"):
            base["error"] = "extract eval failed: " + str(ext.get("error", ""))
            return base

        raw_list = _parse_json_value(ext.get("value"))
        if not isinstance(raw_list, list):
            base["error"] = "extract returned non-list value"
            return base

        products = _coerce_products(raw_list, query)

        # Sin cards: re-comprobar captcha por si el muro aparecio tras el scroll.
        if not products:
            cap2 = cdp_eval(_JS_CAPTCHA, port=port, target_url_substr="aliexpress", timeout_s=timeout_s)
            cap2_data = _parse_json_value(cap2.get("value")) or {}
            if isinstance(cap2_data, dict) and cap2_data.get("captcha"):
                res = dict(base)
                res["status"] = "captcha"
                return res
            base["error"] = "no product cards found"
            return base

        # Respetar el limite (la galeria puede traer mas que `limit`).
        products = products[:limit]

        return {
            "status": "ok",
            "source": "aliexpress",
            "query": query,
            "url": url,
            "count": len(products),
            "products": products,
        }
    except Exception as e:  # noqa: BLE001 — nunca relanzar, devolver status error
        base["error"] = str(e)
        return base


if __name__ == "__main__":
    q = sys.argv[1] if len(sys.argv) > 1 else "organizador maletero coche"
    srt = sys.argv[2] if len(sys.argv) > 2 else "total_tranpro_desc"
    lim = int(sys.argv[3]) if len(sys.argv) > 3 else 40
    out = scrape_aliexpress_cdp(q, sort=srt, limit=lim)
    print(json.dumps(out, ensure_ascii=False, indent=2))
