"""
Agent Bridge — Calls 3rd party agents (local sandboxed or remote hosted).

Two modes:
  - Local agents: Downloaded from marketplace, run in sandbox on user's machine.
    Called via HTTP to localhost:{port}.
  - Remote agents: Hosted by the creator on their own servers. Called via HTTPS
    with marketplace auth token. Creator keeps code private, we take 30% commission.

Both modes return a standardised BridgeResponse.

All stdlib — no external packages.
"""

from __future__ import annotations

import json
import logging
import ssl
import time
import urllib.error
import urllib.request
from typing import Any, Generator, Optional

from .local_bridge import BridgeResponse, _estimate_tokens

log = logging.getLogger("brynq.runtime.agent_bridge")

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

_LOCAL_TIMEOUT = 60  # seconds — local agents should be fast
_REMOTE_TIMEOUT = 120  # seconds — remote agents may need more time
_CONNECT_TIMEOUT = 10  # seconds — fail fast if agent is unreachable


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------


def _build_ssl_context() -> ssl.SSLContext:
    """Return a default SSL context for HTTPS requests."""
    return ssl.create_default_context()


def _http_post(
    url: str,
    body: dict,
    headers: Optional[dict[str, str]] = None,
    timeout: int = _LOCAL_TIMEOUT,
) -> dict:
    """Make an HTTP POST request and return parsed JSON.

    Raises RuntimeError on HTTP or network errors.
    """
    hdrs: dict[str, str] = {
        "Content-Type": "application/json",
        "Accept": "application/json",
    }
    if headers:
        hdrs.update(headers)

    data = json.dumps(body).encode("utf-8")
    req = urllib.request.Request(url, data=data, headers=hdrs, method="POST")
    context = _build_ssl_context() if url.startswith("https://") else None

    try:
        with urllib.request.urlopen(req, timeout=timeout, context=context) as resp:
            raw = resp.read().decode("utf-8")
            return json.loads(raw) if raw.strip() else {}
    except urllib.error.HTTPError as exc:
        detail = ""
        try:
            detail = exc.read().decode("utf-8", errors="replace")
        except Exception:
            pass
        raise RuntimeError(f"Agent returned HTTP {exc.code}: {detail}") from exc
    except urllib.error.URLError as exc:
        raise RuntimeError(f"Cannot reach agent at {url}: {exc.reason}") from exc


def _probe_local_agent(endpoint: str) -> bool:
    """Check if a local agent is running at the given endpoint.

    Tries a GET to the endpoint's health path. Returns True if responsive.
    """
    # Normalise endpoint
    base = endpoint.rstrip("/")
    if not base.startswith("http"):
        base = f"http://{base}"

    # Try /health first, then root
    for path in ("/health", "/"):
        try:
            url = f"{base}{path}"
            req = urllib.request.Request(url, method="GET")
            with urllib.request.urlopen(req, timeout=_CONNECT_TIMEOUT):
                return True
        except Exception:
            continue

    return False


# ---------------------------------------------------------------------------
# Agent Bridge
# ---------------------------------------------------------------------------


