"""monitor_freelance_projects_headless — monitor freelance en un Chrome headless aislado.

Wrapper de `monitor_freelance_projects` (pipeline del proyecto captacion_clientes) que lanza
un Chromium **headless** con un **perfil dedicado** y un puerto de remote-debugging propio,
corre el monitor de proyectos freelance apuntando a ESE puerto, y **cierra la instancia al
terminar** — siempre, incluso si el scraping falla.

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 control del agente. Si `systemd-run` no está disponible, se cae a `subprocess.Popen` en un
grupo de proceso nuevo (`start_new_session=True`).

A diferencia de `ingest_market_trends_headless` (que itera fuentes CDP), este wrapper llama
UNA sola vez al pipeline `monitor_freelance_projects`, pasándole el puerto del Chrome aislado.
El pipeline scrapea Workana (y opcionalmente Upwork) por CDP, deduplica en DuckDB y exporta a
Excel; este wrapper solo gestiona el ciclo de vida del navegador.
"""

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.monitor_freelance_projects import monitor_freelance_projects  # noqa: E402

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 monitor_freelance_projects_headless(
    category: str = "it-programming",
    language: str = "es",
    query: str = "",
    pages: int = 1,
    include_upwork: bool = False,
    upwork_query: str = "custom software",
    duckdb_path: str = "",
    xlsx_path: str = "",
    port: int = DEFAULT_PORT,
    profile_dir: str = "",
    timeout_s: float = 25.0,
) -> dict:
    """Lanza un Chrome headless aislado, corre el monitor freelance y lo cierra al terminar.

    Pipeline IMPURO: arranca su propio Chromium headless con perfil dedicado, ejecuta
    `monitor_freelance_projects` apuntando a ESE puerto, y en el `finally` cierra la
    instancia que lanzó. Nunca abre pestañas en el navegador diario del usuario
    (`chromium-personal`, CDP 9222). NUNCA lanza excepción al caller: cualquier fallo se
    refleja en `status`/`error` y el navegador se cierra igual.

    Args:
        category: categoría de Workana (?category=). Default "it-programming".
        language: idioma de los proyectos de Workana (?language=). Default "es".
        query: query libre aplicada a ambas fuentes (extra_query en Workana; sobrescribe
            upwork_query en Upwork si no está vacía).
        pages: número de páginas de listado a recorrer por fuente. Default 1.
        include_upwork: si True, scrapea Upwork además de Workana. Default False.
        upwork_query: query para Upwork cuando include_upwork. Default "custom software".
        duckdb_path: ruta del archivo DuckDB. Vacío -> ~/.fn_freelance/freelance.duckdb.
        xlsx_path: ruta del .xlsx de salida. Vacío -> ~/.fn_freelance/freelance_projects.xlsx.
        port: puerto de remote-debugging del Chrome headless aislado. Default 9334.
        profile_dir: user-data-dir dedicado. Vacío -> ~/.config/fn_scrape_chrome.
        timeout_s: timeout en segundos por página para los scrapers. Default 25.0.

    Returns:
        dict que SIEMPRE incluye {status, port, profile_dir, launched, closed} y, en éxito,
        las claves del resultado de `monitor_freelance_projects` (new_count, total_in_db,
        new_projects, xlsx_path, duckdb_path, sources, ...). En error sin lanzar incluye
        `error`. El finally cierra siempre la instancia que lanzó (no la que reutiliza).
    """
    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,
    }

    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) Correr el monitor freelance contra el puerto del Chrome aislado.
    try:
        res = monitor_freelance_projects(
            category=category,
            language=language,
            query=query,
            pages=pages,
            include_upwork=include_upwork,
            upwork_query=upwork_query,
            duckdb_path=duckdb_path,
            xlsx_path=xlsx_path,
            port=port,
            timeout_s=timeout_s,
        )
        if isinstance(res, dict):
            # Mezclar el resultado del monitor; las claves de lifecycle (status, port,
            # profile_dir, launched, closed) se restauran/recalculan abajo.
            out.update(res)
        else:
            out["error"] = f"monitor_freelance_projects devolvió un tipo inesperado: {type(res).__name__}"
            out["status"] = "error"
    except Exception as exc:  # noqa: BLE001 — el wrapper nunca lanza al caller
        out["error"] = f"{type(exc).__name__}: {exc}"
        out["status"] = "error"
    finally:
        # 4) Restaurar las claves de lifecycle que `out.update(res)` pudo pisar.
        out["port"] = port
        out["profile_dir"] = profile_dir
        out["launched"] = bool(out.get("launched"))
        # 5) 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=(
            "Monitor de captacion freelance (Workana + Upwork -> DuckDB + Excel) en un "
            "Chrome headless AISLADO con perfil dedicado."
        )
    )
    ap.add_argument("--category", default="it-programming")
    ap.add_argument("--language", default="es")
    ap.add_argument("--query", default="")
    ap.add_argument("--pages", type=int, default=1)
    ap.add_argument("--include-upwork", action="store_true")
    ap.add_argument("--upwork-query", default="custom software")
    ap.add_argument("--duckdb-path", default="")
    ap.add_argument("--xlsx-path", default="")
    ap.add_argument("--port", type=int, default=DEFAULT_PORT,
                    help="Puerto remote-debugging del Chrome aislado (default 9334).")
    ap.add_argument("--profile-dir", default="",
                    help="user-data-dir dedicado (vacío -> ~/.config/fn_scrape_chrome).")
    ap.add_argument("--timeout-s", type=float, default=25.0)
    args = ap.parse_args()

    result = monitor_freelance_projects_headless(
        category=args.category,
        language=args.language,
        query=args.query,
        pages=args.pages,
        include_upwork=args.include_upwork,
        upwork_query=args.upwork_query,
        duckdb_path=args.duckdb_path,
        xlsx_path=args.xlsx_path,
        port=args.port,
        profile_dir=args.profile_dir,
        timeout_s=args.timeout_s,
    )
    print(json.dumps(result, ensure_ascii=False))
    return 0 if result.get("status") == "ok" else 1


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