# mypy: disable-error-code="list-item"
"""exe.dev adapter — wraps an exe.dev VM via SSH gateway.

exe.dev VMs are accessed via SSH through the exe.dev gateway.
Commands are routed as: ``ssh exe.dev ssh <vmName> <command>``.
Use ``exe_defaults()`` for full enforcement (ptrace + seccomp +
landlock + FUSE + cgroups).
"""
from __future__ import annotations

import asyncio
import base64

from ..core.shell import shell_escape, env_prefix, fire_and_forget
from ..core.types import (
    ApprovalsConfig,
    AuditConfig,
    CgroupsConfig,
    DevelopmentConfig,
    DlpConfig,
    DlpCustomPattern,
    ExecOpts,
    ExecResult,
    FileMonitorConfig,
    FuseConfig,
    GrpcConfig,
    HealthConfig,
    LoggingConfig,
    MetricsConfig,
    NetworkInterceptConfig,
    ProxyConfig,
    PtraceConfig,
    PtracePerformanceConfig,
    PtraceTraceConfig,
    SandboxLimitsConfig,
    SeccompDetailsConfig,
    SecureConfig,
    ServerConfig,
    ServerTimeoutsConfig,
    SessionsConfig,
    UnixSocketsConfig,
)
from ..policies.schema import (
    AuditSettings,
    EnvPolicy,
    PolicyDefinition,
    ResourceLimits,
)

_SSH_OPTS = [
    "-o", "StrictHostKeyChecking=no",
    "-o", "UserKnownHostsFile=/dev/null",
    "-o", "LogLevel=ERROR",
]

_NOISE_PREFIXES = ("landlock:", "ptrace:", "agentsh:", "seccomp:", "[agentsh]")


def _filter_noise(text: str) -> str:
    """Strip agentsh debug noise that leaks through SSH stderr."""
    return "\n".join(
        line for line in text.split("\n")
        if not line.strip().lower().startswith(tuple(p.lower() for p in _NOISE_PREFIXES))
    )


def exe(vm_name: str) -> _ExeAdapter:
    """Wrap an exe.dev VM into a SandboxAdapter.

    The VM must already exist (created via ``ssh exe.dev new``).
    exe.dev VMs are persistent — ``stop()`` is a no-op. Destroy
    externally with ``ssh exe.dev rm <vmName>``.

    Usage::

        adapter = exe("my-vm")
        sandbox = await secure_sandbox(adapter, exe_defaults())
    """
    return _ExeAdapter(vm_name)


