"""
Vision sidecar client.

Set VISION_SIDECAR_URL env var to enable.
If unset or empty, all calls return None and the agent
behaves identically to before (full LLM vision always).

All errors return None silently — caller must treat None
as "escalate to full LLM vision". This is intentional:
a broken sidecar must never degrade agent correctness.
"""
import asyncio
import hashlib
import os
import time
from dataclasses import dataclass
from typing import Optional

import httpx

SIDECAR_URL: str = os.getenv("VISION_SIDECAR_URL", "").rstrip("/")
_TIMEOUT = httpx.Timeout(2.0)   # hard ceiling — never block >2s
_CACHE_SIZE = 5                  # cache last N results by image hash

@dataclass
class SidecarResult:
    description: str
    should_escalate: bool
    escalation_reason: Optional[str]
    confidence: float
    detected_signals: dict
    inference_ms: int

# Module-level persistent client (reuses connection pool)
_client: Optional[httpx.AsyncClient] = None

def _get_client() -> Optional[httpx.AsyncClient]:
    global _client
    if not SIDECAR_URL:
        return None
    if _client is None or _client.is_closed:
        _client = httpx.AsyncClient(
            base_url=SIDECAR_URL,
            timeout=_TIMEOUT
        )
    return _client

# Simple LRU-style cache: {md5_hash: SidecarResult}
_cache: dict[str, SidecarResult] = {}
_cache_order: list[str] = []

def _cache_get(image_hash: str) -> Optional[SidecarResult]:
    return _cache.get(image_hash)

def _cache_set(image_hash: str, result: SidecarResult) -> None:
    if image_hash in _cache:
        return
    _cache[image_hash] = result
    _cache_order.append(image_hash)
    if len(_cache_order) > _CACHE_SIZE:
        oldest = _cache_order.pop(0)
        _cache.pop(oldest, None)

async def analyze_frame(
    screenshot_b64: str,
    last_action_type: str = "none",
    session_id: str = "default",
) -> Optional[SidecarResult]:
    """
    Returns SidecarResult if sidecar is reachable and responds.
    Returns None if sidecar is disabled, unreachable, or errors.
    Caller must treat None as "use full LLM vision".
    Never raises.
    """
    client = _get_client()
    if client is None:
        return None

    # Skip cache for mutating actions — always get fresh result
    MUTATING = {"click", "type", "submit", "select",
                "upload", "press_key", "fill"}
    use_cache = last_action_type not in MUTATING

    if use_cache:
        image_hash = hashlib.md5(
            screenshot_b64[:2000].encode()
        ).hexdigest()
        cached = _cache_get(image_hash)
        if cached is not None:
            return cached

    try:
        resp = await client.post("/analyze", json={
            "screenshot_b64": screenshot_b64,
            "last_action_type": last_action_type,
            "session_id": session_id,
        })
        resp.raise_for_status()
        data = resp.json()
        result = SidecarResult(
            description=data["description"],
            should_escalate=data["should_escalate"],
            escalation_reason=data.get("escalation_reason"),
            confidence=data["confidence"],
            detected_signals=data["detected_signals"],
            inference_ms=data["inference_ms"],
        )
        if use_cache:
            _cache_set(image_hash, result)
        return result

    except Exception:
        # Fail open — sidecar error = escalate to full LLM vision
        return None

async def close() -> None:
    """Call on agent shutdown to cleanly close the connection pool."""
    global _client
    if _client and not _client.is_closed:
        await _client.aclose()
        _client = None