# 中文注释：
# 文件：echobot/agent/hooks.py
# 说明：Hook 注册中心与 webhook 分发/验签能力。

"""Hook 管理与 webhook 集成。"""

from __future__ import annotations

import asyncio
import hashlib
import hmac
import json
import time
import uuid
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any, Awaitable, Callable, Mapping

import httpx
from loguru import logger

from echobot.utils.helpers import ensure_dir

HookHandler = Callable[[str, dict[str, Any]], Awaitable[None] | None]


def _utc_ts() -> int:
    """返回当前 Unix 时间戳（秒）。"""
    return int(time.time())


def _match_event(patterns: list[str], event: str) -> bool:
    """
    判断事件是否命中订阅模式。

    支持：
    - `*`：匹配全部
    - `prefix*`：前缀匹配
    - `exact`：精确匹配
    """
    for pattern in patterns:
        p = pattern.strip()
        if p == "*":
            return True
        if p.endswith("*") and event.startswith(p[:-1]):
            return True
        if p == event:
            return True
    return False


@dataclass
class WebhookSubscription:
    """Webhook 订阅项定义。"""
    id: str
    url: str
    events: list[str] = field(default_factory=lambda: ["*"])
    secret: str = ""
    headers: dict[str, str] = field(default_factory=dict)
    max_retries: int = 2
    enabled: bool = True
    created_at: int = field(default_factory=_utc_ts)

    def to_dict(self) -> dict[str, Any]:
        return asdict(self)


