"""Exec approval system — allowlist/denylist + dangerous command detection.

Note: This module handles user-configurable approval policy (allow/deny lists,
dangerous command patterns requiring confirmation). The hardcoded command
allowlist/blocklist enforcement lives in salmalm.tools.tools_common._is_safe_command
and salmalm.constants.EXEC_ALLOWLIST. Both layers apply — constants for the
hard security boundary, this module for the softer approval workflow.
"""

import re
import sys
import threading
import time
import os
from salmalm.security.crypto import log
from salmalm.constants import DATA_DIR

_APPROVALS_FILE = DATA_DIR / "exec_approvals.json"
_lock = threading.Lock()

# Dangerous command patterns that require user approval
DANGEROUS_PATTERNS = [
    r"\brm\s+(-[a-zA-Z]*[rR][a-zA-Z]*\s+|.*--recursive)",  # rm -r, rm -rf
    r"\brm\s+-[a-zA-Z]*f",  # rm -f
    r"\bsudo\b",
    r"\bchmod\s+[0-7]*7[0-7]{0,2}\b",  # chmod with world-writable (e.g. 777, 776, 757)
    r"\bchown\b",
    r"\bmkfs\b",
    r"\bdd\s+",
    r":\(\)\s*\{.*\|.*&\s*\}\s*;",  # fork bomb (relaxed match)
    r"\bshutdown\b",
    r"\breboot\b",
    r"\bsystemctl\s+(stop|disable|mask)",
    r"\bkill\s+-9\s",
    r"\bpkill\b",
    r"\bkillall\b",
    r"\bnc\s.*-[lLeEp]",  # netcat listen/exec
    r"\bncat\s.*-[lLeEp]",  # ncat variant
    r"\bcurl\s.*\|\s*(ba)?sh",  # curl | sh
    r"\bwget\s.*\|\s*(ba)?sh",
    r"\bcurl\s.*\|\s*python",  # curl | python
    r"\bwget\s.*\|\s*python",
    r">\s*/dev/[sh]d[a-z]",  # write to raw block device
    r"\bmv\s+.*\s+/dev/null\b",  # mv to /dev/null (data destruction)
]

# Env vars that cannot be overridden (binary hijacking prevention)
BLOCKED_ENV_OVERRIDES = {
    "PATH",
    "LD_PRELOAD",
    "LD_LIBRARY_PATH",
    "LD_AUDIT",
    "DYLD_INSERT_LIBRARIES",
    "DYLD_LIBRARY_PATH",
    "DYLD_FRAMEWORK_PATH",
}


from salmalm.config_manager import ConfigManager

_EXEC_APPROVALS_DEFAULTS = {"allowlist": [], "denylist": [], "auto_approve": False}


def _load_config() -> dict:
    """Load exec approval config."""
    return ConfigManager.load("exec_approvals", defaults=_EXEC_APPROVALS_DEFAULTS)


def _save_config(config: dict):
    """Save exec approval config."""
    try:
        ConfigManager.save("exec_approvals", config)
    except (OSError, ValueError, TypeError) as e:
        log.error(f"Failed to save exec approvals: {e}")


def check_approval(command: str) -> tuple:
    """Check if a command needs approval.

    Returns (approved: bool, reason: str, needs_user_confirm: bool).
    """
    config = _load_config()
    cmd_stripped = command.strip()

    # Denylist check (exact or pattern match)
    for pattern in config.get("denylist", []):
        if re.search(pattern, cmd_stripped):
            return False, f"Denied by denylist pattern: {pattern}", False

    # Allowlist check (exact prefix match)
    for allowed in config.get("allowlist", []):
        if cmd_stripped.startswith(allowed):
            return True, "Allowed by allowlist", False

    # Dangerous pattern detection
    for pattern in DANGEROUS_PATTERNS:
        if re.search(pattern, cmd_stripped, re.IGNORECASE):
            if config.get("auto_approve"):
                return True, f"Auto-approved (dangerous: {pattern})", False
            return False, f"Dangerous command detected: {pattern}", True

    return True, "OK", False


def check_env_override(env: dict) -> tuple:
    """Check if env dict tries to override blocked variables.

    Returns (safe: bool, blocked_vars: list).
    """
    if not env:
        return True, []
    blocked = []
    for key in env:
        if key in BLOCKED_ENV_OVERRIDES:
            blocked.append(key)
        # Also catch LD_* and DYLD_* patterns
        if key.startswith("LD_") or key.startswith("DYLD_"):
            if key not in BLOCKED_ENV_OVERRIDES:
                blocked.append(key)
    return len(blocked) == 0, blocked


# ============================================================
# Background session management
# ============================================================


