"""Docker as Object - High-level Docker operations abstraction.

This module provides a high-level, object-oriented interface for Docker operations,
abstracting away the complexity of the Docker Python SDK and providing a unified
API for container lifecycle management, resource management, and service orchestration.
"""

import time
import random
from typing import Optional, List, Dict, Any
from dataclasses import dataclass, field

import docker
from docker.models.services import Service as DockerServiceObject
from docker.models.containers import Container as DockerContainer
from docker.models.images import Image as DockerImage

from ..http.client import HTTPClient
from ..utils.logging import get_logger


class DockerConnectionError(Exception):
    """Docker connection specific exception.

    Raised when Docker operations fail due to connection issues,
    Docker daemon unavailability, or API errors.
    """


@dataclass
class ImageConfig:
    """Docker image configuration data class."""

    container: str = field(default="")
    name: str = field(default="")
    tag: str = field(default="stable")


@dataclass
class ResourceConfig:
    """Resource usage configuration data class."""

    use_gpu: bool = field(default=False)
    cpu_limit: int = field(default=1)
    ram_limit: int = field(default=1024)  # MB
    disk_limit: int = field(default=1)  # GB


@dataclass
class ServiceConfig:
    """Service configuration data class."""

    name: str = field(default="")
    image: ImageConfig = field(default_factory=ImageConfig)
    service_type: str = field(default="unknown")
    networks: List[str] = field(default_factory=list)
    environments: Dict[str, str] = field(default_factory=dict)
    parent: Optional[int] = field(default=None)
    resource_config: ResourceConfig = field(default_factory=ResourceConfig)
    node_label: str = field(default="calculation_worker")


