"""
Webhook LLM Adapter (Phase 2)

Routes LLM calls to an external HTTP webhook endpoint (n8n, Flowise, custom).
Enables no-code/low-code AI workflow integration with the voice pipeline.

Request format (POST):
    {
        "messages": [{"role": "system", "content": "..."}, ...],
        "model": "...",
        "maxTokens": 1024,
        "temperature": 0.7
    }

Expected response (JSON):
    {"text": "Assistant's response"}

Or plain text body.

Example:
    from dvgateway.adapters.llm.webhook import WebhookAdapter
    from dvgateway.adapters.llm.anthropic import AnthropicAdapter

    webhook = WebhookAdapter(
        url="https://n8n.company.com/webhook/voice-bot",
        timeout=5.0,
        fallback=AnthropicAdapter(api_key="..."),
    )
"""

from __future__ import annotations

import hashlib
import hmac
import json
from dataclasses import dataclass, field
from typing import AsyncIterable

import aiohttp

from dvgateway.types import LlmAdapter, LlmOptions, Message


@dataclass
class WebhookAdapterOptions:
    """Configuration for WebhookAdapter."""

    url: str
    """Webhook endpoint URL."""

    timeout: float = 5.0
    """Request timeout in seconds (default: 5.0)."""

    secret: str = ""
    """HMAC-SHA256 secret for request signing."""

    fallback: LlmAdapter | None = None
    """Fallback LLM adapter when webhook is unreachable."""

    headers: dict[str, str] = field(default_factory=dict)
    """Custom HTTP headers."""

    system_prompt: str = "You are a helpful voice assistant."
    """Default system prompt (exposed for pipeline builder)."""


class WebhookAdapter(LlmAdapter):
    """LLM adapter that routes to an external webhook endpoint."""

    def __init__(
        self,
        url: str = "",
        timeout: float = 5.0,
        secret: str = "",
        fallback: LlmAdapter | None = None,
        headers: dict[str, str] | None = None,
        system_prompt: str = "You are a helpful voice assistant.",
        *,
        opts: WebhookAdapterOptions | None = None,
    ) -> None:
        if opts:
            self.url = opts.url
            self.timeout = opts.timeout
            self.secret = opts.secret
            self.fallback = opts.fallback
            self.custom_headers = opts.headers
            self.system_prompt = opts.system_prompt
        else:
            self.url = url
            self.timeout = timeout
            self.secret = secret
            self.fallback = fallback
            self.custom_headers = headers or {}
            self.system_prompt = system_prompt

    async def chat(
        self, messages: list[Message], options: LlmOptions | None = None
    ) -> AsyncIterable[str]:
        try:
            response = await self._call_webhook(messages, options)
            yield response
        except Exception:
            if self.fallback:
                async for token in self.fallback.chat(messages, options):
                    yield token
            else:
                raise

    async def _call_webhook(
        self, messages: list[Message], options: LlmOptions | None = None
    ) -> str:
        payload = {
            "messages": [{"role": m.role, "content": m.content} for m in messages],
            "model": options.model if options else None,
            "maxTokens": options.max_tokens if options else None,
            "temperature": options.temperature if options else None,
        }
        body = json.dumps(payload)

        headers: dict[str, str] = {
            "Content-Type": "application/json",
            **self.custom_headers,
        }

        # HMAC-SHA256 signing
        if self.secret:
            sig = hmac.new(
                self.secret.encode(), body.encode(), hashlib.sha256
            ).hexdigest()
            headers["X-Webhook-Signature"] = f"sha256={sig}"

        timeout = aiohttp.ClientTimeout(total=self.timeout)
        async with aiohttp.ClientSession(timeout=timeout) as session:
            async with session.post(self.url, headers=headers, data=body) as resp:
                if resp.status >= 400:
                    text = await resp.text()
                    raise RuntimeError(f"Webhook returned HTTP {resp.status}: {text}")

                text = await resp.text()
                content_type = resp.headers.get("content-type", "")

                if "application/json" in content_type:
                    try:
                        data = json.loads(text)
                        for key in ("text", "response", "message", "content"):
                            if isinstance(data.get(key), str):
                                return data[key]
                        # OpenAI-compatible format
                        if (
                            isinstance(data.get("choices"), list)
                            and data["choices"]
                            and isinstance(data["choices"][0].get("message"), dict)
                        ):
                            return data["choices"][0]["message"].get("content", "")
                        return json.dumps(data)
                    except json.JSONDecodeError:
                        return text

                return text