def exe_defaults() -> SecureConfig:
    """Return exe.dev-optimized defaults for SecureConfig.

    Key characteristics:
    - Full enforcement: ptrace + seccomp + landlock + FUSE + cgroups
    - allow_degraded: False (exe.dev has all kernel capabilities)
    - FUSE deferred with soft-delete quarantine
    - DLP with custom patterns for common AI/cloud API keys
    - High-security, deny-by-default policy
    - Conservative resource limits (2 GB RAM, 50% CPU, 100 PIDs)
    """
    server_config = ServerConfig(
        grpc=GrpcConfig(addr="127.0.0.1:9090"),
        server_timeouts=ServerTimeoutsConfig(
            read_timeout="30s", write_timeout="60s", max_request_size="10MB",
        ),
        logging=LoggingConfig(level="info", format="text", output="stderr"),
        sessions=SessionsConfig(
            max_sessions=100,
            default_timeout="1h",
            idle_timeout="15m",
            cleanup_interval="5m",
        ),
        audit=AuditConfig(enabled=True, sqlite_path="/var/lib/agentsh/events.db"),
        sandbox_limits=SandboxLimitsConfig(
            max_memory_mb=4096, max_cpu_percent=100, max_processes=256,
        ),
        allow_degraded=False,
        fuse=FuseConfig(
            deferred=True,
            deferred_marker_file="/tmp/.agentsh-fuse-enabled",
            deferred_enable_command=["/bin/chmod", "666", "/dev/fuse"],
        ),
        network_intercept=NetworkInterceptConfig(
            intercept_mode="all", proxy_listen_addr="127.0.0.1:0",
        ),
        seccomp_details=SeccompDetailsConfig(
            execve=False,
            file_monitor=FileMonitorConfig(enabled=True, enforce_without_fuse=True),
        ),
        cgroups=CgroupsConfig(enabled=True),
        unix_sockets=UnixSocketsConfig(enabled=True),
        ptrace=PtraceConfig(
            enabled=True,
            trace=PtraceTraceConfig(
                execve=True, file=False, network=False, signal=False,
            ),
            performance=PtracePerformanceConfig(seccomp_prefilter=True),
        ),
        env_inject={"BASH_ENV": "/usr/lib/agentsh/bash_startup.sh"},
        proxy=ProxyConfig(
            mode="embedded",
            port=0,
            providers={
                "anthropic": "https://api.anthropic.com",
                "openai": "https://api.openai.com",
            },
        ),
        dlp=DlpConfig(
            mode="redact",
            patterns={"email": True, "phone": True, "credit_card": True, "ssn": True, "api_keys": True},
            custom_patterns=[
                DlpCustomPattern(name="openai_key", display="OPENAI_KEY", regex=r"sk-[a-zA-Z0-9]{48,}"),
                DlpCustomPattern(name="anthropic_key", display="ANTHROPIC_KEY", regex=r"sk-ant-[a-zA-Z0-9-]{95,}"),
                DlpCustomPattern(name="aws_access_key", display="AWS_KEY", regex=r"AKIA[0-9A-Z]{16}"),
                DlpCustomPattern(name="github_pat", display="GITHUB_TOKEN", regex=r"ghp_[a-zA-Z0-9]{36}"),
                DlpCustomPattern(name="github_oauth", display="GITHUB_OAUTH", regex=r"gho_[a-zA-Z0-9]{36}"),
                DlpCustomPattern(name="jwt_token", display="JWT", regex=r"eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*"),
                DlpCustomPattern(name="private_key", display="PRIVATE_KEY", regex=r"-----BEGIN [A-Z]+ PRIVATE KEY-----"),
                DlpCustomPattern(name="slack_token", display="SLACK_TOKEN", regex=r"xox[baprs]-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24}"),
            ],
        ),
        approvals=ApprovalsConfig(enabled=False),
        metrics=MetricsConfig(enabled=True, path="/metrics"),
        health=HealthConfig(path="/health", readiness_path="/ready"),
        development=DevelopmentConfig(disable_auth=True, verbose_errors=True),
    )

    policy = PolicyDefinition(
        file=[
            {"deny": [
                "/usr/bin/sudo", "/usr/bin/su", "/usr/bin/pkexec", "/usr/bin/doas",
                "/bin/su", "/usr/sbin/chroot", "/usr/bin/nsenter", "/usr/bin/unshare",
            ]},
            {"deny": [
                "/usr/bin/shelley", "/usr/local/bin/shelley",
                "/etc/systemd/**", "/run/systemd/**",
            ]},
            {"deny": ["~/.ssh/**", "/root/.ssh/**"]},
            {"deny": ["~/.aws/**", "/root/.aws/**"]},
            {"deny": [
                "~/.gcloud/**", "~/.azure/**", "~/.config/gcloud/**", "~/.kube/**",
                "/root/.gcloud/**", "/root/.azure/**", "/root/.config/gcloud/**", "/root/.kube/**",
            ]},
            {"deny": ["**/.env", "**/.env.*"]},
            {"deny": ["~/.git-credentials", "/root/.git-credentials", "**/.netrc"]},
            {"allow": ["/root", "/root/**", "/workspace", "/workspace/**"],
             "ops": ["read", "write", "create", "open", "stat", "list", "readlink", "mkdir", "chmod", "rename"]},
            {"soft_delete": ["/root", "/root/**", "/workspace", "/workspace/**"]},
            {"allow": ["/tmp/**", "/var/tmp/**"]},
            {"allow": ["/usr/**", "/lib/**", "/lib64/**", "/bin/**", "/sbin/**"],
             "ops": ["read", "open", "stat", "list", "readlink"]},
            {"allow": [
                "/dev/null", "/dev/zero", "/dev/tty", "/dev/pts/**",
                "/dev/urandom", "/dev/random", "/dev/shm/**",
                "/dev/stdin", "/dev/stdout", "/dev/stderr", "/dev/fd/**",
            ], "ops": ["read", "write", "create", "open", "stat"]},
            {"deny": ["/etc/shadow", "/etc/gshadow", "/etc/sudoers", "/etc/sudoers.d/**"]},
            {"allow": [
                "/etc/hosts", "/etc/resolv.conf",
                "/etc/ssl/**", "/etc/ca-certificates/**",
                "/etc/localtime", "/etc/timezone",
                "/etc/ld.so.cache", "/etc/ld.so.preload", "/etc/ld.so.nohwcap",
                "/etc/ld.so.conf", "/etc/ld.so.conf.d/**",
                "/etc/nsswitch.conf", "/etc/passwd", "/etc/group",
                "/etc/fuse.conf", "/etc/gai.conf",
                "/etc/mime.types", "/etc/protocols", "/etc/services",
            ], "ops": ["read", "open", "stat", "readlink"]},
            {"allow": ["/proc/self/**", "/proc/thread-self/**"],
             "ops": ["read", "open", "stat", "list", "readlink"]},
            {"allow": [
                "~/.npm/**", "~/.cache/**", "~/.cargo/**",
                "/root/.npm/**", "/root/.cache/**", "/root/.cargo/**",
            ], "ops": ["read", "open", "stat", "list"]},
            {"allow": ["/var/lib/agentsh/**", "/var/log/agentsh/**"],
             "ops": ["read", "write", "open", "stat", "list", "readlink"]},
            {"deny": ["**/credentials*", "**/*.pem", "**/*.key"]},
            {"deny": ["/proc/*/environ"]},
            {"deny": [
                "~/.bashrc", "~/.zshrc", "~/.profile", "~/.bash_profile",
                "/root/.bashrc", "/root/.zshrc", "/root/.profile", "/root/.bash_profile",
            ], "ops": ["write", "create"]},
            {"deny": ["/etc/hostname", "/etc/hosts"], "ops": ["write", "create"]},
            {"deny": ["/var/run/docker.sock", "/run/docker.sock"]},
            {"deny": ["~/.gitconfig", "~/.curlrc", "~/.wgetrc"]},
            {"deny": ["**/.cursorrules", "**/CLAUDE.md", "**/copilot-instructions.md"], "ops": ["write", "create", "delete"]},
            {"deny": ["/proc/**", "/sys/**"]},
            {"deny": "**"},
        ],
        network=[
            {"allow_cidrs": ["127.0.0.1/32", "::1/128"]},
            {"allow": ["registry.npmjs.org"], "ports": [443]},
            {"allow": ["pypi.org", "files.pythonhosted.org"], "ports": [443]},
            {"allow": ["crates.io", "static.crates.io"], "ports": [443]},
            {"allow": ["proxy.golang.org", "sum.golang.org"], "ports": [443]},
            {"deny_cidrs": ["169.254.169.254/32", "100.100.100.200/32"]},
            {"deny_cidrs": ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16"]},
            {"deny": "*"},
        ],
        commands=[
            {"allow": ["curl", "wget"]},
            {"allow": [
                "bash", "sh", "/bin/bash", "/bin/sh", "/usr/bin/bash", "/usr/bin/sh",
                "ls", "cat", "head", "tail", "grep", "find", "wc", "sort", "uniq",
                "diff", "pwd", "echo", "date", "which",
                "env", "printenv", "true", "false", "test", "[",
                "expr", "seq", "sh.real", "bash.real",
            ]},
            {"allow": ["git", "node", "npm", "python", "python3", "pip", "pip3", "cargo", "go", "make"]},
            {"deny": ["nc", "netcat", "ncat", "socat", "telnet", "ssh", "scp", "rsync"]},
            {"deny": [
                "shutdown", "reboot", "systemctl", "service",
                "mount", "umount", "dd", "fdisk", "mkfs",
                "kill", "killall", "pkill",
            ]},
            {"deny": ["sudo", "su", "doas", "chroot", "nsenter", "unshare"]},
            {"deny": ["apt", "apt-get", "yum", "dnf", "brew"]},
            {"deny": ["shelley", "iptables", "ip6tables", "nft", "tc", "ip"]},
            {"allow": "*"},
        ],
        env_policy=EnvPolicy(
            allow=[
                "PATH", "HOME", "USER", "SHELL", "LANG", "LANG_*", "LC_*",
                "TERM", "TERM_*", "TZ", "PWD", "OLDPWD", "SHLVL", "_",
                "NODE_ENV", "NODE_PATH", "NPM_*",
                "PYTHONPATH", "VIRTUAL_ENV", "PIP_*",
                "GIT_*",
                "AGENTSH_*",
                "HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "NO_PROXY", "no_proxy",
            ],
            deny=[
                "AWS_*", "AZURE_*", "GCP_*", "GOOGLE_*",
                "OPENAI_API_KEY", "ANTHROPIC_API_KEY",
                "DATABASE_URL", "DB_*",
                "SECRET_*", "PASSWORD*", "PRIVATE_*", "API_KEY*", "TOKEN*",
            ],
            block_iteration=True,
            max_bytes=65536,
            max_keys=100,
        ),
        signal_rules=[
            {"name": "allow-self", "signals": ["@all"], "target": {"type": "self"}, "decision": "allow"},
            {"name": "allow-children", "signals": ["@all"], "target": {"type": "children"}, "decision": "allow"},
            {"name": "allow-session", "signals": ["SIGTERM", "SIGINT", "SIGHUP", "SIGUSR1", "SIGUSR2"], "target": {"type": "session"}, "decision": "allow"},
            {"name": "audit-parent", "signals": ["@all"], "target": {"type": "parent"}, "decision": "audit"},
            {"name": "deny-external-fatal", "signals": ["@fatal"], "target": {"type": "external"}, "decision": "deny", "fallback": "audit", "message": "Blocking signal to process outside session"},
            {"name": "deny-system", "signals": ["@all"], "target": {"type": "system"}, "decision": "deny", "fallback": "audit", "message": "Blocking signal to system process"},
        ],
        unix_socket_rules=[
            {"name": "deny-system-sockets", "paths": ["/var/run/**"], "operations": ["connect", "bind", "listen", "sendto"], "decision": "deny"},
        ],
        resource_limits=ResourceLimits(
            max_memory_mb=2048,
            cpu_quota_percent=50,
            pids_max=100,
            command_timeout="5m",
            session_timeout="1h",
            idle_timeout="15m",
        ),
        audit_settings=AuditSettings(
            log_allowed=True,
            log_denied=True,
            log_approved=True,
            include_stdout=True,
            include_stderr=True,
        ),
    )

    return SecureConfig(
        policy=policy,
        install_strategy="download",
        real_paths=True,
        server_config=server_config,
    )


class _ExeAdapter:
    """Wraps an exe.dev VM via SSH gateway into SandboxAdapter."""

    def __init__(self, vm_name: str) -> None:
        self._vm_name = vm_name

    async def _ssh(self, cmd: str, timeout: int = 120) -> ExecResult:
        """Execute a command on the VM via the exe.dev SSH gateway."""
        escaped = cmd.replace("'", "'\\''")
        ssh_args = [*_SSH_OPTS, "exe.dev", f"ssh {self._vm_name} '{escaped}'"]
        try:
            proc = await asyncio.create_subprocess_exec(
                "ssh", *ssh_args,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            stdout_bytes, stderr_bytes = await asyncio.wait_for(
                proc.communicate(), timeout=timeout,
            )
            return ExecResult(
                stdout=(stdout_bytes or b"").decode("utf-8", errors="replace"),
                stderr=_filter_noise((stderr_bytes or b"").decode("utf-8", errors="replace")),
                exit_code=proc.returncode or 0,
            )
        except asyncio.TimeoutError:
            proc.kill()
            return ExecResult(stdout="", stderr="timeout", exit_code=1)
        except Exception as exc:
            return ExecResult(
                stdout=getattr(exc, "stdout", "") or "",
                stderr=getattr(exc, "stderr", "") or str(exc),
                exit_code=1,
            )

    async def exec(
        self,
        cmd: str,
        args: list[str] | None = None,
        opts: ExecOpts | None = None,
    ) -> ExecResult:
        opts = opts or ExecOpts()
        command = f"{env_prefix(opts.env)}{'sudo ' if opts.sudo else ''}{shell_escape(cmd, args)}"

        if opts.detached:
            fire_and_forget(self._ssh(f"nohup {command} > /dev/null 2>&1 &"))
            return ExecResult(stdout="", stderr="", exit_code=0)

        if opts.cwd:
            safe_cwd = opts.cwd.replace("'", "'\\''")
            return await self._ssh(f"cd '{safe_cwd}' && {command}")

        return await self._ssh(command)

    async def write_file(
        self,
        path: str,
        content: str | bytes,
        *,
        sudo: bool = False,
    ) -> None:
        data = content if isinstance(content, bytes) else content.encode()
        b64 = base64.b64encode(data).decode("ascii")
        safe_path = path.replace("'", "'\\''")
        result = await self._ssh(
            f"printf '%s' '{b64}' | base64 -d > '{safe_path}'"
        )
        if result.exit_code != 0:
            raise Exception(
                f"writeFile failed (exit {result.exit_code}): {result.stderr}"
            )

    async def read_file(self, path: str) -> str:
        safe_path = path.replace("'", "'\\''")
        result = await self._ssh(f"cat '{safe_path}'")
        if result.exit_code != 0:
            raise Exception(
                f"readFile failed (exit {result.exit_code}): {result.stderr}"
            )
        return result.stdout

    async def stop(self) -> None:
        # No-op — exe.dev VMs are persistent.
        # Destroy externally: ssh exe.dev rm <vmName>
        pass

    async def file_exists(self, path: str) -> bool:
        result = await self.exec("test", ["-f", path])
        return result.exit_code == 0
