"""ingest_market_trends_headless — ingesta de fuentes CDP en un Chrome headless aislado.

Wrapper de `ingest_market_trends` (pipeline del proyecto captacion_clientes) que lanza un
Chromium **headless** con un **perfil dedicado** y un puerto de remote-debugging propio,
scrapea las fuentes CDP indicadas, y **cierra la instancia al terminar** — siempre, incluso
si falla el scraping.

Motivo: el scraping NO debe abrir pestañas en el navegador diario del usuario
(`chromium-personal`, puerto 9222). Norma: perfil dedicado + headless + cerrar al terminar.

El Chrome se lanza vía `systemd-run --user` (un scope transitorio), porque lanzar chromium
con un `exec`/`Popen` directo desde el proceso del agente da exit-144 cuando hereda el grupo
de controlado del agente. Si `systemd-run` no está disponible, se cae a `subprocess.Popen`
en un grupo de proceso nuevo (`start_new_session=True`).

Las fuentes CDP (`aliexpress_cdp`, `amazon_movers_cdp`, `amazon_saturation_cdp`) leen su
`port` del config `sources.json` del proyecto. Ese config debe apuntar al MISMO puerto que
este wrapper usa para lanzar el Chrome headless (default 9334). De ese modo
`ingest_market_trends(source)` reutiliza la instancia que este wrapper levantó.
"""

import argparse
import json
import os
import shutil
import signal
import subprocess
import sys
import time
import urllib.request

ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
sys.path.insert(0, os.path.join(ROOT, "python", "functions"))

from pipelines.ingest_market_trends import ingest_market_trends  # noqa: E402

DEFAULT_SOURCES = "aliexpress_cdp,amazon_movers_cdp,amazon_saturation_cdp"
DEFAULT_PORT = 9334
DEFAULT_PROFILE = "~/.config/fn_scrape_chrome"

# Candidatos de binario chromium/chrome. shutil.which primero (respeta PATH), luego
# rutas absolutas conocidas del sistema (el `chromium` del usuario suele ser un alias de
# shell no visible a subprocess, y el binario real vive en /usr/lib/chromium/chromium).
_CHROME_NAMES = ("chromium", "chromium-browser", "google-chrome", "google-chrome-stable")
_CHROME_ABS = (
    "/usr/bin/chromium",
    "/usr/lib/chromium/chromium",
    "/usr/bin/chromium-browser",
    "/usr/bin/google-chrome",
    "/usr/bin/google-chrome-stable",
    "/snap/bin/chromium",
)


def _find_chrome() -> str | None:
    """Devuelve la ruta a un binario chromium/chrome ejecutable, o None."""
    for name in _CHROME_NAMES:
        path = shutil.which(name)
        if path:
            return path
    for path in _CHROME_ABS:
        if os.path.isfile(path) and os.access(path, os.X_OK):
            return path
    return None


def _cdp_alive(port: int, timeout: float = 1.0) -> bool:
    """True si el endpoint CDP responde en 127.0.0.1:<port>/json/version."""
    url = f"http://127.0.0.1:{port}/json/version"
    try:
        with urllib.request.urlopen(url, timeout=timeout) as resp:
            return 200 <= resp.status < 300
    except Exception:  # noqa: BLE001
        return False


def _wait_cdp(port: int, deadline_s: float = 12.0) -> bool:
    """Espera a que el CDP responda hasta `deadline_s` (sondea cada 0.5s)."""
    end = time.time() + deadline_s
    while time.time() < end:
        if _cdp_alive(port):
            return True
        time.sleep(0.5)
    return False


def _chrome_args(chrome_bin: str, port: int, profile_dir: str) -> list[str]:
    return [
        chrome_bin,
        "--headless=new",
        "--disable-gpu",
        f"--remote-debugging-port={port}",
        f"--user-data-dir={profile_dir}",
        "--no-first-run",
        "--no-default-browser-check",
        "--remote-allow-origins=*",
        "--disable-extensions",
    ]


