"""v3.6 Runtime Harness — 6 rule functions.

chair_authorization_id=CHAIR-AUTH-TASK-2703-V36-HARNESS-MVP-260528

Each rule: (tool_name, tool_input, context) -> Optional[tuple[decision, matched_rule, reason]]
  - Returns (decision, rule_id, reason) if matched; None otherwise.
  - decision: "DENY" | "HOLD_FOR_CHAIR"
  - ALL_RULES is the ordered list; guard.evaluate applies first match.

Whitelist-first principle: safe commands bypass expensive pattern checks.
All patterns use substring or regex to stay fast (≤200ms goal).
"""
from __future__ import annotations

import os
import re
from typing import Optional


# ---------------------------------------------------------------------------
# Helper extractors
# ---------------------------------------------------------------------------

def _get_command(tool_name: str, tool_input: dict) -> Optional[str]:
    """Extract the command string for Bash/shell tools."""
    if tool_name.lower() in ("bash", "shell", "terminal"):
        return tool_input.get("command") or tool_input.get("cmd") or ""
    return None


def _get_write_content(tool_name: str, tool_input: dict) -> Optional[str]:
    """Extract content for Write/Edit tools."""
    if tool_name.lower() in ("write", "edit", "notebookedit"):
        return (
            tool_input.get("content")
            or tool_input.get("new_string")
            or tool_input.get("source")
            or ""
        )
    return None


def _get_write_path(tool_name: str, tool_input: dict) -> str:
    """Extract file path for Write/Edit tools."""
    if tool_name.lower() in ("write", "edit", "notebookedit"):
        return (
            tool_input.get("file_path")
            or tool_input.get("path")
            or tool_input.get("notebook_path")
            or ""
        )
    return ""


def _env(key: str) -> str:
    """Safe environment variable getter."""
    return os.environ.get(key, "")


# ---------------------------------------------------------------------------
# Rule 1: session-bound direct polling
# pattern.session_bound_direct_polling
# ---------------------------------------------------------------------------
_R1_PATTERNS = [
    re.compile(r"tail\s+-f\b"),
    re.compile(r"while\b.*\.done.*\bdo\b.*sleep", re.DOTALL | re.IGNORECASE),
    re.compile(r"while\s*\[\s*!\s*-[fe]\s+.*\.done"),
    re.compile(r"python3\s+.*task-timer\.py\s+status"),
    re.compile(r"while\s+true.*sleep.*\.done", re.DOTALL | re.IGNORECASE),
    re.compile(r"inotifywait.*\.done"),
    re.compile(r"watch\s+-n\s+\d+.*\.done"),
    re.compile(r"until\b.*\.done.*\bsleep\b", re.DOTALL | re.IGNORECASE),
]

_R1_WHITELIST = re.compile(r"tail\s+-f\s+/dev/(null|stdin|stdout|stderr)")


def rule_1_session_bound_polling(
    tool_name: str, tool_input: dict, _context: dict
) -> Optional[tuple]:
    """Block session-bound direct polling patterns."""
    _ = _context  # intentionally unused
    cmd = _get_command(tool_name, tool_input)
    if cmd is None:
        return None
    if _R1_WHITELIST.search(cmd):
        return None
    for pat in _R1_PATTERNS:
        if pat.search(cmd):
            return (
                "DENY",
                "pattern.session_bound_direct_polling",
                f"세션 바인딩 직접 폴링 감지: ANU 본체가 .done 파일/타이머를 직접 폴링하는 것은 금지됨. "
                f"callback/collector 위임 방식을 사용하십시오. (matched: {pat.pattern!r})",
            )
    return None


# ---------------------------------------------------------------------------
# Rule 2: ANU direct CI/Gemini long-wait
# pattern.anu_direct_ci_gemini_wait
# ---------------------------------------------------------------------------
_R2_PATTERNS = [
    re.compile(r"gh\s+run\s+watch\b"),
    re.compile(r"gh\s+pr\s+checks\s+--watch\b"),
    re.compile(r"gh\s+pr\s+checks.*--watch\b"),
    re.compile(r"while\b.*\bgh\s+(run|pr|workflow)\b.*\bsleep\b", re.DOTALL | re.IGNORECASE),
    re.compile(r"gh\s+workflow\s+run.*--watch\b"),
    re.compile(r"watch\s+-n\s+\d+.*\bgh\s+"),
    re.compile(r"while\b.*gemini.*\bsleep\b", re.DOTALL | re.IGNORECASE),
    re.compile(r"while\b.*\bci\b.*\bsleep\b", re.DOTALL | re.IGNORECASE),
]


