"""
Cliente HTTP para el servidor ICEBERG Manillas POS.
Todos los metodos son sincronos (no async).
El threading para /api/sell se maneja en la capa de UI.

Si el servidor no esta disponible, get_packages() retorna los paquetes
por defecto locales + combos personalizados guardados.
"""
import httpx
import json
import os
from typing import Optional

from config import DEFAULT_PACKAGES
from ui.add_combo_dialog import load_custom_combos

TIMEOUT_SHORT = 5    # segundos — para GETs
TIMEOUT_SELL  = 45   # segundos — POST /api/sell bloquea hasta 30s en el server + overhead

# Token del agente hardware (generado por agent.py en agent_token.txt)
_AGENT_TOKEN_FILE = os.path.join(os.path.dirname(__file__), "..", "agent", "agent_token.txt")


def _get_agent_token() -> str:
    """Lee el Bearer token del agente desde agent_token.txt."""
    try:
        return open(_AGENT_TOKEN_FILE, "r").read().strip()
    except Exception:
        return ""

# ---------------------------------------------------------------------------
# Configuracion dinamica — se carga desde data/config.json
# ---------------------------------------------------------------------------
_CONFIG_PATH = os.path.join(os.path.dirname(__file__), "data", "config.json")
_DEFAULT_CONFIG = {
    "server_url": "http://192.168.101.120:8000",
    "agent_url":  "http://127.0.0.1:5555",
}


def load_config() -> dict:
    try:
        with open(_CONFIG_PATH, encoding="utf-8") as f:
            return json.load(f)
    except Exception:
        return dict(_DEFAULT_CONFIG)


def save_config(cfg: dict):
    """Guarda la configuracion en config.json."""
    try:
        os.makedirs(os.path.dirname(_CONFIG_PATH), exist_ok=True)
        with open(_CONFIG_PATH, "w", encoding="utf-8") as f:
            json.dump(cfg, f, indent=2, ensure_ascii=False)
    except Exception:
        pass


def _base_url() -> str:
    return load_config().get("server_url", _DEFAULT_CONFIG["server_url"])


def _agent_url() -> str:
    return load_config().get("agent_url", _DEFAULT_CONFIG["agent_url"])


def get_stock_total() -> int:
    """Retorna el stock total de manillas configurado (default 2)."""
    return int(load_config().get("stock_total", 2))


def set_stock_total(n: int):
    """Guarda el nuevo stock total de manillas en config.json."""
    cfg = load_config()
    cfg["stock_total"] = max(0, int(n))
    save_config(cfg)


def get_venue_id() -> str:
    """Retorna el venue_id configurado (default quito-01)."""
    return load_config().get("venue_id", "quito-01")


def get_agent_id() -> str:
    """Retorna el agent_id configurado (default iceberg-agent-local)."""
    return load_config().get("agent_id", "iceberg-agent-local")


def set_venue_id(v: str):
    """Guarda el venue_id en config.json."""
    cfg = load_config()
    cfg["venue_id"] = v
    save_config(cfg)


def _get(path: str) -> dict | list:
    """GET generico. Retorna el JSON parseado o {"error": "..."} si falla."""
    try:
        r = httpx.get(f"{_base_url()}{path}", timeout=TIMEOUT_SHORT)
        r.raise_for_status()
        return r.json()
    except httpx.ConnectError:
        return {"error": "No se puede conectar al servidor"}
    except httpx.TimeoutException:
        return {"error": "El servidor no responde (timeout)"}
    except httpx.HTTPStatusError as e:
        return {"error": f"Error HTTP {e.response.status_code}"}
    except Exception as e:
        return {"error": str(e)}


def _post(path: str, body: dict, timeout: float = TIMEOUT_SHORT) -> dict:
    """POST generico. Retorna el JSON parseado o {"error": "..."} si falla."""
    try:
        r = httpx.post(f"{_base_url()}{path}", json=body, timeout=timeout)
        # 408 (timeout de pulsera) es una respuesta valida, no un error
        if r.status_code not in (200, 408):
            r.raise_for_status()
        return r.json()
    except httpx.ConnectError:
        return {"error": "No se puede conectar al servidor"}
    except httpx.TimeoutException:
        return {"error": "El servidor no responde (timeout de red)"}
    except httpx.HTTPStatusError as e:
        return {"error": f"Error HTTP {e.response.status_code}"}
    except Exception as e:
        return {"error": str(e)}


