"""Modal adapter — wraps Modal Sandbox for gVisor-based sandboxes.

Modal sandboxes run on gVisor which lacks seccomp user-notify and
full FUSE support. Use modal_defaults() for ptrace-optimized config.
"""
from __future__ import annotations

import base64
from typing import Any

from ..core.shell import shell_escape, fire_and_forget
from ..core.types import (
    ApprovalsConfig,
    AuditConfig,
    CgroupsConfig,
    DlpConfig,
    ExecOpts,
    ExecResult,
    FuseConfig,
    GrpcConfig,
    HealthConfig,
    LoggingConfig,
    MetricsConfig,
    NetworkInterceptConfig,
    PtraceConfig,
    PtracePerformanceConfig,
    PtraceTraceConfig,
    SandboxLimitsConfig,
    SecureConfig,
    ServerConfig,
    SessionsConfig,
    UnixSocketsConfig,
)
from ..policies.presets import agent_default


def modal(sandbox: Any) -> _ModalAdapter:
    """Wrap a Modal Sandbox object into a SandboxAdapter.

    The sandbox parameter is typed as Any to avoid a hard dependency
    on the Modal SDK. Pass any object whose .exec() accepts variadic
    string arguments and returns a process with .stdout.read(),
    .stderr.read(), .returncode, and .wait().
    """
    return _ModalAdapter(sandbox)


def modal_defaults() -> SecureConfig:
    """Return Modal/gVisor-optimized defaults for SecureConfig.

    Key differences from other providers:
    - ptrace enabled (gVisor blocks seccomp user-notify)
    - seccomp_prefilter disabled (gVisor blocks BPF injection)
    - FUSE deferred (may not be available on gVisor)
    - unix_sockets disabled (mutually exclusive with ptrace)
    - cgroups disabled (Modal handles resource limits)
    - allow_degraded enabled (graceful fallback for FUSE/seccomp)

    Usage::

        secured = await secure_sandbox(
            modal(sb),
            modal_defaults(),  # or override: modal_defaults().model_copy(update={...})
        )
    """
    server_config = ServerConfig(
        grpc=GrpcConfig(addr="127.0.0.1:9090"),
        logging=LoggingConfig(level="info", format="text", output="stderr"),
        sessions=SessionsConfig(
            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=True,
        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",
        ),
        cgroups=CgroupsConfig(enabled=False),
        unix_sockets=UnixSocketsConfig(enabled=False),
        ptrace=PtraceConfig(
            enabled=True,
            attach_mode="children",
            mask_tracer_pid="off",
            trace=PtraceTraceConfig(
                execve=True, file=False, network=True, signal=True,
            ),
            performance=PtracePerformanceConfig(
                seccomp_prefilter=False, max_tracees=500, max_hold_ms=5000,
            ),
            on_attach_failure="fail_open",
        ),
        env_inject={"BASH_ENV": "/usr/lib/agentsh/bash_startup.sh"},
        dlp=DlpConfig(
            mode="redact",
            patterns={"api_keys": True, "credit_card": True, "ssn": True},
        ),
        approvals=ApprovalsConfig(enabled=False),
        metrics=MetricsConfig(enabled=True, path="/metrics"),
        health=HealthConfig(path="/health", readiness_path="/ready"),
    )

    # agentDefault() now includes comprehensive system paths (usr, lib, dev,
    # proc/self, etc.) needed for ptrace/file_monitor mode. No provider-specific
    # file rules needed for Modal.
    policy = agent_default()

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


class _ModalAdapter:
    """Wraps a Modal Sandbox object into SandboxAdapter."""

    def __init__(self, sandbox: Any) -> None:
        self._sandbox = sandbox

    async def _run(self, cmd: str) -> ExecResult:
        """Run a shell command via sandbox.exec('sh', '-c', cmd)."""
        try:
            proc = await self._sandbox.exec("sh", "-c", cmd)
            await proc.wait()
            stdout_read = getattr(proc.stdout, "read", None)
            stderr_read = getattr(proc.stderr, "read", None)
            stdout = (await stdout_read()) if callable(stdout_read) else (proc.stdout or "")
            stderr = (await stderr_read()) if callable(stderr_read) else (proc.stderr or "")
            exit_code = getattr(proc, "returncode", 0)
            if callable(exit_code):
                exit_code = exit_code()
            return ExecResult(
                stdout=stdout or "",
                stderr=stderr or "",
                exit_code=exit_code or 0,
            )
        except Exception as exc:
            return ExecResult(
                stdout=getattr(exc, "stdout", "") or "",
                stderr=getattr(exc, "stderr", "") or str(exc),
                exit_code=getattr(exc, "returncode", None)
                or getattr(exc, "exit_code", None)
                or 1,
            )

    async def exec(
        self,
        cmd: str,
        args: list[str] | None = None,
        opts: ExecOpts | None = None,
    ) -> ExecResult:
        opts = opts or ExecOpts()
        # Modal containers run as root — drop sudo
        command = shell_escape(cmd, args)

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

        return await self._run(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._run(
            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._run(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:
        terminate = getattr(self._sandbox, "terminate", None)
        if terminate:
            await terminate()

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