"""Hardcoded self-protection rules that run before user-defined policies.

Prevents agents from weakening their own guardrails by detecting tool calls
that target the policy file, uninstall the avakill package, run the approve
command, modify avakill source code, disable hook binaries, tamper with
agent hook configuration files, or shut down the evaluation daemon.
"""

from __future__ import annotations

import re
import shutil
import sys
from fnmatch import fnmatch
from pathlib import Path

from avakill.core.models import Decision, ToolCall

# Tool name patterns that indicate write/delete/modify intent
_WRITE_TOOL_PATTERNS = [
    "*write*",
    "*edit*",
    "*delete*",
    "*remove*",
    "*create*",
    "*overwrite*",
    "*modify*",
    "*patch*",
    "*move*",
    "*rename*",
]

# Policy file basenames to protect
_POLICY_FILES = ("avakill.yaml", "avakill.yml")

# Patterns for dangerous commands in argument content
_UNINSTALL_PATTERN = re.compile(
    r"(?:pipx?|pip3|python3?\s+-m\s+pip|uv|poetry)\s+"
    r"(?:uninstall|remove)\s+avakill",
    re.IGNORECASE,
)

_APPROVE_PATTERN = re.compile(
    r"avakill\s+approve\b",
    re.IGNORECASE,
)

_DAEMON_SHUTDOWN_PATTERN = re.compile(
    r"(?:"
    # avakill daemon stop [--force]
    r"avakill\s+daemon\s+stop"
    r"|"
    # pkill/killall targeting avakill (with optional flags, full paths)
    r"\b(?:pkill|killall)\b.*avakill"
    r"|"
    # kill with pgrep/pidof to find avakill PID (subshells, backticks)
    r"\bkill\b.*\b(?:pgrep|pidof)\b.*avakill"
    r"|"
    # kill referencing avakill PID file
    r"\bkill\b.*avakill[\w.]*\.pid"
    r"|"
    # systemctl stop/kill/disable avakill
    r"\bsystemctl\s+(?:stop|kill|disable)\s+avakill"
    r"|"
    # service avakill stop
    r"\bservice\s+avakill\s+stop"
    r")",
    re.IGNORECASE,
)

# ---------------------------------------------------------------------------
# Shell target-based protection (replaces verb-based _SHELL_POLICY_PATTERN)
# ---------------------------------------------------------------------------

# Tool name patterns that indicate shell/exec context
_SHELL_TOOL_PATTERNS = ["*shell*", "*bash*", "*exec*", "*terminal*", "*command*"]

# Commands known to be destructive (don't need pipe/redirect to cause damage)
_SHELL_DESTRUCTIVE_CMD = re.compile(
    r"\b(?:rm|mv|cp|ln|truncate|tee|dd|sed|del|unlink|python3?|perl|ruby|node)\b",
    re.IGNORECASE,
)

# Indicators of write intent in shell (pipes, redirects, chaining, subshells)
_SHELL_WRITE_INDICATOR = re.compile(r"[|;&]|>{1,2}|`|\$\(")

# Writes to avakill source directories
_SOURCE_WRITE_PATTERN = re.compile(
    r"(?:site-packages|src)/avakill/",
    re.IGNORECASE,
)

_SOURCE_ACTION_PATTERN = re.compile(
    r"\b(?:write|delete|remove|overwrite|modify|patch|rm|unlink|mv|sed|create|truncate)\b|>",
    re.IGNORECASE,
)

# ---------------------------------------------------------------------------
# Hook binary protection
# ---------------------------------------------------------------------------


def _discover_hook_binaries() -> list[str]:
    """Find absolute paths to avakill-hook-* console scripts."""
    paths: list[str] = []
    # Look in the same bin dir as sys.executable (virtualenv)
    bin_dir = Path(sys.executable).resolve().parent
    for candidate in bin_dir.glob("avakill-hook-*"):
        if candidate.is_file():
            paths.append(str(candidate))
    # Also check PATH
    for name in ("avakill-hook-claude-code", "avakill-hook-gemini-cli"):
        found = shutil.which(name)
        if found and found not in paths:
            paths.append(found)
    return paths


_HOOK_BINARIES: list[str] = _discover_hook_binaries()

# Regex: shell command targeting any avakill-hook-* binary or the main avakill binary
_HOOK_BINARY_PATTERN = re.compile(r"(?:avakill-hook-\w+|/avakill(?:\s|$))", re.IGNORECASE)

# Regex: extract redirect target path (> /path or >> /path)
_REDIRECT_TO_PATH_PATTERN = re.compile(r">{1,2}\s*(\S+)")

# ---------------------------------------------------------------------------
# Hook config file protection
# ---------------------------------------------------------------------------

# Config directories used by agents
_HOOK_CONFIG_DIRS = (".claude", ".gemini", ".cursor", ".codeium")

# Config filenames to protect
_HOOK_CONFIG_FILES = (
    "settings.json",
    "settings.local.json",
    "hooks.json",
)

