"""
Docker Sandbox — Ephemeral container execution for remote tasks.

Any code or task received from a room peer MUST run in an ephemeral Docker
container, never on the host.  If Docker is not available, remote task
execution is refused entirely.

Container spec:
    Image:   python:3.12-slim
    CPU:     1 core
    RAM:     512 MB
    Timeout: 60 s (configurable)
    Network: disabled by default
    Mounts:  project dir read-only (write if explicitly allowed)
"""

from __future__ import annotations

import json
import logging
import shutil
import subprocess
import tempfile
import uuid
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional

logger = logging.getLogger("brynq.security.docker_sandbox")

DOCKER_IMAGE = "python:3.12-slim"
DEFAULT_CPU = "1"
DEFAULT_MEM = "512m"
DEFAULT_TIMEOUT = 60  # seconds


@dataclass
class SandboxResult:
    """Result of a sandboxed execution."""

    success: bool
    exit_code: int
    stdout: str
    stderr: str
    container_id: str
    timed_out: bool = False


@dataclass
class SandboxConfig:
    """Configuration for a sandbox run."""

    cpu: str = DEFAULT_CPU
    memory: str = DEFAULT_MEM
    timeout: int = DEFAULT_TIMEOUT
    network_enabled: bool = False
    writable_mount: bool = False
    env_vars: dict[str, str] = field(default_factory=dict)


def docker_available() -> bool:
    """Check whether Docker is installed and the daemon is running."""
    docker_bin = shutil.which("docker")
    if docker_bin is None:
        return False
    try:
        result = subprocess.run(
            ["docker", "info"],
            capture_output=True,
            timeout=10,
        )
        return result.returncode == 0
    except (subprocess.TimeoutExpired, OSError):
        return False


class DockerSandbox:
    """Ephemeral Docker container for running untrusted code."""

    def __init__(self, config: SandboxConfig | None = None):
        self.config = config or SandboxConfig()
        self._available: bool | None = None

    @property
    def available(self) -> bool:
        if self._available is None:
            self._available = docker_available()
        return self._available

    def run(
        self,
        code: str,
        project_dir: str | Path | None = None,
        language: str = "python",
    ) -> SandboxResult:
        """Run *code* inside an ephemeral container.

        Parameters
        ----------
        code:
            Source code to execute.
        project_dir:
            Optional host directory to mount into the container.
        language:
            Execution language (only ``python`` supported today).

        Returns
        -------
        SandboxResult
        """
        if not self.available:
            return SandboxResult(
                success=False,
                exit_code=-1,
                stdout="",
                stderr="Docker is not available. Remote task execution refused.",
                container_id="",
                timed_out=False,
            )

        container_name = f"brynq-sandbox-{uuid.uuid4().hex[:12]}"
        tmp_dir = None

        try:
            # Write code to a temp file that gets mounted into the container
            tmp_dir = tempfile.mkdtemp(prefix="brynq_sandbox_")
            script_path = Path(tmp_dir) / "task.py"
            script_path.write_text(code, encoding="utf-8")

            cmd = self._build_docker_cmd(
                container_name=container_name,
                script_dir=tmp_dir,
                project_dir=str(project_dir) if project_dir else None,
            )

            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=self.config.timeout + 5,  # small grace period
            )

            return SandboxResult(
                success=result.returncode == 0,
                exit_code=result.returncode,
                stdout=result.stdout,
                stderr=result.stderr,
                container_id=container_name,
                timed_out=False,
            )

        except subprocess.TimeoutExpired:
            # Kill the container
            self._kill_container(container_name)
            return SandboxResult(
                success=False,
                exit_code=-1,
                stdout="",
                stderr=f"Execution timed out after {self.config.timeout}s",
                container_id=container_name,
                timed_out=True,
            )

        except Exception as exc:
            logger.error("Sandbox execution failed: %s", exc)
            return SandboxResult(
                success=False,
                exit_code=-1,
                stdout="",
                stderr=str(exc),
                container_id=container_name,
                timed_out=False,
            )

        finally:
            # Always attempt cleanup
            self._remove_container(container_name)
            if tmp_dir:
                shutil.rmtree(tmp_dir, ignore_errors=True)

    # ── internal ──────────────────────────────────────────────────────

    def _build_docker_cmd(
        self,
        container_name: str,
        script_dir: str,
        project_dir: str | None,
    ) -> list[str]:
        cmd = [
            "docker", "run",
            "--name", container_name,
            "--rm",
            "--cpus", self.config.cpu,
            "--memory", self.config.memory,
        ]

        # Network
        if not self.config.network_enabled:
            cmd += ["--network", "none"]

        # Mount script directory
        cmd += ["-v", f"{script_dir}:/sandbox:ro"]

        # Mount project directory if provided
        if project_dir:
            mode = "rw" if self.config.writable_mount else "ro"
            cmd += ["-v", f"{project_dir}:/project:{mode}"]

        # Environment variables
        for key, val in self.config.env_vars.items():
            cmd += ["-e", f"{key}={val}"]

        # Image + entrypoint
        cmd += [
            DOCKER_IMAGE,
            "python", "/sandbox/task.py",
        ]

        return cmd

    def _kill_container(self, name: str) -> None:
        try:
            subprocess.run(
                ["docker", "kill", name],
                capture_output=True,
                timeout=10,
            )
        except Exception:
            pass

    def _remove_container(self, name: str) -> None:
        try:
            subprocess.run(
                ["docker", "rm", "-f", name],
                capture_output=True,
                timeout=10,
            )
        except Exception:
            pass