# ---------------------------------------------------------------------------
# Endpoints publicos
# ---------------------------------------------------------------------------

def check_server_alive() -> bool:
    """Ping rapido al servidor: GET /api/health. True si responde."""
    try:
        r = httpx.get(f"{_base_url()}/api/health", timeout=3)
        return r.status_code == 200
    except Exception:
        return False


def get_hardware_status() -> dict:
    """
    GET /api/hardware/status
    Retorna: {"agent_reachable": bool, "programmer_connected": bool}
    En error: {"error": "..."}
    """
    return _get("/api/hardware/status")


def get_agent_status() -> dict:
    """
    GET /status directamente al agente (puerto 5555).
    Usado para verificar si el controlador USB (programador HID) esta conectado.
    Retorna: {"agent_ok": bool, "programmer_connected": bool}
    """
    try:
        r = httpx.get(f"{_agent_url()}/status", timeout=3)
        if r.status_code == 200:
            data = r.json()
            return {
                "agent_ok": True,
                "programmer_connected": data.get("programmer_connected", False),
            }
        return {"agent_ok": False, "programmer_connected": False}
    except Exception:
        return {"agent_ok": False, "programmer_connected": False}


def get_packages() -> list:
    """
    Solo muestra combos que existan en custom_combos.json (creados localmente).
    Si el combo tiene match en el servidor o en /terminals/.../packages,
    se fusiona con nombre/precio del servidor (assigned=True).
    Si no tiene match, se muestra como deshabilitado (assigned=False).
    Combos que ya no existen en el servidor se eliminan automaticamente.
    """
    from ui.add_combo_dialog import save_custom_combos
    custom_pkgs = load_custom_combos()
    if not custom_pkgs:
        custom_pkgs = recover_combos()
    if not custom_pkgs:
        return []

    # Datos del servidor para fusionar
    result = _get("/api/packages")
    server_pkgs = result if isinstance(result, list) and result else []
    server_by_id = {str(p.get("id", "")): p for p in server_pkgs}

    assigned_pkgs = get_terminal_packages()
    assigned_by_id = {str(p.get("combo_id", "")): p for p in assigned_pkgs}

    # Limpiar custom_combos.json: solo mantener combos adoptados activamente
    # (assigned_by_id contiene solo terminal_packages con active=True)
    active_combo_ids = set(assigned_by_id.keys())
    cleaned = [c for c in custom_pkgs if str(c["id"]) in active_combo_ids]
    if len(cleaned) < len(custom_pkgs):
        removed = [str(c["id"]) for c in custom_pkgs if str(c["id"]) not in active_combo_ids]
        print(f"[App] Limpieza: eliminados {removed} de custom_combos.json (no adoptados en servidor)", flush=True)
        save_custom_combos(cleaned)
        custom_pkgs = cleaned

    merged = []
    for custom in custom_pkgs:
        cid = str(custom["id"])
        server = server_by_id.get(cid) or assigned_by_id.get(cid)
        if server:
            # Fusionar datos del servidor con estilo local
            merged_pkg = dict(server)
            merged_pkg["id"]            = cid   # Siempre usar el combo_id real (ej. "C-CUE001-01"), no el int autoincremental de TerminalPackage
            merged_pkg["custom"]        = True
            merged_pkg["assigned"]      = True
            merged_pkg["color1"]        = custom.get("color1")
            merged_pkg["color2"]        = custom.get("color2")
            merged_pkg["text_color"]    = custom.get("text_color", "#ffffff")
            merged_pkg["bold"]          = custom.get("bold", False)
            merged_pkg["font_size"]     = custom.get("font_size", "M")
            merged_pkg["minutes_green"] = custom.get("minutes_green", 0)
            merged_pkg["minutes_blue"]  = custom.get("minutes_blue", 0)
            merged_pkg["minutes_red"]   = custom.get("minutes_red", 0)
            # minutes es obligatorio para SaleDialog._build_form()
            if "minutes" not in merged_pkg:
                merged_pkg["minutes"] = (
                    merged_pkg["minutes_green"]
                    + merged_pkg["minutes_blue"]
                    + merged_pkg["minutes_red"]
                )
            merged.append(merged_pkg)
        else:
            # No asignado en servidor: mostrar como deshabilitado
            merged_pkg = dict(custom)
            merged_pkg["assigned"]  = False
            merged_pkg["name"]      = custom.get("id", "???")
            merged_pkg["price"]     = 0
            merged_pkg["minutes"]   = 0
            merged.append(merged_pkg)

    # Ordenar: asignados primero, luego por ID (C-CUE001-01 antes que C-CUE001-04)
    merged.sort(key=lambda p: (0 if p.get("assigned") else 1, str(p.get("id", ""))))
    return merged


