"""
BIBIVIDI — OS-level sandbox
Two-layer isolation for sandboxed BSS subprocesses.

Layer 1 — preexec_fn (applied in the child before execve, survives exec):
  RLIMIT_NPROC  = 0       → no new child processes / threads via fork
  RLIMIT_FSIZE  = 0       → kernel blocks all file writes (SIGXFSZ on attempt)
  RLIMIT_AS     = 128 MB  → caps virtual memory (prevents memory bombs)

Layer 2 — seccomp BPF (applied inside SANDBOX_WRAPPER, after exec):
  Blocks: socket, connect, execve, execveat, fork, vfork
  Only on Linux x86_64, via ctypes (no extra dependencies).
  Applied before the import-blocking hook, so ctypes is still available.

Why not seccomp in preexec_fn?
  preexec_fn runs BEFORE execve() replaces the child with the Python
  interpreter. Blocking execve in preexec_fn kills the child before it
  can start. Seccomp must be installed post-exec, inside the script wrapper.

On macOS: layer 1 only.
On Windows: neither layer — log a warning.

Future: replace layer 2 with WASM runtime (TODO-12 / Phase 3).
"""
from __future__ import annotations

import logging
import sys
from collections.abc import Callable

logger = logging.getLogger(__name__)


# ── Layer 1: preexec_fn (resource limits, POSIX) ──────────────────────────────

def get_preexec_fn() -> Callable[[], None] | None:
    """
    Return a preexec_fn for asyncio.create_subprocess_exec, or None on Windows.
    Applies resource limits that survive the subsequent execve().
    """
    if sys.platform == "win32":
        logger.warning(
            "BIBIVIDI sandbox: OS-level isolation unavailable on Windows. "
            "Install skills from trusted sources only."
        )
        return None

    def _preexec() -> None:
        try:
            _apply_resource_limits()
        except Exception as exc:
            # Non-fatal: Python-level import blocking still applies.
            logger.debug("sandbox resource limits failed: %s", exc)

    return _preexec


def _apply_resource_limits() -> None:
    import resource  # POSIX only — guarded above

    resource.setrlimit(resource.RLIMIT_NPROC, (0, 0))   # no fork/threads
    resource.setrlimit(resource.RLIMIT_FSIZE, (0, 0))   # no file writes
    _128_mb = 128 * 1024 * 1024
    resource.setrlimit(resource.RLIMIT_AS, (_128_mb, _128_mb))


# ── Layer 2: seccomp BPF (embedded in SANDBOX_WRAPPER, Linux x86_64) ──────────
#
# Data flow:
#
#   runner.py: _wrap(code)
#       └─ SANDBOX_WRAPPER.format(seccomp_setup=SECCOMP_SETUP_CODE, ...)
#             └─ runs inside child process after exec
#                   1. SECCOMP_SETUP_CODE installs BPF filter via ctypes
#                   2. builtins.__import__ replaced (import blocker)
#                   3. user BSS script code executes
#
# BPF program:
#   check arch == x86_64 (kill if not)
#   load syscall number
#   for each blocked nr: if match → KILL_PROCESS
#   default → ALLOW
#
# Blocked syscalls (x86_64):
#   41  socket     — no new network connections
#   42  connect    — belt-and-suspenders for socket
#   59  execve     — no spawning external programs
#  322  execveat   — same via dirfd variant
#   57  fork       — belt-and-suspenders (RLIMIT_NPROC already blocks)
#   58  vfork      — same

# Python code injected at the very top of SANDBOX_WRAPPER (before import hook).
# Uses ctypes which is available before the allowlist is installed.
SECCOMP_SETUP_CODE = r"""
try:
    import sys as _sys
    if _sys.platform == "linux" and _sys.maxsize > 2**32:  # Linux x86_64 only
        import ctypes as _ct
        import ctypes.util as _ctu
        import struct as _st

        _libc = _ct.CDLL(None, use_errno=True)

        class _SF(_ct.Structure):
            _fields_ = [("code", _ct.c_uint16), ("jt", _ct.c_uint8),
                        ("jf", _ct.c_uint8), ("k", _ct.c_uint32)]

        class _FP(_ct.Structure):
            _fields_ = [("len", _ct.c_uint16), ("filter", _ct.POINTER(_SF))]

        _ALLOW = 0x7FFF_0000
        _KILL  = 0x8000_0000
        _ARCH  = 0xC000_003E  # AUDIT_ARCH_X86_64
        _LD    = 0x20   # BPF_LD | BPF_W | BPF_ABS
        _JEQ   = 0x15   # BPF_JMP | BPF_JEQ | BPF_K
        _RET   = 0x06   # BPF_RET | BPF_K

        _insns = [
            # verify arch
            (_LD,  0, 0, 4),           # load arch field
            (_JEQ, 1, 0, _ARCH),       # if arch==x86_64 skip KILL, else fall through
            (_RET, 0, 0, _KILL),       # wrong arch → kill
            # load syscall nr
            (_LD,  0, 0, 0),
            # block: socket(41), connect(42), execve(59), execveat(322), fork(57), vfork(58)
            (_JEQ, 0, 1, 41),  (_RET, 0, 0, _KILL),
            (_JEQ, 0, 1, 42),  (_RET, 0, 0, _KILL),
            (_JEQ, 0, 1, 59),  (_RET, 0, 0, _KILL),
            (_JEQ, 0, 1, 322), (_RET, 0, 0, _KILL),
            (_JEQ, 0, 1, 57),  (_RET, 0, 0, _KILL),
            (_JEQ, 0, 1, 58),  (_RET, 0, 0, _KILL),
            # allow everything else
            (_RET, 0, 0, _ALLOW),
        ]
        _n = len(_insns)
        _FA = _SF * _n
        _fa = _FA(*(_SF(c, j, f, k) for c, j, f, k in _insns))
        _fp = _FP(len=_n, filter=_fa)
        _libc.prctl(38, 1, 0, 0, 0)          # PR_SET_NO_NEW_PRIVS
        _libc.prctl(22, 2, _ct.byref(_fp))   # PR_SET_SECCOMP FILTER
        del _ct, _ctu, _st, _libc, _SF, _FP, _FA, _fa, _fp, _insns, _n
        del _ALLOW, _KILL, _ARCH, _LD, _JEQ, _RET
    del _sys
except Exception:
    pass  # seccomp unavailable — resource limits + import blocker still apply
"""
