"""
Docker Executor — Run Python code in isolated Docker containers.

Manages container lifecycle for the local Runtime. Each execution
gets an ephemeral container with strict resource limits.
"""
import asyncio
import json
import os
import tempfile
import uuid
from typing import Dict, Any, List, Optional

from .config import RuntimeConfig

# Image mapping (mirrors manager/docker_sandbox_backend.py)
IMAGES = {
    "default": "neura-sandbox-default",
    "neura-ml": "neura-sandbox-ml",
    "neura-data": "neura-sandbox-data",
    "neura-scrape": "neura-sandbox-scrape",
}


class DockerExecutor:
    """Manages Docker containers for code execution."""

    def __init__(self, config: RuntimeConfig):
        self.config = config
        self._containers: Dict[str, str] = {}  # session_id -> container_name

    async def check_docker(self) -> bool:
        """Check if Docker is available."""
        try:
            proc = await asyncio.create_subprocess_exec(
                "docker", "info",
                stdout=asyncio.subprocess.DEVNULL,
                stderr=asyncio.subprocess.DEVNULL,
            )
            await proc.wait()
            return proc.returncode == 0
        except FileNotFoundError:
            return False

    async def create_container(
        self,
        session_id: str,
        template: str = "default",
    ) -> Dict[str, Any]:
        """Create an isolated container for a session."""
        image = IMAGES.get(template, IMAGES["default"])
        container_name = f"neura-rt-{session_id[:12]}"

        # Check image exists
        proc = await asyncio.create_subprocess_exec(
            "docker", "image", "inspect", image,
            stdout=asyncio.subprocess.DEVNULL,
            stderr=asyncio.subprocess.DEVNULL,
        )
        await proc.wait()
        if proc.returncode != 0:
            return {"success": False, "error": f"Image {image} not found. Run: neura-runtime build-images"}

        cmd = [
            "docker", "run", "-d",
            "--name", container_name,
            "--network=none",
            "--read-only",
            "--tmpfs", "/tmp:rw,noexec,nosuid,size=512m",
            "--tmpfs", "/home/sandbox:rw,size=1g",
            f"--memory={self.config.docker_memory_limit}",
            f"--cpus={self.config.docker_cpu_limit}",
            "--security-opt", "no-new-privileges",
            "--pids-limit=256",
            image,
        ]

        proc = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout, stderr = await proc.communicate()

        if proc.returncode != 0:
            return {"success": False, "error": stderr.decode()}

        self._containers[session_id] = container_name
        return {"success": True, "container": container_name}

    async def execute_python(
        self,
        session_id: str,
        code: str,
        timeout: int = 300,
        packages: Optional[List[str]] = None,
    ) -> Dict[str, Any]:
        """Execute Python code in a session's container."""
        container = self._containers.get(session_id)
        if not container:
            # Auto-create container
            result = await self.create_container(session_id)
            if not result["success"]:
                return {"success": False, "output": "", "error": result["error"]}
            container = result["container"]

        # Install packages if needed
        if packages:
            await self._exec(container, f"pip install --quiet {' '.join(packages)}", network=True)

        # Write code to container
        proc = await asyncio.create_subprocess_exec(
            "docker", "exec", "-i", container,
            "bash", "-c", "cat > /tmp/run.py",
            stdin=asyncio.subprocess.PIPE,
            stdout=asyncio.subprocess.DEVNULL,
            stderr=asyncio.subprocess.DEVNULL,
        )
        await proc.communicate(input=code.encode())

        # Execute
        result = await self._exec(container, "python3 /tmp/run.py", timeout=timeout)

        return {
            "success": result["returncode"] == 0,
            "output": result["stdout"],
            "error": result["stderr"] if result["returncode"] != 0 else "",
        }

    async def upload_file(
        self,
        session_id: str,
        filename: str,
        content: bytes,
        directory: str = "/home/sandbox",
    ) -> Dict[str, Any]:
        """Upload a file into a container."""
        container = self._containers.get(session_id)
        if not container:
            return {"success": False, "error": "No container for session"}

        with tempfile.NamedTemporaryFile(delete=False) as tmp:
            tmp.write(content)
            tmp_path = tmp.name

        dest = f"{directory}/{filename}"
        proc = await asyncio.create_subprocess_exec(
            "docker", "cp", tmp_path, f"{container}:{dest}",
            stdout=asyncio.subprocess.DEVNULL,
            stderr=asyncio.subprocess.PIPE,
        )
        _, stderr = await proc.communicate()
        os.unlink(tmp_path)

        if proc.returncode != 0:
            return {"success": False, "error": stderr.decode()}
        return {"success": True, "path": dest}

    async def download_file(self, session_id: str, path: str) -> Dict[str, Any]:
        """Download a file from a container."""
        container = self._containers.get(session_id)
        if not container:
            return {"success": False, "error": "No container for session"}

        with tempfile.NamedTemporaryFile(delete=False) as tmp:
            tmp_path = tmp.name

        proc = await asyncio.create_subprocess_exec(
            "docker", "cp", f"{container}:{path}", tmp_path,
            stdout=asyncio.subprocess.DEVNULL,
            stderr=asyncio.subprocess.PIPE,
        )
        _, stderr = await proc.communicate()

        if proc.returncode != 0:
            os.unlink(tmp_path)
            return {"success": False, "error": stderr.decode()}

        with open(tmp_path, "rb") as f:
            content = f.read()
        os.unlink(tmp_path)
        return {"success": True, "content": content, "size": len(content)}

    async def destroy_container(self, session_id: str) -> bool:
        """Remove a session's container."""
        container = self._containers.pop(session_id, None)
        if not container:
            return False

        proc = await asyncio.create_subprocess_exec(
            "docker", "rm", "-f", container,
            stdout=asyncio.subprocess.DEVNULL,
            stderr=asyncio.subprocess.DEVNULL,
        )
        await proc.wait()
        return True

    async def destroy_all(self):
        """Remove all managed containers."""
        for session_id in list(self._containers.keys()):
            await self.destroy_container(session_id)

    async def _exec(
        self,
        container: str,
        command: str,
        timeout: int = 300,
        network: bool = False,
    ) -> Dict[str, Any]:
        """Execute a command inside a container."""
        if network:
            await asyncio.create_subprocess_exec(
                "docker", "network", "connect", "bridge", container,
                stdout=asyncio.subprocess.DEVNULL,
                stderr=asyncio.subprocess.DEVNULL,
            )

        try:
            proc = await asyncio.create_subprocess_exec(
                "docker", "exec", "-u", "sandbox", container,
                "bash", "-c", command,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)

            return {
                "returncode": proc.returncode,
                "stdout": stdout.decode(errors="replace"),
                "stderr": stderr.decode(errors="replace"),
            }
        except asyncio.TimeoutError:
            return {"returncode": -1, "stdout": "", "stderr": f"Timed out after {timeout}s"}
        finally:
            if network:
                await asyncio.create_subprocess_exec(
                    "docker", "network", "disconnect", "bridge", container,
                    stdout=asyncio.subprocess.DEVNULL,
                    stderr=asyncio.subprocess.DEVNULL,
                )