class WebhookDispatcher:
    """
    出站 webhook 分发器。

    负责：
    - 订阅持久化（JSON 文件）
    - 按事件匹配目标
    - HTTP 发送与失败重试
    - 可选 HMAC 签名
    """

    def __init__(self, workspace: Path, timeout_seconds: float = 8.0):
        # 订阅存储在工作区内，便于实例隔离和迁移。
        self.timeout_seconds = timeout_seconds
        self.store_path = ensure_dir(workspace / "webhooks") / "subscriptions.json"
        self._subscriptions: dict[str, WebhookSubscription] = {}
        self._load()

    def _load(self) -> None:
        """从磁盘加载订阅配置。"""
        if not self.store_path.exists():
            return
        try:
            data = json.loads(self.store_path.read_text(encoding="utf-8"))
            if not isinstance(data, list):
                return
            for item in data:
                if not isinstance(item, dict):
                    continue
                sub = WebhookSubscription(
                    id=str(item.get("id", f"wh_{uuid.uuid4().hex[:10]}")),
                    url=str(item.get("url", "")),
                    events=[str(e) for e in item.get("events", ["*"])],
                    secret=str(item.get("secret", "")),
                    headers={str(k): str(v) for k, v in (item.get("headers") or {}).items()},
                    max_retries=int(item.get("max_retries", 2)),
                    enabled=bool(item.get("enabled", True)),
                    created_at=int(item.get("created_at", _utc_ts())),
                )
                if sub.url:
                    self._subscriptions[sub.id] = sub
        except Exception as e:
            logger.warning(f"Failed to load webhook subscriptions: {e}")

    def _save(self) -> None:
        """将内存中的订阅配置持久化到磁盘。"""
        try:
            payload = [s.to_dict() for s in self._subscriptions.values()]
            self.store_path.write_text(
                json.dumps(payload, ensure_ascii=False, indent=2),
                encoding="utf-8",
            )
        except Exception as e:
            logger.warning(f"Failed to save webhook subscriptions: {e}")

    def list_subscriptions(self) -> list[dict[str, Any]]:
        """列出当前所有订阅（按创建时间倒序）。"""
        items = [s.to_dict() for s in self._subscriptions.values()]
        items.sort(key=lambda x: x["created_at"], reverse=True)
        return items

    def add_subscription(
        self,
        url: str,
        events: list[str] | None = None,
        secret: str = "",
        headers: dict[str, str] | None = None,
        max_retries: int = 2,
        enabled: bool = True,
    ) -> dict[str, Any]:
        """新增订阅并持久化。"""
        sub = WebhookSubscription(
            id=f"wh_{uuid.uuid4().hex[:10]}",
            url=url,
            events=events or ["*"],
            secret=secret,
            headers=headers or {},
            max_retries=max(0, min(max_retries, 5)),
            enabled=enabled,
        )
        self._subscriptions[sub.id] = sub
        self._save()
        return sub.to_dict()

    def remove_subscription(self, subscription_id: str) -> bool:
        """删除指定订阅。"""
        removed = self._subscriptions.pop(subscription_id, None)
        if removed:
            self._save()
            return True
        return False

    def has_targets(self, event: str) -> bool:
        """Check if event has any enabled matching webhook subscribers."""
        return any(
            sub.enabled and _match_event(sub.events, event)
            for sub in self._subscriptions.values()
        )

    async def dispatch(self, event: str, payload: dict[str, Any]) -> None:
        """
        向匹配订阅分发事件。

        注意：
        - 单次事件对多目标并发发送；
        - 某个目标失败不会影响其他目标。
        """
        targets = [
            sub
            for sub in self._subscriptions.values()
            if sub.enabled and _match_event(sub.events, event)
        ]
        if not targets:
            return

        timestamp = str(_utc_ts())
        # 统一封装标准事件包，便于接收端解析和追踪。
        envelope = {"event": event, "timestamp": timestamp, "payload": payload}
        body_text = json.dumps(envelope, ensure_ascii=False, default=str)
        body_bytes = body_text.encode("utf-8")

        await asyncio.gather(*(self._deliver(sub, event, timestamp, body_text, body_bytes) for sub in targets))

    async def _deliver(
        self,
        sub: WebhookSubscription,
        event: str,
        timestamp: str,
        body_text: str,
        body_bytes: bytes,
    ) -> None:
        """
        将一条事件投递到单个订阅目标。

        策略：
        - 最多重试 `max_retries` 次
        - 指数退避短延迟
        - 重试耗尽后仅告警，不抛异常阻塞主流程
        """
        headers = {
            "Content-Type": "application/json",
            "X-Echobot-Event": event,
            "X-Echobot-Timestamp": timestamp,
        }
        headers.update(sub.headers)

        if sub.secret:
            # 签名内容：`timestamp.body`，用于防篡改与防重放窗口校验。
            signature_payload = f"{timestamp}.{body_text}".encode("utf-8")
            signature = hmac.new(
                sub.secret.encode("utf-8"),
                signature_payload,
                hashlib.sha256,
            ).hexdigest()
            headers["X-Echobot-Signature"] = signature

        for attempt in range(sub.max_retries + 1):
            try:
                async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
                    response = await client.post(sub.url, content=body_bytes, headers=headers)
                    response.raise_for_status()
                return
            except Exception as e:
                if attempt >= sub.max_retries:
                    logger.warning(
                        f"Webhook delivery failed (id={sub.id}, event={event}, url={sub.url}): {e}"
                    )
                    return
                await asyncio.sleep(0.2 * (2**attempt))


