"""Container runtime — optional containerized workspace execution."""

from __future__ import annotations

import hashlib
import shutil
import subprocess
from pathlib import Path

from nestdx.config.schema import EnvironmentConfig, NetworkPolicy, ResourcePolicy
from nestdx.runtime.network import NetworkPolicyEnforcer


class NoContainerRuntimeError(Exception):
    """No container runtime (docker/podman/nerdctl) found on PATH."""


class ContainerBuildError(Exception):
    """Container image build failed."""


class ContainerRunError(Exception):
    """Container start or exec failed."""


_RUNTIMES = ("docker", "podman", "nerdctl")


def detect_runtime() -> str:
    """Find the first available container runtime on PATH.

    Checks docker, podman, nerdctl in order.

    Raises:
        NoContainerRuntimeError: If none are installed.
    """
    for rt in _RUNTIMES:
        if shutil.which(rt) is not None:
            return rt
    raise NoContainerRuntimeError(
        "No container runtime found. Install Docker, Podman, or nerdctl.\n"
        "  Docker:  https://docs.docker.com/get-docker/\n"
        "  Podman:  https://podman.io/getting-started/installation\n"
        "  nerdctl: https://github.com/containerd/nerdctl"
    )


def _config_hash(config: EnvironmentConfig) -> str:
    """Hash the environment config for image cache keying."""
    parts = [
        config.base,
        *config.setup,
        config.dockerfile or "",
    ]
    content = "\n".join(parts).encode()
    return hashlib.sha256(content).hexdigest()[:16]


def generate_dockerfile(config: EnvironmentConfig) -> str:
    """Generate a Dockerfile from EnvironmentConfig.

    If config.dockerfile is set, returns its contents instead.
    """
    if config.dockerfile:
        path = Path(config.dockerfile)
        if path.is_file():
            return path.read_text()
        raise ContainerBuildError(f"Custom Dockerfile not found: {config.dockerfile}")

    lines = [f"FROM {config.base}"]
    lines.append("WORKDIR /workspace")

    for cmd in config.setup:
        lines.append(f"RUN {cmd}")

    return "\n".join(lines) + "\n"


class ContainerRuntime:
    """Manages containerized workspace lifecycle."""

    def __init__(
        self,
        config: EnvironmentConfig,
        project_root: Path,
        resources: ResourcePolicy | None = None,
        network_policy: NetworkPolicy | None = None,
    ):
        self.config = config
        self.project_root = project_root
        self.resources = resources
        self.runtime = detect_runtime()
        self._container_id: str | None = None
        self._image_id: str | None = None
        self._network_enforcer: NetworkPolicyEnforcer | None = None
        if network_policy and network_policy.block_all_other_egress:
            self._network_enforcer = NetworkPolicyEnforcer(
                network_policy, self.runtime, project_root
            )

    def build(self) -> str:
        """Build the workspace image. Returns image tag.

        Uses a content-based hash to skip rebuilds when config hasn't changed.
        """
        tag = f"nestdx-workspace:{_config_hash(self.config)}"

        # Check if image already exists
        result = subprocess.run(
            [self.runtime, "image", "inspect", tag],
            capture_output=True,
            timeout=30,
        )
        if result.returncode == 0:
            self._image_id = tag
            return tag

        # Generate and build
        dockerfile_content = generate_dockerfile(self.config)
        result = subprocess.run(
            [self.runtime, "build", "-t", tag, "-f", "-", "."],
            input=dockerfile_content.encode(),
            capture_output=True,
            cwd=self.project_root,
            timeout=600,
        )
        if result.returncode != 0:
            stderr = result.stderr.decode(errors="replace")
            raise ContainerBuildError(
                f"Image build failed (exit {result.returncode}):\n{stderr}"
            )

        self._image_id = tag
        return tag

    def start(self, session_id: str) -> str:
        """Start a workspace container. Returns container ID."""
        if self._image_id is None:
            self.build()

        container_name = f"nestdx-{session_id}"

        cmd: list[str] = [
            self.runtime,
            "run",
            "-d",
            "--name",
            container_name,
            "-v",
            f"{self.project_root}:/workspace",
            "-w",
            "/workspace",
        ]

        # Network isolation (must be before image name)
        if self._network_enforcer and self._network_enforcer.should_enforce:
            cmd.extend(self._network_enforcer.build_run_args())

        # Env file
        if self.config.env_file:
            env_path = self.project_root / self.config.env_file
            if env_path.is_file():
                cmd.extend(["--env-file", str(env_path)])

        # Extra volumes
        for vol in self.config.volumes:
            cmd.extend(["-v", vol])

        # Resource limits
        if self.resources:
            if self.resources.max_cpu:
                cmd.extend(["--cpus", self.resources.max_cpu])
            if self.resources.max_memory:
                cmd.extend(["--memory", self.resources.max_memory])

        cmd.append(self._image_id)  # type: ignore[arg-type]
        cmd.extend(["tail", "-f", "/dev/null"])  # Keep container alive

        result = subprocess.run(
            cmd,
            capture_output=True,
            timeout=60,
        )
        if result.returncode != 0:
            stderr = result.stderr.decode(errors="replace")
            raise ContainerRunError(
                f"Container start failed (exit {result.returncode}):\n{stderr}"
            )

        self._container_id = result.stdout.decode().strip()[:12]

        # Apply network policy after container is running
        if self._network_enforcer and self._network_enforcer.should_enforce:
            self._network_enforcer.setup_network(self._container_id, session_id)

        return self._container_id

    def exec_cmd(self, command: list[str]) -> subprocess.CompletedProcess:
        """Execute a command inside the running container.

        Returns the CompletedProcess with full stdin/stdout passthrough.
        """
        if self._container_id is None:
            raise ContainerRunError("No running container. Call start() first.")

        container_name = self._container_id
        return subprocess.run(
            [self.runtime, "exec", "-it", container_name, *command],
        )

    def stop(self) -> None:
        """Stop and remove the workspace container."""
        if self._container_id is None:
            return

        container_name = self._container_id

        # Teardown network policy before stopping container
        if self._network_enforcer:
            self._network_enforcer.teardown_network(container_name)

        # Stop
        subprocess.run(
            [self.runtime, "stop", container_name],
            capture_output=True,
            timeout=30,
        )
        # Remove
        subprocess.run(
            [self.runtime, "rm", "-f", container_name],
            capture_output=True,
            timeout=30,
        )

        self._container_id = None

    @property
    def is_running(self) -> bool:
        """Check if the workspace container is active."""
        if self._container_id is None:
            return False

        result = subprocess.run(
            [self.runtime, "inspect", "-f", "{{.State.Running}}", self._container_id],
            capture_output=True,
            timeout=10,
        )
        return result.returncode == 0 and b"true" in result.stdout.lower()

    @property
    def container_id(self) -> str | None:
        return self._container_id

    @property
    def image_id(self) -> str | None:
        return self._image_id