def register_combo(combo_id: str, config: dict | None = None) -> dict:
    """
    POST /api/combos/register?agent_id={agent_id}
    Registra un combo custom en el servidor para que el admin le asigne nombre y precio.
    Envia config visual completa para que el servidor la persista.
    Idempotente: si ya existe actualiza config.
    """
    agent_id = get_agent_id()
    body: dict = {"combo_id": combo_id}
    if config:
        body["config"] = config
    return _post(f"/api/combos/register?agent_id={agent_id}", body)


def unregister_combo(combo_id: str) -> dict:
    """
    DELETE /api/combos/{combo_id}
    Elimina un combo del servidor (desactiva terminal_packages + sync al cloud).
    """
    try:
        url = f"{_server_url()}/api/combos/{combo_id}"
        r = httpx.delete(url, timeout=8)
        if r.status_code == 200:
            return r.json()
        print(f"[API] unregister_combo {combo_id}: HTTP {r.status_code}", flush=True)
        return {"error": r.text}
    except Exception as e:
        print(f"[API] unregister_combo {combo_id}: {e}", flush=True)
        return {"error": str(e)}


def recover_combos() -> list:
    """
    Recupera combos del servidor cuando custom_combos.json esta vacio.
    1) Intenta GET /api/terminals/{agent_id}/combos (config_json completo).
    2) Fallback: GET /api/venue/packages para combos adoptados sin config_json
       (crea tarjeta minima con nombre/precio del servidor).
    """
    from ui.add_combo_dialog import save_custom_combos
    agent_id = get_agent_id()
    recovered = []
    recovered_ids = set()

    # 1) Combos con config_json completo
    result = _get(f"/api/terminals/{agent_id}/combos")
    if isinstance(result, list):
        for combo in result:
            config_str = combo.get("config_json")
            if not config_str:
                continue
            try:
                pkg = json.loads(config_str)
                pkg["id"] = combo.get("combo_id", pkg.get("id"))
                pkg["custom"] = True
                recovered.append(pkg)
                recovered_ids.add(pkg["id"])
            except Exception:
                pass

    # 2) Fallback: combos adoptados (terminal_packages) sin config_json
    assigned = get_terminal_packages()
    if isinstance(assigned, list):
        for tp in assigned:
            cid = str(tp.get("combo_id", ""))
            if not cid or cid in recovered_ids:
                continue
            # Intentar extraer minutos del nombre (e.g. "Paquete 60 minutos" -> 60)
            import re
            tp_name = tp.get("name", "")
            m = re.search(r'(\d+)\s*min', tp_name, re.IGNORECASE)
            mins = int(m.group(1)) if m else 60  # default 60 si no se puede parsear
            # Color blue por defecto (el mas comun)
            recovered.append({
                "id": cid,
                "custom": True,
                "color1": "#1a3a6a",
                "color2": "#234888",
                "text_color": "#ffffff",
                "bold": False,
                "font_size": "M",
                "minutes_green": 1,
                "minutes_blue": mins,
                "minutes_red": 1,
            })
            recovered_ids.add(cid)
            print(f"[App] Combo {cid} recuperado sin config_json: blue={mins}min (de nombre '{tp_name}')", flush=True)

    if recovered:
        save_custom_combos(recovered)
        print(f"[App] Recuperados {len(recovered)} combo(s) del servidor", flush=True)
    return recovered


def get_terminal_packages(agent_id: str | None = None) -> list:
    """
    GET /api/venue/packages
    Retorna todos los combos adoptados del venue (todas las terminales).
    """
    result = _get("/api/venue/packages")
    return result if isinstance(result, list) else []