class DockerDAO:  # pylint: disable=too-many-instance-attributes
    """Docker as Object - High-level Docker operations interface.

    Provides object-oriented abstraction over Docker Python SDK operations,
    including container lifecycle management, image operations, node management,
    and service orchestration.
    """

    def __init__(
        self,
        docker_host: Optional[str] = None,
        registry_config: Optional[Dict] = None,
        http_client: Optional[HTTPClient] = None,
    ):
        """Initialize Docker DAO.

        Args:
            docker_host: Docker daemon host URL (default: from environment)
            registry_config: Docker registry configuration
            http_client: HTTP client for API communication
        """
        self.logger = get_logger("docker.dao")
        self.http_client = http_client

        # Initialize Docker client
        try:
            if docker_host:
                self.docker_client = docker.DockerClient(base_url=docker_host)
            else:
                self.docker_client = docker.from_env()
        except docker.errors.DockerException as docker_error:
            raise DockerConnectionError(
                f"Cannot connect to Docker daemon: {docker_error}"
            ) from docker_error

        # Registry configuration
        self.registry_config = registry_config or {}

        # Login to registry if config provided
        if self.registry_config:
            self._login_to_registry()

    def _login_to_registry(self):
        """Login to Docker registry using provided configuration."""
        try:
            self.docker_client.login(
                registry=self.registry_config.get("url"),
                username=self.registry_config.get("username"),
                password=self.registry_config.get("password"),
            )
            self.logger.info("Successfully logged in to Docker registry")
        except docker.errors.APIError as api_error:
            self.logger.error("Failed to login to Docker registry: %s", api_error)
            raise DockerConnectionError(f"Registry login failed: {api_error}") from api_error

    # Container Operations
    def get_containers(self, all_containers: bool = True) -> List[DockerContainer]:
        """Get list of Docker containers.

        Args:
            all_containers: Include stopped containers (default: True)

        Returns:
            List of Docker container objects

        Raises:
            DockerConnectionError: When Docker API fails
        """
        try:
            return self.docker_client.containers.list(all=all_containers)
        except docker.errors.APIError as api_error:
            self.logger.error("Cannot get containers from Docker: %s", api_error)
            raise DockerConnectionError(f"Failed to list containers: {api_error}") from api_error

    def get_container(self, container_id: str) -> DockerContainer:
        """Get Docker container by ID.

        Args:
            container_id: Container ID or name

        Returns:
            Docker container object

        Raises:
            DockerConnectionError: When container not found or API fails
        """
        try:
            return self.docker_client.containers.get(container_id)
        except docker.errors.NotFound as not_found_error:
            raise DockerConnectionError(f"Container {container_id} not found") from not_found_error
        except docker.errors.APIError as api_error:
            self.logger.error("Cannot get container %s: %s", container_id, api_error)
            raise DockerConnectionError(f"Failed to get container: {api_error}") from api_error

    # Image Operations
    def get_images(self, all_images: bool = True) -> List[DockerImage]:
        """Get list of Docker images.

        Args:
            all_images: Include untagged images (default: True)

        Returns:
            List of Docker image objects
        """
        try:
            return self.docker_client.images.list(all=all_images)
        except docker.errors.APIError as api_error:
            self.logger.error("Cannot get images from Docker: %s", api_error)
            raise DockerConnectionError(f"Failed to list images: {api_error}") from api_error

    def pull_image(self, image_name: str, tag: str = "latest") -> DockerImage:
        """Pull Docker image from registry.

        Args:
            image_name: Image name
            tag: Image tag (default: "latest")

        Returns:
            Docker image object
        """
        try:
            full_image_name = f"{image_name}:{tag}"
            self.logger.info("Pulling image: %s", full_image_name)
            return self.docker_client.images.pull(image_name, tag=tag)
        except docker.errors.NotFound as not_found_error:
            raise DockerConnectionError(f"Image {image_name}:{tag} not found") from not_found_error
        except docker.errors.APIError as api_error:
            self.logger.error("Cannot pull image %s:%s: %s", image_name, tag, api_error)
            raise DockerConnectionError(f"Failed to pull image: {api_error}") from api_error

    # Node Operations (Docker Swarm)
    def get_nodes(self, node_name: Optional[str] = None, node_id: Optional[str] = None) -> List:
        """Get Docker Swarm nodes.

        Args:
            node_name: Filter by exact node name
            node_id: Filter by node ID

        Returns:
            List of Docker node objects
        """
        try:
            if node_name:
                # Exact match filtering for node name
                all_nodes = self.docker_client.nodes.list()
                return [
                    node for node in all_nodes if node.attrs["Description"]["Hostname"] == node_name
                ]
            if node_id:
                return self.docker_client.nodes.list(filters={"id": node_id})
            return self.docker_client.nodes.list()
        except docker.errors.APIError as api_error:
            self.logger.error("Cannot get nodes from Docker: %s", api_error)
            raise DockerConnectionError(f"Failed to list nodes: {api_error}") from api_error

    def get_node(self, node_id: str):
        """Get single Docker Swarm node by ID.

        Args:
            node_id: Node ID to retrieve

        Returns:
            Docker node object
        """
        try:
            return self.docker_client.nodes.get(node_id)
        except docker.errors.NotFound as not_found_error:
            raise DockerConnectionError(f"Node {node_id} not found") from not_found_error
        except docker.errors.APIError as api_error:
            self.logger.error("Cannot get node %s: %s", node_id, api_error)
            raise DockerConnectionError(f"Failed to get node: {api_error}") from api_error

    def get_node_ip(self, node_id: str) -> Optional[str]:
        """Get IP address of Docker Swarm node.

        Args:
            node_id: Node ID

        Returns:
            Node IP address or None if not found
        """
        try:
            node = self.docker_client.nodes.get(node_id)

            # Check manager status first, then worker status
            if node.attrs.get("ManagerStatus"):
                return node.attrs["ManagerStatus"]["Addr"].split(":")[0]
            if node.attrs.get("Status"):
                return node.attrs["Status"]["Addr"].split(":")[0]
            return None

        except docker.errors.NotFound:
            self.logger.warning("Node %s not found", node_id)
            return None
        except docker.errors.APIError as api_error:
            self.logger.error("Cannot get IP for node %s: %s", node_id, api_error)
            raise DockerConnectionError(f"Failed to get node IP: {api_error}") from api_error

    # Service Operations (Docker Swarm)
    def create_service(self, service_config: ServiceConfig) -> DockerServiceObject:
        """Create Docker Swarm service.

        Args:
            service_config: Service configuration

        Returns:
            Created Docker service object
        """
        image_name = f"{service_config.image.container}/{service_config.image.name}"
        full_image = f"{image_name}:{service_config.image.tag}"

        # Pull image first
        self.pull_image(image_name, service_config.image.tag)

        try:
            # Convert resource limits
            cpu_limit = service_config.resource_config.cpu_limit * 1000000000  # nanocpus
            mem_limit = service_config.resource_config.ram_limit * 1000000  # bytes

            service_resources = docker.types.Resources(cpu_limit=cpu_limit, mem_limit=mem_limit)

            # Node selection
            node_key, node_value = self._node_selector(
                service_config.name, service_config.node_label
            )

            # Create service
            docker_service = self.docker_client.services.create(
                name=service_config.name,
                hostname=service_config.name,
                image=full_image,
                env=service_config.environments,
                labels={"service_type": service_config.service_type},
                mode=docker.types.ServiceMode("replicated", 1),
                networks=service_config.networks or ["science_module_callback_net"],
                constraints=[f"node.labels.{node_key} == {node_value}"],
                resources=service_resources,
                log_driver="json-file",
                log_driver_options={
                    "max-size": "10m",
                    "max-file": "3",
                    "labels": service_config.name,
                },
            )

            self.logger.info("Service %s created: %s", service_config.name, docker_service.id)
            return docker_service

        except docker.errors.APIError as api_error:
            self.logger.error("Cannot create service %s: %s", service_config.name, api_error)
            raise DockerConnectionError(f"Failed to create service: {api_error}") from api_error

    def get_service(self, service_id: str) -> DockerServiceObject:
        """Get Docker service by ID.

        Args:
            service_id: Service ID or name

        Returns:
            Docker service object
        """
        try:
            return self.docker_client.services.get(service_id)
        except docker.errors.NotFound as not_found_error:
            raise DockerConnectionError(f"Service {service_id} not found") from not_found_error
        except docker.errors.APIError as api_error:
            self.logger.error("Cannot get service %s: %s", service_id, api_error)
            raise DockerConnectionError(f"Failed to get service: {api_error}") from api_error

    def is_service_running(self, service_id: str) -> bool:
        """Check if Docker service has running tasks.

        Args:
            service_id: Service ID or name

        Returns:
            True if service has running tasks, False otherwise
        """
        try:
            service = self.get_service(service_id)
            return self._any_task_running(service)
        except DockerConnectionError:
            return False

    def delete_service(self, service_id: str):
        """Delete Docker service.

        Args:
            service_id: Service ID or name
        """
        try:
            service = self.docker_client.services.get(service_id)
            service.remove()
            self.logger.info("Service %s deleted", service_id)
        except docker.errors.NotFound:
            self.logger.warning("Service %s not found for deletion", service_id)
        except docker.errors.APIError as api_error:
            self.logger.error("Cannot delete service %s: %s", service_id, api_error)
            raise DockerConnectionError(f"Failed to delete service: {api_error}") from api_error

    # Utility Methods
    def _node_selector(self, service_name: str, preferred_node: Optional[str] = None):
        """Select appropriate node for service deployment.

        Args:
            service_name: Name of service to deploy
            preferred_node: Preferred node label value

        Returns:
            Tuple of (label_key, label_value) for node constraint
        """
        selected_key = "type"
        selected_value = "calculation_worker"

        if preferred_node:
            nodes = self.get_nodes()
            if nodes:
                # Check for worker_label in nodes
                worker_labels = []
                for node in nodes:
                    labels = node.attrs.get("Spec", {}).get("Labels", {})
                    if "worker_label" in labels:
                        worker_labels.append(labels["worker_label"])

                if preferred_node in worker_labels:
                    selected_key = "worker_label"
                    selected_value = preferred_node
                else:
                    self.logger.warning(
                        "Preferred node '%s' not available, using default '%s:%s'",
                        preferred_node, selected_key, selected_value
                    )

        self.logger.info(
            "Selected node constraint for '%s': %s=%s",
            service_name, selected_key, selected_value
        )
        return selected_key, selected_value

    def _any_task_running(self, docker_service: DockerServiceObject) -> bool:
        """Check if any task in service is running.

        Args:
            docker_service: Docker service object

        Returns:
            True if any task is running, False otherwise
        """
        try:
            for task in docker_service.tasks():
                if task["Status"]["State"] == "running":
                    return True
            return False
        except Exception as exc:
            self.logger.error("Cannot check service tasks: %s", exc)
            return False

    def wait_for_service_ready(self, service: DockerServiceObject, timeout: int = 300) -> bool:
        """Wait for service to become ready.

        Args:
            service: Docker service object
            timeout: Maximum wait time in seconds

        Returns:
            True if service became ready, False if timeout
        """
        self.logger.info("Waiting for service %s to become ready...", service.name)

        for _ in range(timeout):
            if self._any_task_running(service):
                self.logger.info("Service %s is ready", service.name)
                return True

            sleep_time = random.uniform(0.5, 1.5)
            time.sleep(sleep_time)

        self.logger.warning("Service %s did not become ready in %ss", service.name, timeout)
        return False

    def get_swarm_info(self) -> Dict[str, Any]:
        """Get Docker Swarm cluster information.

        Returns:
            Dictionary containing Swarm attributes
        """
        try:
            return self.docker_client.swarm.attrs
        except docker.errors.APIError as api_error:
            self.logger.error("Cannot get Swarm info: %s", api_error)
            raise DockerConnectionError(f"Failed to get Swarm info: {api_error}") from api_error

    def __enter__(self):
        """Context manager entry."""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit - cleanup resources."""
        if hasattr(self.docker_client, "close"):
            self.docker_client.close()