def _launch(chrome_bin: str, port: int, profile_dir: str) -> tuple[str, int | None]:
    """Lanza Chrome headless aislado. Devuelve (mecanismo, pid).

    mecanismo: 'systemd' (scope transitorio) o 'popen' (grupo de proceso propio).
    pid: solo poblado en modo 'popen' (para poder matar el grupo en el cierre).
    """
    unit = f"fnscrape_dag_{port}"
    systemd_run = shutil.which("systemd-run")
    if systemd_run:
        cmd = [
            systemd_run, "--user", "--quiet", "--collect", f"--unit={unit}",
            *_chrome_args(chrome_bin, port, profile_dir),
        ]
        try:
            subprocess.run(cmd, check=True, timeout=15,
                           stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            return "systemd", None
        except Exception:  # noqa: BLE001
            # systemd-run falló (sin --user bus, etc.) -> fallback a Popen.
            pass

    proc = subprocess.Popen(
        _chrome_args(chrome_bin, port, profile_dir),
        start_new_session=True,
        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
    )
    return "popen", proc.pid


def _close(mechanism: str, pid: int | None, port: int, profile_dir: str) -> bool:
    """Cierra la instancia que ESTE wrapper lanzó. Devuelve True si el puerto ya no responde."""
    unit = f"fnscrape_dag_{port}"
    if mechanism == "systemd":
        systemctl = shutil.which("systemctl")
        if systemctl:
            for kind in (f"{unit}.scope", f"{unit}.service"):
                try:
                    subprocess.run([systemctl, "--user", "stop", kind],
                                   timeout=10, stdout=subprocess.DEVNULL,
                                   stderr=subprocess.DEVNULL)
                except Exception:  # noqa: BLE001
                    pass
    elif mechanism == "popen" and pid is not None:
        try:
            pgid = os.getpgid(pid)
            os.killpg(pgid, signal.SIGTERM)
            for _ in range(20):  # hasta ~2s para salida limpia
                time.sleep(0.1)
                if not _cdp_alive(port):
                    break
            if _cdp_alive(port):
                os.killpg(pgid, signal.SIGKILL)
        except ProcessLookupError:
            pass
        except Exception:  # noqa: BLE001
            pass

    # Respaldo: matar cualquier chromium colgado de este perfil concreto.
    pkill = shutil.which("pkill")
    if pkill:
        try:
            subprocess.run([pkill, "-f", f"user-data-dir={profile_dir}"],
                           timeout=10, stdout=subprocess.DEVNULL,
                           stderr=subprocess.DEVNULL)
        except Exception:  # noqa: BLE001
            pass

    # Esperar a que el puerto deje de responder (cierre asíncrono del cgroup).
    for _ in range(20):  # hasta ~2s
        if not _cdp_alive(port):
            return True
        time.sleep(0.1)
    return not _cdp_alive(port)


def ingest_market_trends_headless(
    sources: str = "",
    port: int = DEFAULT_PORT,
    profile_dir: str = "",
) -> dict:
    """Lanza un Chrome headless aislado, scrapea las fuentes CDP y lo cierra al terminar.

    Args:
        sources: fuentes CDP separadas por coma. Vacío -> las 3 del proyecto
            (aliexpress_cdp,amazon_movers_cdp,amazon_saturation_cdp).
        port: puerto de remote-debugging del Chrome aislado (debe coincidir con el `port`
            de los bloques `*_cdp` en sources.json). Default 9334.
        profile_dir: user-data-dir dedicado. Vacío -> ~/.config/fn_scrape_chrome.

    Returns:
        dict con {status, port, profile_dir, launched, closed, results:[...]}. Nunca lanza
        excepción al caller: cualquier fallo se refleja en `status`/`results` y el finally
        cierra la instancia.
    """
    if not sources:
        sources = DEFAULT_SOURCES
    if not profile_dir:
        profile_dir = os.path.expanduser(DEFAULT_PROFILE)
    profile_dir = os.path.abspath(os.path.expanduser(profile_dir))
    os.makedirs(profile_dir, exist_ok=True)

    out: dict = {
        "status": "error",
        "port": port,
        "profile_dir": profile_dir,
        "launched": False,
        "closed": False,
        "results": [],
    }

    mechanism = ""
    pid: int | None = None
    reuse = False

    # 1) Si ya hay un CDP vivo en el puerto, reutilizarlo (no lo cerraremos).
    if _cdp_alive(port):
        reuse = True
    else:
        chrome_bin = _find_chrome()
        if not chrome_bin:
            out["error"] = (
                "no se encontró binario chromium/chrome "
                f"(probados: {', '.join(_CHROME_NAMES)} + rutas absolutas conocidas)"
            )
            return out
        try:
            mechanism, pid = _launch(chrome_bin, port, profile_dir)
            out["launched"] = True
        except Exception as exc:  # noqa: BLE001
            out["error"] = f"fallo al lanzar chromium: {exc}"
            return out

        # 2) Esperar a que el CDP responda.
        if not _wait_cdp(port, deadline_s=12.0):
            out["error"] = f"el CDP no respondió en 127.0.0.1:{port} tras 12s"
            out["closed"] = _close(mechanism, pid, port, profile_dir)
            return out

    # 3) Scrapear cada fuente. Un fallo de una fuente no aborta el resto.
    try:
        for raw in sources.split(","):
            source = raw.strip()
            if not source:
                continue
            try:
                summary = ingest_market_trends(source)
                out["results"].append({
                    "source": source,
                    "scraped": summary.get("scraped"),
                    "inserted": summary.get("inserted"),
                    **({"note": summary["note"]} if summary.get("note") else {}),
                    **({"status": summary["status"]} if summary.get("status") else {}),
                })
            except Exception as exc:  # noqa: BLE001
                out["results"].append({"source": source, "error": str(exc)})
        out["status"] = "ok"
    finally:
        # 4) Cerrar SIEMPRE lo que nosotros lanzamos (no si reutilizamos uno ajeno).
        if out["launched"] and not reuse:
            out["closed"] = _close(mechanism, pid, port, profile_dir)
        else:
            out["closed"] = False

    return out


def main() -> int:
    ap = argparse.ArgumentParser(
        description="Ingesta de fuentes CDP en un Chrome headless aislado (perfil dedicado)."
    )
    ap.add_argument("--sources", default="",
                    help=f"Fuentes CDP separadas por coma. Vacío -> {DEFAULT_SOURCES}.")
    ap.add_argument("--port", type=int, default=DEFAULT_PORT,
                    help="Puerto remote-debugging del Chrome aislado.")
    ap.add_argument("--profile-dir", default="",
                    help="user-data-dir dedicado (vacío -> ~/.config/fn_scrape_chrome).")
    args = ap.parse_args()

    result = ingest_market_trends_headless(
        sources=args.sources, port=args.port, profile_dir=args.profile_dir,
    )
    print(json.dumps(result, ensure_ascii=False))
    return 0 if result.get("status") == "ok" else 1


if __name__ == "__main__":
    sys.exit(main())