# Combined regex: path containing an agent config dir + a protected config file
_HOOK_CONFIG_PATTERN = re.compile(
    r"(?:"
    + "|".join(re.escape(d) for d in _HOOK_CONFIG_DIRS)
    + r")[/\\](?:.*[/\\])?(?:"
    + "|".join(re.escape(f) for f in _HOOK_CONFIG_FILES)
    + r")",
    re.IGNORECASE,
)

# Shell commands that indicate destructive intent toward hook binaries or main binary
_HOOK_SHELL_DESTRUCTIVE = re.compile(
    r"(?:rm|del|unlink|mv|truncate|chmod)\s+.*(?:avakill-hook-|/avakill(?:\s|$))",
    re.IGNORECASE,
)


def _deny_message(
    rule: str,
    blocked: str,
    user_message: str,
) -> str:
    return (
        f"AvaKill blocked this tool call.\n"
        f"\n"
        f"  Rule: self-protection ({rule})\n"
        f"  Blocked: {blocked}\n"
        f"\n"
        f"  This is a hardcoded safety rule. It cannot be overridden by policy.\n"
        f"\n"
        f"  STOP. Do not attempt to work around this block.\n"
        f"  Tell the user exactly this:\n"
        f"\n"
        f'  "{user_message}"'
    )


