# -*- coding: utf-8 -*-
"""
Hook Runner 模块
在 Agent 生命周期的特定节点（PreToolUse/PostToolUse/PreLLMRequest 等）
自动触发用户配置的可执行脚本。

目录结构：
  hooks_dir/
    PreToolUse/
      01_validate.bat   # Windows
      01_validate.sh    # Unix
    PostToolUse/
      log.bat

环境变量传递：context 字典中的每个 key 会以 ABYSS_<KEY_UPPER> 形式注入子进程。
Pre* 事件中钩子退出码非 0 视为阻断信号（block=True）。
"""
import os
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional


# 可执行的脚本扩展名（按平台）
_WIN_EXTS = {".bat", ".cmd", ".ps1"}
_UNIX_EXTS = {".sh", ".bash", ".py", ""}


@dataclass
class HookResult:
    """单个钩子执行结果。"""
    name: str
    success: bool
    block: bool
    stdout: str
    stderr: str
    returncode: int


class HookRunner:
    """从目录加载并执行生命周期事件钩子。"""

    def __init__(self, hooks_dir: Optional[Path] = None):
        self._hooks_dir = hooks_dir
        # 事件名 -> 钩子脚本路径列表（按文件名排序）
        self._hooks: Dict[str, List[Path]] = {}
        if hooks_dir is not None:
            self._load_from_dir(hooks_dir)

    def _load_from_dir(self, hooks_dir: Path) -> None:
        """扫描 hooks_dir/<event>/ 下的可执行脚本"""
        if not hooks_dir.exists() or not hooks_dir.is_dir():
            return
        for event_dir in hooks_dir.iterdir():
            if not event_dir.is_dir():
                continue
            event_name = event_dir.name
            scripts = []
            for f in event_dir.iterdir():
                if not f.is_file():
                    continue
                if not self._is_executable(f):
                    continue
                scripts.append(f)
            scripts.sort(key=lambda p: p.name)
            self._hooks[event_name] = scripts

    @staticmethod
    def _is_executable(path: Path) -> bool:
        """判断文件是否为可执行脚本（按平台扩展名过滤）"""
        ext = path.suffix.lower()
        if sys.platform.startswith("win"):
            return ext in _WIN_EXTS
        return ext in _UNIX_EXTS or os.access(path, os.X_OK)

    def list_hooks(self, event: str) -> List[Path]:
        """返回指定事件的所有钩子脚本路径"""
        return list(self._hooks.get(event, []))

    def run(self, event: str, context: Dict[str, str]) -> List[HookResult]:
        """执行指定事件的所有钩子，按文件名排序依次运行。
        context 中的键值会以 ABYSS_<KEY_UPPER> 注入子进程环境变量。
        返回每个钩子的执行结果。
        """
        results = []
        for script in self._hooks.get(event, []):
            result = self._run_script(script, context)
            results.append(result)
            # Pre* 事件中阻断后不再执行后续钩子
            if result.block and event.startswith("Pre"):
                break
        return results

    @staticmethod
    def _run_script(script: Path, context: Dict[str, str]) -> HookResult:
        """执行单个钩子脚本"""
        env = os.environ.copy()
        for key, value in context.items():
            env_key = f"ABYSS_{key.upper()}"
            env[env_key] = str(value)

        try:
            if sys.platform.startswith("win"):
                # Windows 用 cmd 执行 .bat/.cmd
                proc = subprocess.run(
                    ["cmd", "/c", str(script)],
                    capture_output=True, text=True,
                    env=env, timeout=30, encoding="utf-8", errors="replace"
                )
            else:
                proc = subprocess.run(
                    [str(script)],
                    capture_output=True, text=True,
                    env=env, timeout=30, encoding="utf-8", errors="replace"
                )
            returncode = proc.returncode
            stdout = proc.stdout or ""
            stderr = proc.stderr or ""
        except subprocess.TimeoutExpired:
            return HookResult(
                name=script.name, success=False, block=False,
                stdout="", stderr="timeout after 30s", returncode=-1
            )
        except Exception as e:
            return HookResult(
                name=script.name, success=False, block=False,
                stdout="", stderr=str(e), returncode=-1
            )

        return HookResult(
            name=script.name,
            success=(returncode == 0),
            block=(returncode != 0),
            stdout=stdout,
            stderr=stderr,
            returncode=returncode,
        )


def create_default_runner() -> HookRunner:
    """创建指向 ~/.abyss/hooks/ 的默认 HookRunner"""
    return HookRunner(hooks_dir=Path.home() / ".abyss" / "hooks")