class BackgroundSession:
    """Manages a background exec process."""

    _sessions: dict = {}  # session_id -> BackgroundSession
    _counter = 0
    _lock = threading.Lock()

    def __init__(self, command: str, timeout: int = 1800, notify_on_exit: bool = False, env: dict = None) -> None:
        """Init  ."""
        with BackgroundSession._lock:
            BackgroundSession._counter += 1
            self.session_id = f"bg-{BackgroundSession._counter}"

        self.command = command
        self.timeout = timeout
        self.notify_on_exit = notify_on_exit
        self.started = time.time()
        self.process = None
        self.stdout_data = ""
        self.stderr_data = ""
        self.exit_code = None
        self.status = "starting"  # starting, running, completed, timeout, error
        self._thread = None
        self._env = env

    def start(self):
        """Start the background process."""
        import subprocess
        import shlex

        # Env var security check
        if self._env:
            safe, blocked = check_env_override(self._env)
            if not safe:
                self.status = "error"
                self.stderr_data = f"Blocked env overrides: {', '.join(blocked)}"
                return self.session_id

        needs_shell = any(c in self.command for c in ["|", ">", "<", "&&", "||", ";"])
        if needs_shell and not os.environ.get("SALMALM_ALLOW_SHELL"):
            self.status = "error"
            self.stderr_data = "Shell operators require SALMALM_ALLOW_SHELL=1"
            return self.session_id
        # Secret isolation: strip API keys/tokens from background env
        from salmalm.tools.tools_exec import _sanitized_env

        run_env = _sanitized_env(self._env)

        from salmalm.constants import WORKSPACE_DIR as _ws

        def _run():
            """Run."""
            try:
                if needs_shell:
                    popen_args = {"args": self.command, "shell": True}
                else:
                    try:
                        popen_args = {"args": shlex.split(self.command), "shell": False}
                    except ValueError:
                        self.status = "error"
                        self.stderr_data = "Failed to parse command"
                        return

                self.status = "running"
                def _bg_limits():
                    """Resource limits for background sessions (same as foreground)."""
                    try:
                        import resource
                        resource.setrlimit(resource.RLIMIT_AS, (1024**3, 1024**3))  # 1 GB RAM cap
                        resource.setrlimit(resource.RLIMIT_FSIZE, (50 * 1024 * 1024, 50 * 1024 * 1024))
                    except Exception:
                        pass

                _extra = {"preexec_fn": _bg_limits} if sys.platform != "win32" else {}
                proc = subprocess.Popen(
                    **popen_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=run_env, cwd=str(_ws), **_extra
                )
                self.process = proc  # Now kill() can reach it
                try:
                    stdout, stderr = proc.communicate(timeout=self.timeout)
                    self.stdout_data = stdout[-100_000:] if stdout else ""
                    self.stderr_data = stderr[-10_000:] if stderr else ""
                    self.exit_code = proc.returncode
                    self.status = "completed"
                except subprocess.TimeoutExpired:
                    proc.kill()
                    proc.wait(timeout=5)
                    self.status = "timeout"
                    self.stderr_data = f"Process timed out after {self.timeout}s"
            except Exception as e:
                self.status = "error"
                self.stderr_data = str(e)[:1000]

            # Notify on exit if requested
            if self.notify_on_exit:
                try:
                    from salmalm import core as _c

                    if _c._tg_bot and _c._tg_bot.token:
                        msg = (
                            f"🖥 Background session `{self.session_id}` finished\n"
                            f"Command: `{self.command[:80]}`\n"
                            f"Status: {self.status} (exit: {self.exit_code})"
                        )
                        _c._tg_bot._api(
                            "sendMessage", {"chat_id": _c._tg_bot.owner_id, "text": msg, "parse_mode": "Markdown"}
                        )
                except Exception as e:  # noqa: broad-except
                    log.debug(f"Suppressed: {e}")

        self._thread = threading.Thread(target=_run, daemon=True, name=f"bg-exec-{self.session_id}")
        self._thread.start()
        BackgroundSession._sessions[self.session_id] = self
        return self.session_id

    def poll(self) -> dict:
        """Poll current status."""
        elapsed = time.time() - self.started
        return {
            "session_id": self.session_id,
            "command": self.command[:100],
            "status": self.status,
            "exit_code": self.exit_code,
            "elapsed_s": round(elapsed, 1),
            "stdout_tail": self.stdout_data[-2000:] if self.stdout_data else "",
            "stderr_tail": self.stderr_data[-1000:] if self.stderr_data else "",
        }

    def kill(self) -> str:
        """Kill the background process and its children."""
        if self.process and self.process.poll() is None:
            try:
                import signal

                # Kill process group if possible (catches child processes)
                pgid = os.getpgid(self.process.pid)
                os.killpg(pgid, signal.SIGTERM)
            except (ProcessLookupError, PermissionError, OSError):
                pass
            try:
                self.process.kill()
                self.process.wait(timeout=5)
            except Exception as e:  # noqa: broad-except
                log.debug(f"Suppressed: {e}")
        self.status = "killed"
        self.exit_code = -9
        return f"Killed background session {self.session_id}"

    @classmethod
    def list_sessions(cls) -> list:
        """List all background sessions."""
        return [s.poll() for s in cls._sessions.values()]

    @classmethod
    def get_session(cls, session_id: str):
        """Get a background session by ID."""
        return cls._sessions.get(session_id)

    @classmethod
    def kill_session(cls, session_id: str) -> str:
        """Kill a specific background session."""
        s = cls._sessions.get(session_id)
        if not s:
            return f"❌ Session {session_id} not found"
        return s.kill()