class AgentBridge:
    """Bridge to 3rd party agents, both local and remote.

    Local agents run in a sandbox on the user's machine and are called
    via HTTP to localhost:{port}. Remote agents run on the creator's
    servers and are called via HTTPS with a marketplace auth token.
    """

    def call_local(
        self,
        agent_id: str,
        endpoint: str,
        prompt: str,
        config: Optional[dict] = None,
    ) -> BridgeResponse:
        """Call a local (sandboxed) agent.

        The agent is expected to expose a JSON endpoint that accepts:
            {"prompt": "...", "config": {...}}
        and returns:
            {"output": "...", "metadata": {...}}

        Args:
            agent_id: Marketplace agent identifier.
            endpoint: Local endpoint (e.g. "localhost:9100" or
                      "http://localhost:9100/v1/chat").
            prompt: The prompt to send to the agent.
            config: Optional config dict (forwarded to agent).

        Returns:
            BridgeResponse with the agent's output or error details.
        """
        config = config or {}
        response = BridgeResponse(
            model=agent_id,
            backend="local_agent",
        )
        t0 = time.monotonic()

        try:
            # Normalise endpoint URL
            base = endpoint.rstrip("/")
            if not base.startswith("http"):
                base = f"http://{base}"

            # Check if agent is running
            if not _probe_local_agent(endpoint):
                raise ConnectionError(
                    f"Local agent '{agent_id}' is not running at {endpoint}. "
                    "Check that the agent is started."
                )

            # Build request body
            body: dict[str, Any] = {
                "prompt": prompt,
            }
            if config:
                body["config"] = config

            # Determine the call URL. If the endpoint already contains a path
            # beyond the host:port, use it directly. Otherwise, try standard paths.
            url = base
            if base.count("/") <= 2:
                # Just host:port, no specific path — use the standard agent path
                url = f"{base}/v1/chat"

            resp = _http_post(url, body, timeout=_LOCAL_TIMEOUT)

            # Extract output — agents should return {"output": "..."} but we
            # also handle {"response": "..."} and {"message": "..."} and
            # {"choices": [{"message": {"content": "..."}}]} for flexibility.
            output = _extract_output(resp)

            response.output = output
            response.success = True
            response.token_estimate = _estimate_tokens(output)
            response.metadata = resp.get("metadata", {})

        except ConnectionError as exc:
            response.success = False
            response.error = f"Agent unreachable: {exc}"
            log.error("Agent bridge (local) connection error for '%s': %s", agent_id, exc)

        except RuntimeError as exc:
            response.success = False
            response.error = f"Agent error: {exc}"
            log.error("Agent bridge (local) runtime error for '%s': %s", agent_id, exc)

        except Exception as exc:
            response.success = False
            response.error = f"Unexpected error: {exc}"
            log.error(
                "Agent bridge (local) unexpected error for '%s': %s",
                agent_id, exc, exc_info=True,
            )

        finally:
            response.elapsed_seconds = time.monotonic() - t0

        return response

    def call_remote(
        self,
        agent_id: str,
        endpoint: str,
        auth_token: str,
        prompt: str,
        config: Optional[dict] = None,
    ) -> BridgeResponse:
        """Call a remote (creator-hosted) agent.

        The agent is expected to expose an HTTPS endpoint that accepts:
            {"prompt": "...", "config": {...}}
        with Authorization header, and returns:
            {"output": "...", "metadata": {...}}

        Args:
            agent_id: Marketplace agent identifier.
            endpoint: Full HTTPS URL (e.g. "https://api.acme-agents.com/v1/review").
            auth_token: Marketplace auth token for this agent.
            prompt: The prompt to send to the agent.
            config: Optional config dict (forwarded to agent).

        Returns:
            BridgeResponse with the agent's output or error details.
        """
        config = config or {}
        response = BridgeResponse(
            model=agent_id,
            backend="remote_agent",
        )
        t0 = time.monotonic()

        try:
            if not auth_token:
                raise RuntimeError(
                    f"No auth token for remote agent '{agent_id}'. "
                    "Install the agent from the marketplace first."
                )

            # Validate endpoint is HTTPS for remote agents
            if not endpoint.startswith("https://"):
                raise RuntimeError(
                    f"Remote agent endpoint must use HTTPS. Got: {endpoint}"
                )

            # Build request body
            body: dict[str, Any] = {
                "prompt": prompt,
            }
            if config:
                body["config"] = config

            # Make the call with auth header
            resp = _http_post(
                endpoint,
                body,
                headers={
                    "Authorization": f"Bearer {auth_token}",
                    "X-Brynq-Agent-Id": agent_id,
                },
                timeout=_REMOTE_TIMEOUT,
            )

            # Extract output
            output = _extract_output(resp)

            response.output = output
            response.success = True
            response.token_estimate = _estimate_tokens(output)
            response.metadata = resp.get("metadata", {})

        except RuntimeError as exc:
            response.success = False
            error_str = str(exc)
            if "401" in error_str or "403" in error_str:
                response.error = (
                    f"Auth failed for agent '{agent_id}'. "
                    "Your marketplace token may have expired."
                )
            elif "404" in error_str:
                response.error = (
                    f"Agent '{agent_id}' not found at {endpoint}. "
                    "The agent may have been removed by its creator."
                )
            elif "429" in error_str:
                response.error = (
                    f"Rate limit exceeded for agent '{agent_id}'. "
                    "Try again in a moment."
                )
            else:
                response.error = f"Remote agent error: {exc}"
            log.error("Agent bridge (remote) error for '%s': %s", agent_id, exc)

        except Exception as exc:
            response.success = False
            response.error = f"Unexpected error: {exc}"
            log.error(
                "Agent bridge (remote) unexpected error for '%s': %s",
                agent_id, exc, exc_info=True,
            )

        finally:
            response.elapsed_seconds = time.monotonic() - t0

        return response


def _extract_output(resp: dict) -> str:
    """Extract text output from an agent response dict.

    Agents may return output in different formats. We try several
    common patterns in priority order.
    """
    # Standard: {"output": "..."}
    if "output" in resp:
        return str(resp["output"])

    # Alternative: {"response": "..."}
    if "response" in resp:
        return str(resp["response"])

    # Alternative: {"message": "..."}
    if "message" in resp and isinstance(resp["message"], str):
        return resp["message"]

    # OpenAI-compatible: {"choices": [{"message": {"content": "..."}}]}
    choices = resp.get("choices", [])
    if choices and isinstance(choices, list):
        content = choices[0].get("message", {}).get("content", "")
        if content:
            return str(content)

    # Alternative: {"result": "..."}
    if "result" in resp:
        return str(resp["result"])

    # Last resort: dump the whole response
    if resp:
        return json.dumps(resp, default=str)

    return ""
