"""Generic webhook hooks for calling external APIs.

Customers expose a REST or WebSocket endpoint; RoohAI calls it as the LLM hook.
Supports both batch (REST POST) and streaming (WebSocket) modes.

Authentication uses an auto-generated API key sent as:
  - REST / SSE: x-roohai-key header
  - WebSocket: x-roohai-key field in the JSON payload

Usage in agent YAML:
    llm_hook: roohai.webhook.rest_hook
    llm_stream_hook: roohai.webhook.ws_stream_hook

    webhook:
      url: https://customer.com/api/chat
      ws_url: wss://customer.com/ws/chat     # optional, for streaming
      api_key: rh-xxxxxxxxxxxxxxxxxxxxxxxx   # auto-generated
      timeout: 30                            # optional, seconds
"""

from __future__ import annotations

import json
import logging
import secrets
import string
from typing import Any

import aiohttp

logger = logging.getLogger(__name__)

# Module-level config — set by the agent activation code
_config: dict[str, Any] = {}


def generate_api_key() -> str:
    """Generate a RoohAI webhook API key: rh-<24 alphanumeric chars>."""
    alphabet = string.ascii_lowercase + string.digits
    random_part = "".join(secrets.choice(alphabet) for _ in range(24))
    return f"rh-{random_part}"


_MAX_TIMEOUT = 60


def configure(
    url: str | None = None,
    ws_url: str | None = None,
    api_key: str | None = None,
    timeout: int = 30,
    **kwargs: Any,
) -> None:
    """Set webhook config. Called during agent activation."""
    clamped = max(1, min(_MAX_TIMEOUT, int(timeout)))
    _config.update({
        "url": url,
        "ws_url": ws_url,
        "api_key": api_key,
        "timeout": clamped,
        **kwargs,
    })


def _headers() -> dict[str, str]:
    headers = {"Content-Type": "application/json"}
    if _config.get("api_key"):
        headers["x-roohai-key"] = _config["api_key"]
    return headers


def _payload(
    message: str,
    history: list[dict] | None,
    system_prompt: str | None,
    session_id: str | None,
) -> dict:
    payload: dict[str, Any] = {
        "message": message,
        "history": history or [],
        "system_prompt": system_prompt or "",
        "session_id": session_id or "",
    }
    # For WebSocket, include the API key in the payload
    if _config.get("api_key"):
        payload["x-roohai-key"] = _config["api_key"]
    return payload


# ── REST batch hook ───────────────────────────────────────────────────────────

async def rest_hook(
    message: str,
    *,
    history: list[dict] | None = None,
    system_prompt: str | None = None,
    session_id: str | None = None,
) -> str:
    """Batch hook — POST to customer REST endpoint, return response text."""
    url = _config.get("url")
    if not url:
        raise ValueError("webhook url not configured")

    timeout = aiohttp.ClientTimeout(total=_config.get("timeout", 30))
    payload = _payload(message, history, system_prompt, session_id)

    async with aiohttp.ClientSession(timeout=timeout) as session:
        async with session.post(url, json=payload, headers=_headers()) as resp:
            if resp.status != 200:
                body = await resp.text()
                # Try to extract a clean error message from JSON response
                try:
                    err_data = json.loads(body)
                    detail = err_data.get("error") or err_data.get("detail") or body
                except (json.JSONDecodeError, AttributeError):
                    detail = body
                raise RuntimeError(f"Webhook error {resp.status}: {detail}")
            data = await resp.json()
            # Support both {"response": "..."} and {"text": "..."} formats
            return data.get("response") or data.get("text") or data.get("message") or json.dumps(data)


# ── REST streaming hook (SSE / chunked) ──────────────────────────────────────

async def rest_stream_hook(
    message: str,
    *,
    history: list[dict] | None = None,
    system_prompt: str | None = None,
    session_id: str | None = None,
):
    """Streaming hook — POST to REST endpoint, yield chunks from SSE or chunked response."""
    url = _config.get("url")
    if not url:
        raise ValueError("webhook url not configured")

    timeout = aiohttp.ClientTimeout(total=_config.get("timeout", 30))
    payload = _payload(message, history, system_prompt, session_id)
    headers = _headers()
    headers["Accept"] = "text/event-stream"

    async with aiohttp.ClientSession(timeout=timeout) as session:
        async with session.post(url, json=payload, headers=headers) as resp:
            if resp.status != 200:
                body = await resp.text()
                try:
                    err_data = json.loads(body)
                    detail = err_data.get("error") or err_data.get("detail") or body
                except (json.JSONDecodeError, AttributeError):
                    detail = body
                raise RuntimeError(f"Webhook stream error {resp.status}: {detail}")

            async for line in resp.content:
                text = line.decode("utf-8").strip()
                if not text:
                    continue
                # SSE format: "data: ..."
                if text.startswith("data:"):
                    data = text[5:].strip()
                    if data == "[DONE]":
                        break
                    try:
                        chunk = json.loads(data)
                        if chunk.get("error"):
                            raise RuntimeError(f"Webhook endpoint error: {chunk['error']}")
                        token = chunk.get("text") or chunk.get("token") or chunk.get("delta", "")
                        if token:
                            yield token
                    except json.JSONDecodeError:
                        # Plain text SSE
                        if data:
                            yield data
                else:
                    # Plain chunked response
                    yield text


# ── WebSocket streaming hook ─────────────────────────────────────────────────

async def ws_stream_hook(
    message: str,
    *,
    history: list[dict] | None = None,
    system_prompt: str | None = None,
    session_id: str | None = None,
):
    """Streaming hook — connect to customer WebSocket, send message, yield response chunks."""
    ws_url = _config.get("ws_url") or _config.get("url")
    if not ws_url:
        raise ValueError("webhook ws_url not configured")

    # Convert http(s) to ws(s) if needed
    if ws_url.startswith("http://"):
        ws_url = "ws://" + ws_url[7:]
    elif ws_url.startswith("https://"):
        ws_url = "wss://" + ws_url[8:]

    timeout = aiohttp.ClientTimeout(total=_config.get("timeout", 30))
    # _payload already includes x-roohai-key for WS auth
    payload = _payload(message, history, system_prompt, session_id)

    async with aiohttp.ClientSession(timeout=timeout) as session:
        async with session.ws_connect(ws_url) as ws:
            await ws.send_json(payload)

            async for msg in ws:
                if msg.type == aiohttp.WSMsgType.TEXT:
                    data = msg.data.strip()
                    if data == "[DONE]" or data == "":
                        break
                    try:
                        chunk = json.loads(data)
                        if chunk.get("error"):
                            raise RuntimeError(f"Webhook endpoint error: {chunk['error']}")
                        token = chunk.get("text") or chunk.get("token") or chunk.get("delta", "")
                        if token:
                            yield token
                        if chunk.get("done"):
                            break
                    except json.JSONDecodeError:
                        # Plain text
                        yield data
                elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
                    break
