"""Outgoing webhook connector — POST a JSON payload to a fixed endpoint.

Simpler and safer than the generic HTTP connector for the common "notify an
external system" case: the destination is fixed in config, and if a signing
secret is set the body is signed with HMAC-SHA256 so the receiver can verify it.
Sending is sensitive and routes through the policy engine.
"""

from __future__ import annotations

import hashlib
import hmac
import json as jsonlib

import httpx

from legate.agent.tooling import ToolContext, ToolResult, ToolSpec
from legate.connectors.base import BaseConnector, ConnectionResult, ConnectorConfigError
from legate.connectors.runtime import new_async_client
from legate.security.ssrf import SSRFError, validate_public_url

_DEFAULT_SIGNATURE_HEADER = "X-Legate-Signature"


class WebhookConnector(BaseConnector):
    type = "webhook"
    title = "Outgoing Webhook"
    config_schema = {
        "type": "object",
        "properties": {
            "url": {"type": "string", "description": "Destination URL for the POST."},
            "signature_header": {"type": "string", "description": "Header carrying the HMAC."},
        },
        "required": ["url"],
    }
    secret_schema = {
        "type": "object",
        "properties": {
            "signing_secret": {"type": "string", "description": "Optional HMAC-SHA256 secret."}
        },
    }

    def validate_config(self, config: dict) -> None:
        super().validate_config(config)
        url = config.get("url", "")
        if not (url.startswith("http://") or url.startswith("https://")):
            raise ConnectorConfigError("url must be an absolute http(s) URL")

    def list_tools(self) -> list[ToolSpec]:
        return [
            ToolSpec(
                name="webhook.send",
                description="POST a JSON payload to the configured webhook URL.",
                parameters={
                    "type": "object",
                    "properties": {
                        "payload": {"type": "object", "description": "JSON body to send."},
                        "text": {"type": "string", "description": 'Shortcut for {"text": ...}.'},
                    },
                },
                is_sensitive=True,
                scopes=["webhook:write"],
            )
        ]

    @staticmethod
    def _sign(secret: str, body: bytes) -> str:
        digest = hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
        return f"sha256={digest}"

    async def test_connection(self, config: dict, secrets: dict) -> ConnectionResult:
        # Deliberately does not fire the webhook (that could trigger a real action);
        # it only confirms the target is configured.
        if config.get("url"):
            return ConnectionResult(ok=True, message="webhook target configured")
        return ConnectionResult(ok=False, message="no url configured")

    async def execute(
        self, tool: str, args: dict, config: dict, secrets: dict, ctx: ToolContext
    ) -> ToolResult:
        if tool != "webhook.send":
            return ToolResult(ok=False, error=f"unknown webhook tool: {tool}")
        url = config.get("url")
        if not url:
            return ToolResult(ok=False, error="no url configured")
        try:
            validate_public_url(url)
        except SSRFError as exc:
            return ToolResult(ok=False, error=str(exc))

        payload = args.get("payload")
        if payload is None:
            payload = {"text": args.get("text", "")}
        body = jsonlib.dumps(payload, separators=(",", ":")).encode("utf-8")

        headers = {"Content-Type": "application/json"}
        secret = secrets.get("signing_secret")
        if secret:
            header_name = config.get("signature_header") or _DEFAULT_SIGNATURE_HEADER
            headers[header_name] = self._sign(secret, body)

        try:
            async with new_async_client() as client:
                resp = await client.post(url, content=body, headers=headers)
        except httpx.HTTPError as exc:
            return ToolResult(ok=False, error=f"webhook request failed: {exc}")

        return ToolResult(
            ok=resp.status_code < 400,
            data={"status_code": resp.status_code, "signed": bool(secret)},
            error=None if resp.status_code < 400 else f"HTTP {resp.status_code}",
        )
