"""Identifica los botones de un banner de cookies/consentimiento usando un LLM.

En lugar de depender de selectores hardcodeados por CMP (que rompen cuando el
banner usa marcas/textos distintos), esta funcion recolecta los controles
clicables visibles de la pagina via CDP, los marca con un atributo estable
`data-fnllm="N"` en el DOM, y pregunta a un modelo (haiku via ask_llm) cual es
el boton de "ACEPTAR TODO", cual el de "RECHAZAR" y cual el enlace de
"VER SOCIOS / configurar / mas opciones / finalidades".

Resuelve los CMP cuyos botones no encajan con selectores fijos (los casos
"no-button" del scanner de databrokers). El caller usa los selectores
`[data-fnllm="N"]` devueltos para clicar el control elegido con cdp_eval.
"""

import json
import os
import re
import sys

sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

from browser.cdp_eval import cdp_eval  # noqa: E402
from core.ask_llm import ask_llm  # noqa: E402

# JS que recolecta controles clicables visibles y los marca con data-fnllm="N".
# {MAXC} se sustituye en Python por max_candidates.
_COLLECT_JS = """
(function(){
  var nodes=[].slice.call(document.querySelectorAll('button, a[role=button], [role=button], input[type=button], input[type=submit], a'));
  var out=[],n=0;
  for(var i=0;i<nodes.length && n<{MAXC};i++){
    var el=nodes[i];
    if(!el.getClientRects().length) continue;
    var txt=((el.innerText||el.textContent||el.value||'').trim()).slice(0,60);
    if(!txt) continue;
    el.setAttribute('data-fnllm', String(n));
    out.push({idx:n, tag:el.tagName.toLowerCase(), text:txt,
      aria:(el.getAttribute('aria-label')||'').slice(0,60),
      id:(el.id||'').slice(0,40), cls:((el.className||'').toString().split(' ')[0]||'').slice(0,40)});
    n++;
  }
  return JSON.stringify(out);
})()
"""

_SYSTEM = (
    "Eres un clasificador de banners de consentimiento de cookies (espanol/ingles). "
    "Respondes SOLO con JSON valido, sin texto extra."
)


def _selector(idx):
    """Construye el selector estable `[data-fnllm="N"]` o None si idx es None."""
    if idx is None:
        return None
    return '[data-fnllm="{}"]'.format(idx)


def _extract_json_block(raw: str):
    """Extrae el primer bloque {...} de la respuesta del LLM y lo parsea.

    El modelo puede envolver en ```json o anadir texto; nos quedamos con el
    primer objeto JSON balanceado. Devuelve dict o lanza ValueError.
    """
    # Buscar el primer '{' y emparejar llaves para soportar objetos anidados.
    start = raw.find("{")
    if start == -1:
        raise ValueError("no json object found")
    depth = 0
    for i in range(start, len(raw)):
        c = raw[i]
        if c == "{":
            depth += 1
        elif c == "}":
            depth -= 1
            if depth == 0:
                return json.loads(raw[start : i + 1])
    raise ValueError("unbalanced json object")


def _coerce_idx(val, n_candidates):
    """Normaliza un indice del LLM: int valido en rango o None."""
    if val is None:
        return None
    try:
        i = int(val)
    except (TypeError, ValueError):
        return None
    if 0 <= i < n_candidates:
        return i
    return None