def sell(package_id: str, payment_method: str,
         amount_received: float, cashier: str = "caja1",
         timeout: int = 30, agent_id: str | None = None,
         red_minutes: int | None = None,
         green_minutes: int | None = None,
         blue_minutes: int | None = None) -> dict:
    """
    POST /api/sell
    ADVERTENCIA: bloquea hasta `timeout` segundos esperando la pulsera.
    Llamar SIEMPRE desde un hilo secundario (threading.Thread).

    Para combos custom: agent_id es obligatorio y los 3 campos RGB tambien.

    Retorna:
      {"status": "ok", "session_id", "package_name", "minutes", "amount", "programmed_at"}
      {"status": "timeout", "session_id", ...}
      {"error": "..."} si hay falla de red
    """
    body = {
        "package_id": package_id,
        "payment_method": payment_method,
        "amount_received": amount_received,
        "cashier": cashier,
        "timeout": timeout,
    }
    if agent_id:
        body["agent_id"] = agent_id
    if red_minutes is not None:
        body["red_minutes"] = red_minutes
    if green_minutes is not None:
        body["green_minutes"] = green_minutes
    if blue_minutes is not None:
        body["blue_minutes"] = blue_minutes
    return _post("/api/sell", body, timeout=TIMEOUT_SELL)


def retry_sell(session_id: str, timeout: int = 30) -> dict:
    """
    POST /api/sell/{session_id}/retry
    Reintenta programar una venta que quedo en timeout.
    Tambien bloquea — llamar desde hilo secundario.
    """
    body = {"timeout": timeout}
    return _post(f"/api/sell/{session_id}/retry", body, timeout=TIMEOUT_SELL)


def get_sessions() -> list:
    """
    GET /api/sessions
    Retorna lista de ventas del dia.
    En error: lista vacia []
    """
    result = _get("/api/sessions")
    if isinstance(result, list):
        return result
    return []


def get_daily_report() -> dict:
    """
    GET /api/reports/daily
    Retorna resumen del dia: {"date", "total_sales", "total_revenue", "by_package"}
    En error: {"error": "..."}
    """
    return _get("/api/reports/daily")


def get_deactivations() -> int:
    """
    GET /deactivations del agente (puerto 5555).
    Retorna cuantas pulseras fueron desactivadas desde la ultima llamada.
    No bloquea: si el agente no responde, retorna 0.
    """
    try:
        token = _get_agent_token()
        headers = {"Authorization": f"Bearer {token}"} if token else {}
        r = httpx.get(f"{_agent_url()}/deactivations", headers=headers, timeout=2)
        if r.status_code == 200:
            return int(r.json().get("count", 0))
    except Exception:
        pass
    return 0


def validar_pin_supervisor(pin: str, accion: str, solicitante: str, motivo: str) -> dict:
    """
    POST /api/supervisor/validar-pin
    Pre-valida el PIN de supervisor antes de operaciones restringidas.

    Retorna:
      {"status": "ok", "supervisor": "...", "supervisor_id": N}  — PIN correcto
      {"error": "..."}  — PIN incorrecto, sin motivo, o falla de red
    """
    url = f"{_base_url()}/api/supervisor/validar-pin"
    try:
        r = httpx.post(
            url,
            json={
                "pin": pin,
                "accion": accion,
                "solicitante": solicitante,
                "motivo": motivo,
            },
            timeout=TIMEOUT_SHORT,
        )
        try:
            data = r.json()
        except Exception:
            return {"error": f"Error HTTP {r.status_code}: {r.text[:120]}"}
        if r.status_code == 200:
            return data
        return {"error": data.get("detail", f"Error HTTP {r.status_code}")}
    except httpx.ConnectError:
        return {"error": "No se puede conectar al servidor"}
    except httpx.TimeoutException:
        return {"error": "El servidor no responde (timeout)"}
    except Exception as e:
        return {"error": str(e)}


def sync_upload(agent_id: str, sales: list) -> dict:
    """
    POST /api/sync/upload
    Envia un lote de ventas offline al servidor para que las registre.
    El servidor es idempotente: ignora session_ids ya conocidos.

    Retorna:
      {"status": "ok", "accepted": N}   — N ventas aceptadas
      {"error": "..."} si hay falla de red
    """
    body = {"agent_id": agent_id, "sales": sales}
    return _post("/api/sync/upload", body, timeout=TIMEOUT_SHORT)