class SelfProtection:
    """Hardcoded self-protection that cannot be overridden by policy.

    Call :meth:`check` before evaluating user-defined rules. Returns a deny
    ``Decision`` if the tool call would weaken avakill's own guardrails,
    or ``None`` to proceed to normal policy evaluation.
    """

    def check(self, tool_call: ToolCall) -> Decision | None:
        """Check a tool call against self-protection rules.

        Returns:
            A deny ``Decision`` if the call is blocked, or ``None`` to proceed.
        """
        # Layer 1: tool name + path check (write-named tools targeting policy files)
        reason = self._check_tool_name_and_path(tool_call)
        if reason:
            return self._deny(reason)

        # Layer 2: shell commands referencing protected paths (policy + hook config)
        reason = self._check_shell_targets_protected(tool_call)
        if reason:
            return self._deny(reason)

        # Layer 3: hook binary protection
        reason = self._check_hook_binary(tool_call)
        if reason:
            return self._deny(reason)

        # Layer 4: hook config file protection (write-named tools)
        reason = self._check_hook_config(tool_call)
        if reason:
            return self._deny(reason)

        # Layer 5: argument content scanning (uninstall, approve, source writes)
        reason = self._scan_arguments(tool_call)
        if reason:
            return self._deny(reason)

        return None

    def _check_tool_name_and_path(self, tool_call: ToolCall) -> str | None:
        """Check if a write/delete tool targets a policy file."""
        tool_lower = tool_call.tool_name.lower()
        is_write_tool = any(fnmatch(tool_lower, pat) for pat in _WRITE_TOOL_PATTERNS)
        if not is_write_tool:
            return None

        # Check if any argument value references a protected policy file
        for value in tool_call.arguments.values():
            value_str = str(value).lower()
            for policy_file in _POLICY_FILES:
                if policy_file in value_str and ".proposed." not in value_str:
                    return _deny_message(
                        "policy-write",
                        f"{tool_call.tool_name}({policy_file})",
                        "I'm blocked from modifying avakill.yaml by AvaKill's self-protection. "
                        "You can edit the policy file directly, or use avakill.proposed.yaml "
                        "for me to suggest changes that you review with avakill approve.",
                    )
        return None

    def _check_shell_targets_protected(self, tool_call: ToolCall) -> str | None:
        """Block shell commands with write intent targeting protected files.

        Instead of enumerating dangerous shell verbs, check whether the command
        references a protected path and shows write intent (destructive command
        or shell write indicator like pipes/redirects).
        """
        tool_lower = tool_call.tool_name.lower()
        is_shell = any(fnmatch(tool_lower, pat) for pat in _SHELL_TOOL_PATTERNS)
        if not is_shell:
            return None

        scan_text = self._build_scan_text(tool_call)
        scan_lower = scan_text.lower()

        # Check if args reference a protected policy file
        targets_policy = any(pf in scan_lower for pf in _POLICY_FILES)

        # Check if args reference a protected hook config path
        targets_hook_config = _HOOK_CONFIG_PATTERN.search(scan_text) is not None

        if not targets_policy and not targets_hook_config:
            return None

        # Check for write indicators or destructive commands
        has_write_intent = (
            _SHELL_DESTRUCTIVE_CMD.search(scan_text) is not None
            or _SHELL_WRITE_INDICATOR.search(scan_text) is not None
        )

        if not has_write_intent:
            return None

        if targets_policy:
            return _deny_message(
                "policy-shell",
                "shell command targeting avakill.yaml",
                "I'm blocked from running a shell command that would modify the "
                "AvaKill policy file. You can run this command yourself if intended.",
            )
        return _deny_message(
            "hook-config-shell",
            "shell command targeting hook config",
            "I'm blocked from running a shell command that modifies your agent's "
            "hook configuration. This protects AvaKill from being uninstalled "
            "by the agent it's monitoring. You can run this command yourself.",
        )

    def _check_hook_binary(self, tool_call: ToolCall) -> str | None:
        """Block shell commands and write tools targeting hook binaries."""
        scan_text = self._build_scan_text(tool_call)

        # Check for shell commands that destructively target hook binaries
        if _HOOK_SHELL_DESTRUCTIVE.search(scan_text):
            return _deny_message(
                "hook-binary",
                "shell command targeting hook binary",
                "I'm blocked from modifying an AvaKill hook binary. This protects "
                "the firewall from being disabled. You can manage hook binaries "
                "yourself with: avakill hook install/uninstall",
            )

        # Check for redirect to hook binary path
        for m in _REDIRECT_TO_PATH_PATTERN.finditer(scan_text):
            target = m.group(1)
            if _HOOK_BINARY_PATTERN.search(target):
                return _deny_message(
                    "hook-binary",
                    f"redirect to hook binary ({target})",
                    "I'm blocked from modifying an AvaKill hook binary. This protects "
                    "the firewall from being disabled. You can manage hook binaries "
                    "yourself with: avakill hook install/uninstall",
                )

        # Check for write tools targeting hook binary paths
        tool_lower = tool_call.tool_name.lower()
        is_write_tool = any(fnmatch(tool_lower, pat) for pat in _WRITE_TOOL_PATTERNS)
        if is_write_tool:
            for value in tool_call.arguments.values():
                value_str = str(value)
                if _HOOK_BINARY_PATTERN.search(value_str):
                    return _deny_message(
                        "hook-binary",
                        f"{tool_call.tool_name}({value_str})",
                        "I'm blocked from modifying an AvaKill hook binary. This protects "
                        "the firewall from being disabled. You can manage hook binaries "
                        "yourself with: avakill hook install/uninstall",
                    )

        return None

    def _check_hook_config(self, tool_call: ToolCall) -> str | None:
        """Block write tools targeting agent hook configuration files."""
        tool_lower = tool_call.tool_name.lower()
        is_write_tool = any(fnmatch(tool_lower, pat) for pat in _WRITE_TOOL_PATTERNS)
        if not is_write_tool:
            return None

        for value in tool_call.arguments.values():
            value_str = str(value)
            if _HOOK_CONFIG_PATTERN.search(value_str):
                return _deny_message(
                    "hook-config-write",
                    f"{tool_call.tool_name}({value_str})",
                    "I'm blocked from writing to your agent's hook config file because "
                    "it contains AvaKill's hook registration. You can edit this file "
                    "directly, or manage hooks with: avakill hook install/uninstall",
                )

        return None

    def _scan_arguments(self, tool_call: ToolCall) -> str | None:
        """Scan all argument values for dangerous command patterns."""
        scan_text = self._build_scan_text(tool_call)

        # Check for package uninstall
        if _UNINSTALL_PATTERN.search(scan_text):
            return _deny_message(
                "uninstall",
                "shell command attempting to uninstall avakill",
                "I'm blocked from uninstalling AvaKill. If you want to remove it, "
                "run the uninstall command yourself: pipx uninstall avakill",
            )

        # Check for approve command (only humans should run this)
        if _APPROVE_PATTERN.search(scan_text):
            return _deny_message(
                "approve",
                "avakill approve (only humans may activate policies)",
                "I'm blocked from running avakill approve — only humans should "
                "activate policy changes. You can run it yourself: "
                "avakill approve <policy-file>",
            )

        # Check for daemon shutdown commands
        if _DAEMON_SHUTDOWN_PATTERN.search(scan_text):
            return _deny_message(
                "daemon-shutdown",
                "command that would shut down the AvaKill daemon",
                "I'm blocked from stopping AvaKill's background service. If you "
                "want to stop it, run: avakill tracking off",
            )

        # Check for writes to avakill source
        if _SOURCE_WRITE_PATTERN.search(scan_text):
            tool_lower = tool_call.tool_name.lower()
            is_write_tool = any(fnmatch(tool_lower, pat) for pat in _WRITE_TOOL_PATTERNS)
            if is_write_tool or _SOURCE_ACTION_PATTERN.search(scan_text):
                return _deny_message(
                    "source-write",
                    f"{tool_call.tool_name} targeting avakill source/package files",
                    "I'm blocked from modifying AvaKill's installed package files. "
                    "This protects the firewall's runtime integrity. If you need to "
                    "edit these files, do so directly.",
                )

        return None

    @staticmethod
    def _build_scan_text(tool_call: ToolCall) -> str:
        """Concatenate tool name + all argument values into one string."""
        parts: list[str] = [tool_call.tool_name]
        for value in tool_call.arguments.values():
            parts.append(str(value))
        return " ".join(parts)

    @staticmethod
    def _deny(reason: str) -> Decision:
        """Create a deny Decision for self-protection."""
        return Decision(
            allowed=False,
            action="deny",
            policy_name="self-protection",
            reason=reason,
        )