def find_consent_controls_llm(
    *,
    port: int = 9222,
    max_candidates: int = 40,
    model: str = "claude-haiku-4-5-20251001",
) -> dict:
    """Identifica accept/reject/vendors de un banner de cookies via LLM.

    Recolecta los controles clicables visibles de la pagina (marcandolos en el
    DOM con `data-fnllm="N"`) y pregunta al modelo cual es cada uno. Util para
    CMP cuyos botones no encajan con selectores hardcodeados.

    Args:
        port: Puerto de remote debugging de Chrome. Default 9222.
        max_candidates: Maximo de controles a recolectar. Default 40.
        model: Modelo Anthropic a usar via ask_llm. Default haiku.

    Returns:
        dict con claves:
            status: "ok" | "error".
            candidates: lista de {idx, tag, text, aria, id, cls}.
            accept_idx / reject_idx / vendors_idx: int|None elegidos por el LLM.
            accept_selector / reject_selector / vendors_selector: str|None,
                formato `[data-fnllm="N"]` para clicar via cdp_eval.
            reason: str — explicacion breve del LLM.
            error: str — presente solo si status=="error".
        Nunca lanza: cualquier fallo de CDP/eval/LLM se devuelve en el dict.
    """
    # 1. Recolectar controles clicables visibles y marcarlos en el DOM.
    expr = _COLLECT_JS.replace("{MAXC}", str(int(max_candidates)))
    res = cdp_eval(expr, port=port)
    if not res.get("ok"):
        return {
            "status": "error",
            "error": "cdp_eval: " + (res.get("error") or "fallo evaluando JS"),
        }

    raw_list = res.get("value")
    try:
        candidates = json.loads(raw_list) if isinstance(raw_list, str) else (raw_list or [])
    except (TypeError, ValueError) as e:
        return {"status": "error", "error": "candidates_parse: " + str(e)}

    if not candidates:
        return {
            "status": "ok",
            "candidates": [],
            "accept_idx": None,
            "reject_idx": None,
            "vendors_idx": None,
            "accept_selector": None,
            "reject_selector": None,
            "vendors_selector": None,
            "reason": "sin controles visibles",
        }

    # 2. Construir el prompt para el LLM.
    listing = json.dumps(candidates, ensure_ascii=False)
    prompt = (
        "Recibes la lista de controles clicables de un banner de cookies / "
        "consentimiento de una pagina web. Cada control tiene un 'idx' numerico "
        "y su texto/atributos. Identifica:\n"
        '  - accept_idx: el boton para ACEPTAR / CONSENTIR TODO ("Aceptar", '
        '"Aceptar todo", "Accept all", "Consentir", "De acuerdo", "Estoy de acuerdo").\n'
        '  - reject_idx: el boton para RECHAZAR TODO ("Rechazar", "Rechazar todo", '
        '"Reject all", "No acepto", "Continuar sin aceptar").\n'
        '  - vendors_idx: el enlace para VER SOCIOS / partners / proveedores / '
        'configurar / mas opciones / finalidades / "Ver socios", "Configurar", '
        '"Mas informacion", "Gestionar opciones", "Personalizar".\n'
        "Si alguno no existe en la lista, usa null para ese campo.\n\n"
        "Responde EXACTAMENTE con este JSON (sin markdown, sin texto extra):\n"
        '{"accept_idx": N|null, "reject_idx": N|null, "vendors_idx": N|null, "reason": "..."}\n\n'
        "Controles:\n" + listing
    )

    # 3. Preguntar al modelo (sin stream a stdout, respuesta corta).
    answer = ask_llm(prompt, model=model, system=_SYSTEM, max_tokens=300, echo=False)
    if not answer:
        return {
            "status": "error",
            "error": "llm_empty",
            "candidates": candidates,
        }

    # 4. Parsear el JSON de la respuesta de forma robusta.
    try:
        parsed = _extract_json_block(answer)
    except (ValueError, json.JSONDecodeError):
        return {
            "status": "error",
            "error": "llm_parse",
            "raw": answer[:500],
            "candidates": candidates,
        }

    n = len(candidates)
    accept_idx = _coerce_idx(parsed.get("accept_idx"), n)
    reject_idx = _coerce_idx(parsed.get("reject_idx"), n)
    vendors_idx = _coerce_idx(parsed.get("vendors_idx"), n)
    reason = str(parsed.get("reason", ""))

    # 5. Devolver con selectores estables construidos a partir de los idx.
    return {
        "status": "ok",
        "candidates": candidates,
        "accept_idx": accept_idx,
        "reject_idx": reject_idx,
        "vendors_idx": vendors_idx,
        "accept_selector": _selector(accept_idx),
        "reject_selector": _selector(reject_idx),
        "vendors_selector": _selector(vendors_idx),
        "reason": reason,
    }


if __name__ == "__main__":
    _port = int(sys.argv[1]) if len(sys.argv) > 1 else 9222
    out = find_consent_controls_llm(port=_port)
    print(json.dumps(out, ensure_ascii=False, indent=2))
