"""Ejecuta una peticion HTTP y la registra en el History de Hoppscotch self-host.

Doble proposito: (1) lanza la request real con `requests` resolviendo placeholders
de variables y (2) opcionalmente la persiste en el UserHistory del backend
Hoppscotch self-hosted via la mutation GraphQL createUserHistory, de modo que el
humano la vea aparecer en vivo en la pestana History de su GUI (la GUI escucha la
subscription `userHistoryCreated`).

La request se ejecuta con las variables resueltas, pero lo que se guarda en el
History es la request SIN resolver (con `<<var>>`/`{{var}}` literales), igual que
en la GUI: asi el humano ve la plantilla con sus variables, no los valores
expandidos. La mutation esta protegida por GqlAuthGuard: el JWT de sesion viaja en
la cookie `access_token`.
"""

import json
import re

import requests

from infra.build_hoppscotch_collection import build_hoppscotch_collection

# Hoppscotch usa la sintaxis <<var>>; muchas plantillas tambien traen {{var}}.
# Aceptamos ambas: grupo 1 = delimitador de apertura, grupo 2 = nombre de la
# variable, grupo 3 = delimitador de cierre.
_VAR_RE = re.compile(r"(<<|\{\{)\s*([A-Za-z0-9_]+)\s*(>>|\}\})")

# Limite del cuerpo de respuesta en el output, para no devolver payloads enormes.
_BODY_TRUNCATE = 5000

_HISTORY_MUTATION = (
    "mutation($d:String!,$m:String!,$t:ReqType!){"
    " createUserHistory(reqData:$d, resMetadata:$m, reqType:$t){ id } }"
)


def _resolve_placeholders(text: str, variables: dict) -> str:
    """Sustituye <<var>>/{{var}} por su valor en `variables`.

    Si la variable no esta en `variables`, se conserva el literal tal cual
    (incluidos los delimitadores). Determinista y sin I/O.

    Args:
        text: cadena con (opcionales) placeholders.
        variables: dict name->value con los valores de sustitucion.

    Returns:
        la cadena con los placeholders conocidos resueltos.
    """

    def repl(match: re.Match) -> str:
        name = match.group(2)
        if name in variables:
            return str(variables[name])
        return match.group(0)

    return _VAR_RE.sub(repl, text)


def hoppscotch_run_request(
    method: str,
    url: str,
    *,
    title: str | None = None,
    headers: dict | None = None,
    body: str | None = None,
    body_type: str | None = None,
    variables: dict | None = None,
    access_token: str,
    backend_url: str = "http://localhost:3170",
    record_history: bool = True,
    timeout_s: float = 30.0,
    verify_tls: bool = True,
) -> dict:
    """Ejecuta una request HTTP y la registra en el History de Hoppscotch.

    Resuelve los placeholders `<<var>>`/`{{var}}` de la url, los headers y el
    body usando `variables`, lanza la peticion real con `requests`, y (si
    `record_history`) guarda en el UserHistory del backend self-host la request
    SIN resolver (para que en la GUI History se vea con las variables, igual que
    en el editor).

    Args:
        method: metodo HTTP (GET, POST, ...).
        url: endpoint, puede contener placeholders `<<var>>`/`{{var}}`.
        title: nombre visible de la request en el History. None = derivar de
            method + path via build_hoppscotch_collection.
        headers: dict name->value de cabeceras. Sus values admiten placeholders.
        body: cuerpo de la request como texto ya serializado. Admite placeholders.
        body_type: tipo de cuerpo ("json"|"form"|"raw"|None).
        variables: dict name->value para resolver los placeholders al EJECUTAR.
            None = no se resuelve nada (los literales viajan tal cual).
        access_token: JWT de sesion (de hoppscotch_login). Viaja en la cookie
            `access_token`, NO en el header Authorization. Necesario para grabar
            en el History.
        backend_url: base del backend Hoppscotch self-host (sin barra final).
        record_history: si True y hay access_token, registra la request en el
            UserHistory via createUserHistory.
        timeout_s: timeout de la peticion HTTP en segundos.
        verify_tls: verificacion del certificado TLS de la request ejecutada.

    Returns:
        Dict. En exito de la ejecucion HTTP:
        ``{"status": "ok", "status_code": int, "duration_ms": int,
        "response_body": str (truncado a 5000 chars), "response_headers": dict,
        "recorded": bool, "history_id": str|None}``. Si la ejecucion fue ok pero
        el registro de History fallo, `status` sigue "ok", `recorded` False y se
        anade `history_error`. Si la ejecucion HTTP falla (RequestException):
        ``{"status": "error", "error": str, "recorded": False}``.
    """
    variables = variables or {}
    headers = headers or {}

    # 1) Resolver placeholders para EJECUTAR (copia; los originales se conservan
    #    para registrarlos sin resolver en el History).
    resolved_url = _resolve_placeholders(url, variables)
    resolved_headers = {
        key: _resolve_placeholders(str(value), variables)
        for key, value in headers.items()
    }
    resolved_body = (
        _resolve_placeholders(body, variables) if body is not None else None
    )

    # 2) Ejecutar la peticion real.
    try:
        resp = requests.request(
            method,
            resolved_url,
            headers=resolved_headers,
            data=resolved_body if resolved_body is not None else None,
            timeout=timeout_s,
            verify=verify_tls,
        )
    except requests.RequestException as exc:
        return {
            "status": "error",
            "error": f"transport error: {exc}",
            "recorded": False,
        }

    duration_ms = int(resp.elapsed.total_seconds() * 1000)
    status_code = resp.status_code
    response_body = resp.text[:_BODY_TRUNCATE]
    response_headers = dict(resp.headers)

    result = {
        "status": "ok",
        "status_code": status_code,
        "duration_ms": duration_ms,
        "response_body": response_body,
        "response_headers": response_headers,
        "recorded": False,
        "history_id": None,
    }

    # 3) Registrar en el UserHistory (request SIN resolver, como en la GUI).
    if not record_history or not access_token:
        return result

    spec = {
        "method": method,
        "url": url,
        "headers": headers,
        "body": body,
        "body_type": body_type,
    }
    req_names = [title] if title else None
    req_item = build_hoppscotch_collection([spec], request_names=req_names)[
        "requests"
    ][0]
    req_data = json.dumps(req_item)
    res_metadata = json.dumps(
        {"statusCode": status_code, "duration": duration_ms}
    )

    payload = {
        "query": _HISTORY_MUTATION,
        "variables": {"d": req_data, "m": res_metadata, "t": "REST"},
    }

    try:
        hist_resp = requests.post(
            f"{backend_url}/graphql",
            json=payload,
            cookies={"access_token": access_token},
            timeout=timeout_s,
        )
    except requests.RequestException as exc:
        result["history_error"] = f"transport error: {exc}"
        return result

    try:
        hist_data = hist_resp.json()
    except ValueError:
        result["history_error"] = (
            f"non-JSON history response (HTTP {hist_resp.status_code})"
        )
        return result

    if hist_data.get("errors"):
        result["history_error"] = f"graphql errors: {hist_data['errors']}"
        return result

    created = (hist_data.get("data") or {}).get("createUserHistory")
    if not created or not created.get("id"):
        result["history_error"] = "createUserHistory returned no id"
        return result

    result["recorded"] = True
    result["history_id"] = created["id"]
    return result
