# pxy_bots/router.py
import json
import logging
import time  # <-- add
from typing import Dict, Optional, Tuple
from urllib.parse import urlparse

logger = logging.getLogger(__name__)

# Try to use requests; fallback to urllib d
try:
    import requests  # type: ignore
    _HAS_REQUESTS = True
except Exception:
    import urllib.request  # type: ignore
    _HAS_REQUESTS = False


def _is_allowed(url: str, allowed_hosts: Optional[set]) -> Tuple[bool, str]:
    """Allowlist check for DB routes (per-Connection)."""
    try:
        p = urlparse(url)
        if p.scheme not in {"http", "https"}:
            return False, "bad_scheme"
        host = (p.hostname or "").lower()
        return (host in (allowed_hosts or set())), f"host={host}"
    except Exception as e:
        return False, f"invalid_url:{e}"


def _compose_url(base: str, path: str) -> str:
    base = (base or "").rstrip("/")
    path = (path or "").lstrip("/")
    return f"{base}/{path}" if path else base


# -----------------------------
# DB routing (Admin-driven)
# -----------------------------
def pick_db_route(bot_name: str, canon: Dict) -> Optional[Dict]:
    """
    Look up CommandRoute for this bot + trigger/(optional) command.
    Returns: {"url": str, "headers": dict, "timeout": int}
             or None if no active route.
    """
    try:
        # Lazy import to avoid circulars at startup
        from .models import CommandRoute, Connection, TelegramBot  # noqa

        bot = TelegramBot.objects.filter(name=bot_name, is_active=True).first()
        if not bot:
            logger.info("router.no_bot bot=%s", bot_name)
            return None

        trigger = ((canon.get("command") or {}).get("trigger")) or "message"
        cmd = ((canon.get("command") or {}).get("name")) or None
        cmd = (cmd or "").strip().lstrip("/").lower() or None

        logger.info("router.lookup bot=%s trigger=%s cmd=%s", bot.username, trigger, cmd or "")

        qs = (
            CommandRoute.objects
            .select_related("connection")
            .filter(bot=bot, enabled=True, connection__is_active=True, trigger=trigger)
            .order_by("priority", "id")
        )
        # Log a small snapshot of candidates
        snapshot = list(qs.values("command_name", "trigger", "priority", "path", "connection__name")[:10])
        logger.info("router.candidates n=%s sample=%s", qs.count(), snapshot)

        # Prefer exact command; then default (blank/null)
        route = (
            qs.filter(command_name=cmd).first()
            or qs.filter(command_name__isnull=True).first()
            or qs.filter(command_name="").first()
        )
        if not route:
            logger.info("router.no_match bot=%s trigger=%s cmd=%s", bot.username, trigger, cmd or "")
            return None

        conn: Connection = route.connection
        url = _compose_url(conn.base_url, route.path or conn.path_default)

        ok, why = _is_allowed(url, conn.allowed_host_set())
        if not ok:
            logger.warning(
                "router.db.reject url=%s reason=%s allowed=%s route_conn=%s",
                url, why, conn.allowed_host_set(), conn.name
            )
            return None

        headers = {}
        headers.update(conn.auth_headers())
        headers.update(conn.extra_headers())

        logger.info(
            "router.route_ok bot=%s trigger=%s cmd=%s url=%s conn=%s timeout=%ss",
            bot.username, trigger, cmd or "", url, conn.name, conn.timeout_s
        )
        return {"url": url, "headers": headers, "timeout": conn.timeout_s}

    except Exception as e:
        logger.exception("router.db.error: %s", e)
        return None


# -----------------------------
# HTTP POST (DB routes)
# -----------------------------
def post_json(url: str, payload: Dict, timeout: float = 4.0, headers: Optional[Dict] = None) -> Tuple[int, Dict]:
    """
    Blocking POST JSON; never raises.
    Returns (status_code, body_json_or_wrapper).
    `headers` is optional for DB routes.
    """
    hdrs = {"Content-Type": "application/json", **(headers or {})}
    data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
    t0 = time.perf_counter()

    if _HAS_REQUESTS:
        try:
            r = requests.post(url, data=data, headers=hdrs, timeout=timeout)
            dt = (time.perf_counter() - t0) * 1000
            try:
                body = r.json()
                preview = str(body)[:200].replace("\n", " ")
            except Exception:
                body = {"text": r.text[:2000]}
                preview = body["text"][:200].replace("\n", " ")
            logger.info("router.http requests url=%s status=%s t_ms=%.1f body~=%s", url, r.status_code, dt, preview)
            return r.status_code, body
        except Exception as e:
            dt = (time.perf_counter() - t0) * 1000
            logger.exception("router.requests_failed url=%s t_ms=%.1f err=%s", url, dt, e.__class__.__name__)
            return 502, {"ok": False, "error": f"requests_failed:{e.__class__.__name__}"}
    else:
        try:
            req = urllib.request.Request(url, data=data, headers=hdrs, method="POST")
            with urllib.request.urlopen(req, timeout=timeout) as resp:  # nosec
                raw = resp.read(65536)
                dt = (time.perf_counter() - t0) * 1000
                try:
                    body = json.loads(raw.decode("utf-8"))
                    preview = str(body)[:200].replace("\n", " ")
                except Exception:
                    txt = raw.decode("utf-8", errors="replace")[:2000]
                    body = {"text": txt}
                    preview = txt[:200].replace("\n", " ")
                status = getattr(resp, "status", 200)
                logger.info("router.http urllib url=%s status=%s t_ms=%.1f body~=%s", url, status, dt, preview)
                return status, body
        except Exception as e:
            dt = (time.perf_counter() - t0) * 1000
            logger.exception("router.urllib_failed url=%s t_ms=%.1f err=%s", url, dt, e.__class__.__name__)
            return 502, {"ok": False, "error": f"urllib_failed:{e.__class__.__name__}"}