def program_via_agent(
    green_minutes: int,
    blue_minutes: int,
    red_minutes: int,
    timeout: int = 30,
) -> dict:
    """
    POST /program  directamente al agente (puerto 5555).
    Se usa para combos personalizados, sin pasar por el servidor.

    ADVERTENCIA: bloquea hasta `timeout` segundos esperando la pulsera.
    Llamar SIEMPRE desde un hilo secundario.

    Retorna:
      {"status": "ok",      "programmed_at": "ISO"}
      {"status": "timeout"}
      {"error": "..."}
    """
    # El hardware requiere al menos 1 en cada canal (0 significa "no encender").
    # El clamping a min=1 debe hacerse antes de llamar a esta funcion.
    body = {
        "green_minutes": green_minutes,
        "blue_minutes":  blue_minutes,
        "red_minutes":   red_minutes,
        "timeout":       timeout,
    }
    try:
        token = _get_agent_token()
        headers = {"Authorization": f"Bearer {token}"} if token else {}
        r = httpx.post(f"{_agent_url()}/program", json=body, headers=headers, timeout=TIMEOUT_SELL)
        if r.status_code == 200:
            data = r.json()
            return {"status": "ok", "programmed_at": data.get("programmed_at", "")}
        elif r.status_code == 408:
            return {"status": "timeout"}
        else:
            return {"error": f"Error del agente: HTTP {r.status_code}"}
    except httpx.ConnectError:
        return {"error": "No se puede conectar al agente (puerto 5555). Verifica que el agent este corriendo."}
    except httpx.TimeoutException:
        return {"error": "El agente no responde (timeout de red)."}
    except Exception as e:
        return {"error": str(e)}


def cortesia(package_id: str, cashier: str, motivo: str,
             pin_supervisor: str = "", supervisor: str = "",
             red_minutes: int = None, green_minutes: int = None,
             blue_minutes: int = None) -> dict:
    """
    POST /api/cortesia
    Programa una pulsera de cortesia (amount=0).
    Si pin_supervisor esta vacio, se envia supervisor (pre-validado).

    Retorna:
      {"status": "ok", "folio_number": "...", "supervisor_nombre": "..."}
      {"status": "error", "message": "..."}  (403 PIN invalido, 503 sin cloud)
      {"error": "..."}  si hay falla de red
    ADVERTENCIA: bloquea hasta ~30s esperando la pulsera. Llamar desde hilo secundario.
    Usa httpx directo (no _post) para que errores 403/503 pasen con su JSON.
    """
    body = {
        "package_id":     package_id,
        "pin_supervisor": pin_supervisor,
        "cashier":        cashier,
        "motivo":         motivo,
        "supervisor":     supervisor,
        "agent_id":       get_agent_id(),
    }
    if red_minutes is not None:
        body["red_minutes"] = red_minutes
    if green_minutes is not None:
        body["green_minutes"] = green_minutes
    if blue_minutes is not None:
        body["blue_minutes"] = blue_minutes
    try:
        r = httpx.post(f"{_base_url()}/api/cortesia", json=body, timeout=TIMEOUT_SELL)
        try:
            data = r.json()
        except Exception:
            return {"error": f"HTTP {r.status_code}"}
        # Convertir errores HTTP (FastAPI devuelve {"detail": "..."})
        if r.status_code >= 400:
            return {"error": data.get("detail", f"Error HTTP {r.status_code}")}
        return data
    except httpx.ConnectError:
        return {"error": "No se puede conectar al servidor"}
    except httpx.TimeoutException:
        return {"error": "El servidor no responde (timeout de red)"}
    except Exception as e:
        return {"error": str(e)}


def anular_venta(session_id: str, cashier: str, motivo: str,
                 pin_supervisor: str = "", supervisor: str = "") -> dict:
    """
    POST /api/sell/{session_id}/anular
    Anula una venta activa.
    Si pin_supervisor esta vacio, se envia supervisor (pre-validado).

    Retorna:
      {"status": "ok", "supervisor_nombre": "..."}
      {"status": "error", "message": "..."}  (400 no activa, 403 PIN invalido)
      {"error": "..."}  si hay falla de red
    Usa httpx directo para que errores 400/403 pasen con su JSON.
    """
    body = {
        "pin_supervisor": pin_supervisor,
        "cashier":        cashier,
        "motivo":         motivo,
        "supervisor":     supervisor,
    }
    try:
        r = httpx.post(
            f"{_base_url()}/api/sell/{session_id}/anular",
            json=body, timeout=TIMEOUT_SHORT,
        )
        try:
            return r.json()
        except Exception:
            return {"error": f"HTTP {r.status_code}"}
    except httpx.ConnectError:
        return {"error": "No se puede conectar al servidor"}
    except httpx.TimeoutException:
        return {"error": "El servidor no responde (timeout de red)"}
    except Exception as e:
        return {"error": str(e)}