def rule_2_direct_ci_wait(
    tool_name: str, tool_input: dict, _context: dict
) -> Optional[tuple]:
    """Block ANU direct CI/Gemini long-waiting."""
    _ = _context  # intentionally unused
    cmd = _get_command(tool_name, tool_input)
    if cmd is None:
        return None
    for pat in _R2_PATTERNS:
        if pat.search(cmd):
            return (
                "DENY",
                "pattern.anu_direct_ci_gemini_wait",
                f"ANU 직접 CI/Gemini 장시간 대기 감지: ANU 본체가 CI/Gemini를 직접 감시하는 것은 금지됨. "
                f"handoff 또는 callback 방식을 사용하십시오. (matched: {pat.pattern!r})",
            )
    return None


# ---------------------------------------------------------------------------
# Rule 3: mtime/speculation stated as fact
# pattern.mtime_speculation_as_fact
# ---------------------------------------------------------------------------
_R3_CONTENT_PATTERNS = [
    re.compile(r"\bmtime\b"),
    re.compile(r"추정\s*(완료|됨|함|적용)", re.IGNORECASE),
    re.compile(r"likely\s+complete", re.IGNORECASE),
    re.compile(r"completed?\s+\(mtime\)", re.IGNORECASE),
    re.compile(r"mtime.*완료", re.IGNORECASE),
    re.compile(r"수정\s*시각.*완료", re.IGNORECASE),
    re.compile(r"추측.*사실", re.IGNORECASE),
    re.compile(r"파일.*수정.*완료로\s*간주", re.IGNORECASE),
]
_R3_REPORT_PATH = re.compile(r"\.done$|events/.*task.*\.json|closeout.*marker|\.md$", re.IGNORECASE)
_R3_REPORT_TOOLS = {"write", "edit", "notebookedit"}


def rule_3_mtime_speculation(
    tool_name: str, tool_input: dict, _context: dict
) -> Optional[tuple]:
    """Block speculation/mtime stated as fact in reports/closeout writes."""
    _ = _context  # intentionally unused
    if tool_name.lower() not in _R3_REPORT_TOOLS:
        return None

    content = _get_write_content(tool_name, tool_input) or ""
    path = _get_write_path(tool_name, tool_input) or ""

    content_hit = any(pat.search(content) for pat in _R3_CONTENT_PATTERNS)
    if not content_hit:
        return None

    # Must also be writing to a done/events/report path to trigger HOLD
    if not _R3_REPORT_PATH.search(path):
        return None

    return (
        "HOLD_FOR_CHAIR",
        "pattern.mtime_speculation_as_fact",
        "추측/mtime 단서를 fact로 단정하는 보고서/closeout marker 작성 감지. "
        "회장 승인 없이 mtime/추정 완료 표현을 근거로 상태를 확정할 수 없음.",
    )


# ---------------------------------------------------------------------------
# Rule 4: self status confirm without callback/collector
# pattern.self_status_confirm_no_collector
# ---------------------------------------------------------------------------
_R4_TOUCH_DONE = re.compile(
    r"\btouch\b.*(memory|events|workspace).*\.done\b"
)
_R4_ECHO_DONE = re.compile(
    r"echo\s+.*>\s*.*\.done\b"
)
_R4_WRITE_DONE_PATH = re.compile(r"/memory/events/.*\.done$|\.done$")


def rule_4_self_status_confirm(
    tool_name: str, tool_input: dict, _context: dict
) -> Optional[tuple]:
    """Block self-confirmation of done status without authorized collector."""
    _ = _context  # intentionally unused
    collector_running = _env("ANU_CALLBACK_COLLECTOR_RUNNING") == "1"

    # Check for Bash touch .done without collector env
    if tool_name.lower() in ("bash", "shell", "terminal"):
        cmd = tool_input.get("command") or ""
        if _R4_TOUCH_DONE.search(cmd) and not collector_running:
            return (
                "DENY",
                "pattern.self_status_confirm_no_collector",
                "callback/collector/authorized watcher 없이 .done 파일 직접 생성 시도 감지. "
                "ANU 본체 self-key로 완료 상태를 자체 확정하는 것은 금지됨.",
            )
        if _R4_ECHO_DONE.search(cmd) and not collector_running:
            return (
                "DENY",
                "pattern.self_status_confirm_no_collector",
                "echo redirect로 .done 파일 직접 생성 시도 감지. callback/collector 없이 완료 상태 자체 확정 금지.",
            )

    # Check for Write tool writing to .done paths
    if tool_name.lower() == "write":
        path = _get_write_path(tool_name, tool_input)
        if _R4_WRITE_DONE_PATH.search(path) and not collector_running:
            return (
                "DENY",
                "pattern.self_status_confirm_no_collector",
                f"Write tool로 .done 파일 직접 생성 시도 감지: {path!r}. "
                "authorized watcher callback 없이 완료 상태 자체 확정 금지.",
            )

    return None


