from __future__ import annotations

import json
import re
import subprocess
from typing import Any

from atlas_runner.errors import ErrorCode, RunnerError

_DAEMON_MARKERS = (
    "cannot connect to the docker daemon",
    "is the docker daemon running",
    "connection refused",
)


class DockerCLI:
    """Low-level wrapper around the ``docker`` CLI.

    Every Docker operation in atlas-runner goes through this class so that
    ``subprocess.run`` is isolated to a single module (clean mock boundary).
    """

    def __init__(self, docker_host: str | None = None) -> None:
        self._base: list[str] = ["docker"]
        if docker_host is not None:
            self._base = ["docker", "--host", docker_host]

    # ------------------------------------------------------------------
    # Core executor
    # ------------------------------------------------------------------

    def _run(
        self,
        args: list[str],
        *,
        check: bool = True,
        capture_output: bool = True,
        input_data: str | None = None,
        timeout: int | None = None,
    ) -> subprocess.CompletedProcess[str]:
        cmd = self._base + args
        try:
            result = subprocess.run(
                cmd,
                check=False,
                capture_output=capture_output,
                text=True,
                input=input_data,
                timeout=timeout,
            )
        except subprocess.TimeoutExpired as exc:
            raise RunnerError(
                code=ErrorCode.BACKEND_ERROR,
                message=f"Docker command timed out after {timeout}s",
                details={"command": cmd, "timeout": timeout},
            ) from exc

        if check and result.returncode != 0:
            stderr = result.stderr.strip() if result.stderr else ""
            if any(m in stderr.lower() for m in _DAEMON_MARKERS):
                raise RunnerError(
                    code=ErrorCode.DOCKER_DAEMON_UNAVAILABLE,
                    message="Docker daemon is not available",
                    details={"stderr": stderr},
                )
            raise RunnerError(
                code=ErrorCode.BACKEND_ERROR,
                message=f"Docker command failed (exit {result.returncode})",
                details={"command": cmd, "stderr": stderr},
            )

        return result

    # ------------------------------------------------------------------
    # Public methods
    # ------------------------------------------------------------------

    def login(
        self,
        registry: str,
        username: str,
        password: str,
    ) -> None:
        try:
            self._run(
                ["login", "--username", username, "--password-stdin", registry],
                input_data=password,
            )
        except RunnerError as exc:
            if exc.code == ErrorCode.BACKEND_ERROR:
                raise RunnerError(
                    code=ErrorCode.LOGIN_FAILED,
                    message=f"Login failed for registry: {registry}",
                    details=exc.details,
                ) from exc
            raise

    def pull(self, image: str) -> None:
        try:
            self._run(["pull", image])
        except RunnerError as exc:
            if exc.code == ErrorCode.BACKEND_ERROR:
                raise RunnerError(
                    code=ErrorCode.IMAGE_PULL_FAILED,
                    message=f"Failed to pull image: {image}",
                    details=exc.details,
                ) from exc
            raise

    def run(self, args: list[str]) -> str:
        result = self._run(["run"] + args)
        return result.stdout.strip()

    def stop(self, name: str) -> None:
        self._run(["stop", name], check=False)

    def rm(self, name: str) -> None:
        self._run(["rm", name], check=False)

    def inspect(self, name: str) -> dict[str, Any]:
        result = self._run(["inspect", name])
        data: list[dict[str, Any]] = json.loads(result.stdout)
        return data[0]

    def ps(self, filters: list[str] | None = None) -> list[dict[str, Any]]:
        args = ["ps", "--format", "{{json .}}", "--no-trunc"]
        for f in filters or []:
            args.extend(["--filter", f])
        result = self._run(args)
        entries: list[dict[str, Any]] = []
        for line in result.stdout.strip().splitlines():
            if line:
                entries.append(json.loads(line))
        return entries

    def logs(self, name: str) -> str:
        result = self._run(["logs", name])
        return result.stdout

    def container_exists(self, name: str) -> bool:
        escaped = re.escape(name)
        result = self._run(
            ["ps", "-a", "--filter", f"name=^/{escaped}$", "--format", "{{.ID}}"],
        )
        return bool(result.stdout.strip())
