#!/usr/bin/env python3
"""
utils/event_hooks.py — 이벤트 훅 시스템

이벤트를 emit하면 등록된 핸들러를 순서대로 호출한다.
- 동기/비동기 핸들러 자동 감지
- 와일드카드 매칭 (dispatch:* → dispatch:start, dispatch:end …)
- 핸들러 예외 격리 (catch + log, 메인 파이프라인 블록 안 함)
- 디렉토리 기반 동적 로드 (HOOK.yaml + handler.py)

Usage:
    from utils.event_hooks import Event, get_registry, emit

    reg = get_registry()
    reg.register(Event.DISPATCH_START, my_handler)
    await emit(Event.DISPATCH_START, {"key": "value"})
"""

from __future__ import annotations

import asyncio
import fnmatch
from typing import Any, Callable

from utils.logger import get_logger

logger = get_logger(__name__)

# ---------------------------------------------------------------------------
# Event 상수
# ---------------------------------------------------------------------------


class Event:
    """이벤트 타입 상수 모음."""

    DISPATCH_START: str = "dispatch:start"
    DISPATCH_END: str = "dispatch:end"
    DISPATCH_ERROR: str = "dispatch:error"
    SESSION_START: str = "session:start"
    SESSION_END: str = "session:end"
    AGENT_STEP: str = "agent:step"
    SECURITY_BLOCK: str = "security:block"
    SECURITY_WARN: str = "security:warn"
    SKILL_INSTALL: str = "skill:install"


# ---------------------------------------------------------------------------
# HookRegistry
# ---------------------------------------------------------------------------

_HandlerInfo = dict[str, Any]  # {"fn": callable, "name": str}


class HookRegistry:
    """이벤트 핸들러 레지스트리."""

    def __init__(self) -> None:
        # event_type -> list of handler info dicts
        self._handlers: dict[str, list[_HandlerInfo]] = {}

    # ------------------------------------------------------------------
    # 등록
    # ------------------------------------------------------------------

    def register(self, event_type: str, handler: Callable[..., Any], name: str = "") -> None:
        """핸들러를 이벤트 타입에 등록한다 (와일드카드 포함 가능, e.g. "dispatch:*")."""
        if event_type not in self._handlers:
            self._handlers[event_type] = []
        info: _HandlerInfo = {"fn": handler, "name": name or getattr(handler, "__name__", repr(handler))}
        self._handlers[event_type].append(info)
        logger.debug("Registered handler '%s' for event '%s'", info["name"], event_type)

    # ------------------------------------------------------------------
    # 매칭 로직
    # ------------------------------------------------------------------

    def _matching_handlers(self, event_type: str) -> list[_HandlerInfo]:
        """event_type에 매칭되는 모든 핸들러를 반환한다 (정확 매칭 + 와일드카드)."""
        result: list[_HandlerInfo] = []
        for pattern, handlers in self._handlers.items():
            if pattern == event_type or fnmatch.fnmatch(event_type, pattern):
                result.extend(handlers)
        return result

    # ------------------------------------------------------------------
    # emit (비동기)
    # ------------------------------------------------------------------

    async def emit(self, event_type: str, context: Any = None) -> None:
        """비동기 이벤트를 발생시킨다. 핸들러 예외는 격리되어 log만 남긴다."""
        for info in self._matching_handlers(event_type):
            fn = info["fn"]
            handler_name = info["name"]
            try:
                if asyncio.iscoroutinefunction(fn):
                    await fn(context)
                else:
                    fn(context)
            except Exception as exc:  # noqa: BLE001
                logger.warning(
                    "Hook handler '%s' for event '%s' raised an exception: %s",
                    handler_name,
                    event_type,
                    exc,
                    exc_info=True,
                )

    # ------------------------------------------------------------------
    # emit_sync (동기)
    # ------------------------------------------------------------------

    def emit_sync(self, event_type: str, context: Any = None) -> None:
        """동기 컨텍스트에서 이벤트를 발생시킨다.

        루프 실행 중이면 ensure_future, 없으면 asyncio.run으로 실행.
        """
        try:
            loop = asyncio.get_running_loop()
        except RuntimeError:
            loop = None

        if loop is not None and loop.is_running():
            # 이미 이벤트 루프가 돌고 있는 경우 — 동기 핸들러만 직접 호출
            for info in self._matching_handlers(event_type):
                fn = info["fn"]
                handler_name = info["name"]
                try:
                    if asyncio.iscoroutinefunction(fn):
                        # 실행 중인 루프에서 코루틴을 직접 스케줄
                        asyncio.ensure_future(fn(context), loop=loop)
                    else:
                        fn(context)
                except Exception as exc:  # noqa: BLE001
                    logger.warning(
                        "Hook handler '%s' (sync emit) for event '%s' raised: %s",
                        handler_name,
                        event_type,
                        exc,
                        exc_info=True,
                    )
        else:
            asyncio.run(self.emit(event_type, context))

    # ------------------------------------------------------------------
    # discover_and_load
    # ------------------------------------------------------------------

    def discover_and_load(self, hooks_dir: str) -> None:
        """디렉토리를 순회하여 HOOK.yaml + handler.py 기반 훅을 동적으로 로드한다.

        실제 로직은 utils.event_hooks_loader.discover_hooks에 위임한다.

        Args:
            hooks_dir: 훅 서브디렉토리들이 위치한 상위 디렉토리 경로
        """
        from utils.event_hooks_loader import discover_hooks

        discover_hooks(self, hooks_dir)


# ---------------------------------------------------------------------------
# 싱글턴
# ---------------------------------------------------------------------------

_registry: HookRegistry | None = None


def get_registry() -> HookRegistry:
    """프로세스 전역 싱글턴 HookRegistry를 반환한다."""
    global _registry
    if _registry is None:
        _registry = HookRegistry()
    return _registry


async def emit(event_type: str, context: Any = None) -> None:
    """전역 싱글턴 레지스트리를 통해 이벤트를 발생시키는 편의 함수."""
    await get_registry().emit(event_type, context)