class InboundWebhookVerifier:
    """
    入站 webhook 验签器。

    能力：
    - HMAC 签名校验
    - 时间窗口校验（防旧包重放）
    - 事件 ID 去重（防重复投递）
    """

    def __init__(
        self,
        secret: str = "",
        max_skew_seconds: int = 300,
        replay_ttl_seconds: int = 3600,
    ):
        self.secret = secret
        self.max_skew_seconds = max(60, max_skew_seconds)
        self.replay_ttl_seconds = max(300, replay_ttl_seconds)
        self._seen_event_ids: dict[str, int] = {}

    def set_secret(self, secret: str) -> None:
        """动态更新验签密钥。"""
        self.secret = secret

    def _cleanup_replay_cache(self, now_ts: int) -> None:
        """清理重放缓存中过期事件。"""
        threshold = now_ts - self.replay_ttl_seconds
        stale = [event_id for event_id, ts in self._seen_event_ids.items() if ts < threshold]
        for event_id in stale:
            self._seen_event_ids.pop(event_id, None)

    def verify(self, headers: Mapping[str, str], body: bytes) -> tuple[bool, str]:
        """
        校验请求合法性。

        返回 `(ok, reason)`，其中 `reason` 便于 API 层直接回传诊断信息。
        """
        if not self.secret:
            return False, "Webhook secret is not configured."

        timestamp_raw = (
            headers.get("x-echobot-timestamp")
            or headers.get("X-Echobot-Timestamp")
            or ""
        )
        signature = (
            headers.get("x-echobot-signature")
            or headers.get("X-Echobot-Signature")
            or ""
        )
        if not timestamp_raw or not signature:
            # 不接受无签名请求，避免匿名调用。
            return False, "Missing webhook signature headers."

        try:
            timestamp = int(timestamp_raw)
        except ValueError:
            return False, "Invalid webhook timestamp."

        now_ts = _utc_ts()
        if abs(now_ts - timestamp) > self.max_skew_seconds:
            # 时间偏差过大，拒绝请求，降低重放风险。
            return False, "Webhook timestamp is outside accepted window."

        body_text = body.decode("utf-8", errors="replace")
        payload = f"{timestamp}.{body_text}".encode("utf-8")
        expected = hmac.new(self.secret.encode("utf-8"), payload, hashlib.sha256).hexdigest()
        if not hmac.compare_digest(expected, signature):
            return False, "Webhook signature mismatch."

        event_id = (
            headers.get("x-echobot-event-id")
            or headers.get("X-Echobot-Event-Id")
            or hashlib.sha256(payload).hexdigest()
        )
        self._cleanup_replay_cache(now_ts)
        if event_id in self._seen_event_ids:
            return False, "Duplicate webhook event."

        self._seen_event_ids[event_id] = now_ts
        return True, "ok"


class HookManager:
    """
    应用内 Hook 注册中心。

    提供两类扩展：
    1. 进程内处理器（本地回调）
    2. 出站 webhook（跨系统事件订阅）
    """

    def __init__(self, workspace: Path, inbound_secret: str = ""):
        # `_handlers` 结构：event -> [handler1, handler2, ...]
        self._handlers: dict[str, list[HookHandler]] = {}
        self.webhooks = WebhookDispatcher(workspace=workspace)
        self.inbound_verifier = InboundWebhookVerifier(secret=inbound_secret)

    def register(self, event: str, handler: HookHandler) -> None:
        """注册一个事件处理器。"""
        handlers = self._handlers.setdefault(event, [])
        handlers.append(handler)

    def unregister(self, event: str, handler: HookHandler) -> None:
        """注销一个事件处理器。"""
        handlers = self._handlers.get(event)
        if not handlers:
            return
        try:
            handlers.remove(handler)
        except ValueError:
            return
        if not handlers:
            self._handlers.pop(event, None)

    async def emit(self, event: str, payload: dict[str, Any]) -> None:
        """
        发布事件：
        1. 先执行本地处理器
        2. 再异步触发 webhook 分发

        约束：
        - 任何处理器异常仅告警，不中断主链路
        - 无匹配订阅时不创建分发任务（避免空转）
        """
        handlers = list(self._handlers.get(event, [])) + list(self._handlers.get("*", []))
        for handler in handlers:
            try:
                result = handler(event, payload)
                if asyncio.iscoroutine(result):
                    await result
            except Exception as e:
                logger.warning(f"Hook handler failed for {event}: {e}")

        if self.webhooks.has_targets(event):
            try:
                asyncio.create_task(self.webhooks.dispatch(event=event, payload=payload))
            except Exception as e:
                logger.warning(f"Failed to schedule webhook dispatch for {event}: {e}")

    def verify_inbound(self, headers: Mapping[str, str], body: bytes) -> tuple[bool, str]:
        """代理入站验签。"""
        return self.inbound_verifier.verify(headers=headers, body=body)

    def set_inbound_secret(self, secret: str) -> None:
        """更新入站验签密钥。"""
        self.inbound_verifier.set_secret(secret)