# ---------------------------------------------------------------------------
# Rule 5: forbidden tool/shell commands
# pattern.forbidden_tool_or_shell
# ---------------------------------------------------------------------------
_R5_FORBIDDEN = [
    # Git write/force ops - must catch 'git push' broadly
    re.compile(r"\bgit\s+push\b"),
    re.compile(r"\bgit\s+reset\s+--hard\b"),
    # GitHub PR/merge writes
    re.compile(r"\bgh\s+pr\s+create\b"),
    re.compile(r"\bgh\s+pr\s+merge\b"),
    re.compile(r"\bgh\s+pr\s+review\b.*--approve\b"),
    # Dangerous destructive
    re.compile(r"\brm\s+-rf\s+(/home/jay/workspace|/home/jay/\.claude)\b"),
    re.compile(r"\bgit\s+branch\s+-D\s+(main|master)\b"),
]

def rule_5_forbidden_tool(
    tool_name: str, tool_input: dict, _context: dict
) -> Optional[tuple]:
    """Block explicitly forbidden tool calls and shell commands."""
    _ = _context  # intentionally unused
    cmd = _get_command(tool_name, tool_input)
    if cmd is None:
        return None

    for pat in _R5_FORBIDDEN:
        if pat.search(cmd):
            # For git push: safe push to non-main non-force is allowed
            if re.search(r"\bgit\s+push\b", pat.pattern):
                # Deny if force push OR if push to main/master
                if re.search(r"--force|-f\b", cmd) or re.search(r"\b(main|master)\b", cmd):
                    return (
                        "DENY",
                        "pattern.forbidden_tool_or_shell",
                        f"금지된 shell 명령 감지 (git push force/main): {cmd[:120]!r}. "
                        "v3.6 harness 절대 금지 명령입니다.",
                    )
                # Plain push without force/main: also deny per spec
                return (
                    "DENY",
                    "pattern.forbidden_tool_or_shell",
                    f"금지된 shell 명령 감지 (git push): {cmd[:120]!r}. "
                    "v3.6 harness 절대 금지 명령입니다. PR 생성/머지는 허가된 workflow만 사용.",
                )
            return (
                "DENY",
                "pattern.forbidden_tool_or_shell",
                f"금지된 tool/shell 명령 감지: {cmd[:120]!r}. "
                f"이 명령은 v3.6 harness에서 절대 금지됩니다. (rule: {pat.pattern!r})",
            )

    return None


# ---------------------------------------------------------------------------
# Rule 6: doctrine-only (doc/memory박제 + finish-task.sh, no code artifacts)
# pattern.doctrine_only_no_code
# ---------------------------------------------------------------------------
_R6_FINISH_TASK = re.compile(r"finish[_-]task\.sh|finish_task\.sh")
_R6_MD_PATH = re.compile(r"\.md$", re.IGNORECASE)


def rule_6_doctrine_only(
    tool_name: str, tool_input: dict, context: dict
) -> Optional[tuple]:
    """Block finish-task.sh execution when only .md files written (no code artifacts).

    Detection logic:
    - Bash: finish-task.sh call AND context.new_code_files == 0 AND context.new_md_files >= 1
    - Bash: finish-task.sh call AND context.doctrine_only_flag set
    - Write: .md path AND context.new_code_files == 0 AND context.finish_task_pending
    """
    cmd = _get_command(tool_name, tool_input)

    if cmd and _R6_FINISH_TASK.search(cmd):
        new_code = context.get("new_code_files", None)
        new_md = context.get("new_md_files", None)

        if new_code is not None and new_md is not None:
            if new_code == 0 and new_md >= 1:
                return (
                    "HOLD_FOR_CHAIR",
                    "pattern.doctrine_only_no_code",
                    f"문서/메모리 박제만으로 재발 방지 완료 판정 시도 감지. "
                    f"신규 .md {new_md}건, py/sh/json/test 0건. "
                    "실질 코드 artifact 없이 finish-task.sh 실행은 회장 승인 필요.",
                )
        elif context.get("doctrine_only_flag"):
            return (
                "HOLD_FOR_CHAIR",
                "pattern.doctrine_only_no_code",
                "finish-task.sh 실행 시도 감지. doctrine_only_flag 설정됨. 회장 승인 필요.",
            )

    # Write: .md path AND context indicates no code
    if tool_name.lower() == "write":
        path = _get_write_path(tool_name, tool_input)
        if _R6_MD_PATH.search(path):
            new_code = context.get("new_code_files", 1)  # default: assume code exists
            if new_code == 0 and context.get("finish_task_pending"):
                return (
                    "HOLD_FOR_CHAIR",
                    "pattern.doctrine_only_no_code",
                    f"코드 artifact 0건 상태에서 .md 박제 + finish-task 패턴 감지: {path!r}. "
                    "회장 승인 필요.",
                )

    return None


# ---------------------------------------------------------------------------
# Rule registry — ordered; first match wins
# ---------------------------------------------------------------------------
ALL_RULES = [
    rule_1_session_bound_polling,
    rule_2_direct_ci_wait,
    rule_3_mtime_speculation,
    rule_4_self_status_confirm,
    rule_5_forbidden_tool,
    rule_6_doctrine_only,
]
