from __future__ import annotations

import shutil

from app.core.command_runner import CommandError, CommandRunner


DOCKER_REQUIRED_MESSAGE = "Docker runtime is required"


def image_reference(component: dict) -> str:
    image = component["image"]
    digest = component.get("digest")
    tag = component.get("tag")

    if "@" in image:
        return image
    if digest:
        return f"{_image_without_tag(image)}@{digest}"
    if _image_has_tag(image) or not tag:
        return image
    return f"{image}:{tag}"


def _image_has_tag(image: str) -> bool:
    last_slash = image.rfind("/")
    last_colon = image.rfind(":")
    return last_colon > last_slash


def _image_without_tag(image: str) -> str:
    if not _image_has_tag(image):
        return image
    return image[: image.rfind(":")]


class DockerInstaller:
    def __init__(self, command_runner: CommandRunner) -> None:
        self.command_runner = command_runner

    def ensure_runtime(self, auto_install: bool = False) -> None:
        if not shutil.which("docker"):
            if not auto_install:
                raise RuntimeError(DOCKER_REQUIRED_MESSAGE)
            self.install_runtime()

        try:
            self._verify_runtime()
            return
        except CommandError as error:
            self._log("warning", self._runtime_error_message(error))

        self._start_runtime()
        try:
            self._verify_runtime()
        except CommandError as error:
            raise RuntimeError(self._runtime_error_message(error)) from error

    def install_runtime(self) -> None:
        if not shutil.which("apt-get"):
            raise RuntimeError(
                f"{DOCKER_REQUIRED_MESSAGE}. Install Docker Engine on this client or disable Docker packages."
            )

        self._log("info", "Docker runtime not found; installing docker.io")
        try:
            self.command_runner.run(["apt-get", "update"], timeout=600)
            self.command_runner.run(
                ["env", "DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y", "docker.io"],
                timeout=1200,
            )
        except CommandError as error:
            raise RuntimeError(
                f"{DOCKER_REQUIRED_MESSAGE}: failed to install docker.io with apt-get: "
                f"{self._command_error_detail(error)}"
            ) from error
        self._start_runtime()

    def _verify_runtime(self) -> None:
        self.command_runner.run(["docker", "version", "--format", "{{.Server.Version}}"])

    def _start_runtime(self) -> None:
        if shutil.which("systemctl"):
            try:
                self.command_runner.run(["systemctl", "enable", "--now", "docker"], timeout=120)
                return
            except CommandError as error:
                self._log("warning", f"Could not start Docker with systemctl: {self._command_error_detail(error)}")

        if shutil.which("service"):
            try:
                self.command_runner.run(["service", "docker", "start"], timeout=120)
            except CommandError as error:
                self._log("warning", f"Could not start Docker service: {self._command_error_detail(error)}")

    def _runtime_error_message(self, error: CommandError) -> str:
        detail = self._command_error_detail(error)
        return f"{DOCKER_REQUIRED_MESSAGE}: {detail}" if detail else DOCKER_REQUIRED_MESSAGE

    def _command_error_detail(self, error: CommandError) -> str:
        output = (error.stderr or error.stdout or "").strip()
        if not output:
            return str(error)
        return output.splitlines()[-1].strip()

    def _log(self, level: str, message: str) -> None:
        if self.command_runner.task_id:
            self.command_runner.repository.add_log(self.command_runner.task_id, level, message)

    def pull_image(self, reference: str) -> None:
        self.command_runner.run(["docker", "pull", reference])

    def recreate_container(self, app_id: str, component: dict) -> None:
        container_name = component["containerName"]
        reference = image_reference(component)

        self.remove_container(container_name)

        command = [
            "docker",
            "run",
            "-d",
            "--name",
            container_name,
            "--restart",
            component.get("restartPolicy", "unless-stopped"),
            "--label",
            f"local-installer-agent.app-id={app_id}",
            "--label",
            f"local-installer-agent.component-id={component['componentId']}",
        ]

        for name, value in sorted((component.get("env") or {}).items()):
            command.extend(["-e", f"{name}={value}"])
        for port in component.get("ports") or []:
            command.extend(["-p", port])
        for volume in component.get("volumes") or []:
            command.extend(["-v", volume])

        command.append(reference)
        self.command_runner.run(command)

    def remove_container(self, container_name: str, remove_volumes: bool = False) -> None:
        result = self.command_runner.run([
            "docker",
            "ps",
            "-aq",
            "--filter",
            f"name=^/{container_name}$",
        ])
        if result.stdout.strip():
            command = ["docker", "rm", "-f"]
            if remove_volumes:
                command.append("-v")
            command.append(container_name)
            self.command_runner.run(command)

    def remove_labeled_containers(self, app_id: str, component_id: str, remove_volumes: bool = False) -> None:
        result = self.command_runner.run([
            "docker",
            "ps",
            "-aq",
            "--filter",
            f"label=local-installer-agent.app-id={app_id}",
            "--filter",
            f"label=local-installer-agent.component-id={component_id}",
        ])
        container_ids = [line.strip() for line in result.stdout.splitlines() if line.strip()]
        if not container_ids:
            return

        command = ["docker", "rm", "-f"]
        if remove_volumes:
            command.append("-v")
        command.extend(container_ids)
        self.command_runner.run(command)

    def remove_image(self, reference: str) -> None:
        self.command_runner.run(["docker", "image", "rm", reference])

    def assert_container_running(self, container_name: str) -> None:
        result = self.command_runner.run([
            "docker",
            "inspect",
            "-f",
            "{{.State.Running}}",
            container_name,
        ])
        if result.stdout.strip().lower() != "true":
            raise RuntimeError(f"Docker container is not running: {container_name}")
