"""Docker image builder module for BlackAnt SDK."""

import docker
import os
import shutil
import tempfile
from pathlib import Path
from typing import Optional, Dict, Any, Iterator
from datetime import datetime

from blackant.utils.logging import get_logger
from blackant.exceptions import BlackAntDockerError
from blackant.config.docker_config import get_docker_config


class DockerImageBuilder:
    """Remote Docker image builder class for BlackAnt SDK.

    Implements remote Docker build using BlackAnt infrastructure as per
    the architecture specification. Supports both nginx proxy (recommended)
    and direct TLS connection to remote Docker daemon.
    """

    def __init__(self, auth=None, registry_url: str = None):
        """Initialize remote Docker builder.

        Args:
            auth: BlackAntAuth instance for JWT token authentication
            registry_url: Docker registry URL (if None, loaded from config)
        """
        self.logger = get_logger("docker-builder")
        self.config = get_docker_config()
        self.auth = auth
        self.registry_url = registry_url or self.config.registry.url

        # Initialize remote Docker client
        self._init_remote_docker_client()

    def _init_remote_docker_client(self):
        """Initialize Docker client with remote daemon connection.

        Supports two modes:
        1. Nginx proxy with JWT token authentication (recommended)
        2. Direct TLS connection to Docker daemon

        Raises:
            BlackAntDockerError: If remote Docker connection fails
        """
        base_url = self.config.daemon.base_url

        try:
            # Mode 1: Nginx proxy with JWT token authentication
            if self.config.daemon.use_nginx_proxy:
                # IMPORTANT: Create APIClient with explicit version to bypass auto-detection
                # Auto-detection calls version() during __init__() which requires JWT auth
                from docker import APIClient

                # Get JWT token if available
                token = None
                if self.auth:
                    token = self.auth.get_token()
                    if not token:
                        self.logger.warning("No JWT token available for Docker daemon authentication")

                # Check if we should disable SSL verification (for dev environments)
                should_disable_ssl_verify = (
                    "dev.blackant.app" in base_url or
                    "localhost" in base_url or
                    "127.0.0.1" in base_url
                )

                # Disable SSL warnings for dev environments
                if should_disable_ssl_verify:
                    import urllib3
                    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
                    self.logger.debug(f"SSL verification disabled for dev environment: {base_url}")

                # Create TLS config for HTTPS connections in dev environment
                tls_config = None
                if "https://" in base_url and should_disable_ssl_verify:
                    from docker import tls
                    tls_config = tls.TLSConfig(verify=False)
                    self.logger.debug("TLS config created with SSL verification disabled")

                # Create APIClient with explicit API version (bypasses auto-detection)
                api_client = APIClient(
                    base_url=base_url,
                    timeout=self.config.daemon.timeout,
                    version='1.47',  # Explicit version prevents auth-required version() call during init
                    tls=tls_config  # SSL verification disabled for dev environments
                )

                # Add JWT token for subsequent API calls
                if token:
                    api_client.headers.update({
                        'Authorization': f'Bearer {token}'
                    })
                    self.logger.debug("JWT token injected into API client headers")

                # Wrap APIClient in a DockerClient for high-level operations
                # Use from_env() as dummy then replace api attribute
                self.docker_client = object.__new__(docker.DockerClient)
                self.docker_client.api = api_client

                # Initialize collections manually
                from docker.models.containers import ContainerCollection
                from docker.models.images import ImageCollection
                from docker.models.networks import NetworkCollection
                from docker.models.volumes import VolumeCollection
                from docker.models.services import ServiceCollection

                object.__setattr__(self.docker_client, '_containers', ContainerCollection(client=self.docker_client))
                object.__setattr__(self.docker_client, '_images', ImageCollection(client=self.docker_client))
                object.__setattr__(self.docker_client, '_networks', NetworkCollection(client=self.docker_client))
                object.__setattr__(self.docker_client, '_volumes', VolumeCollection(client=self.docker_client))
                object.__setattr__(self.docker_client, '_services', ServiceCollection(client=self.docker_client))

                if token:
                    self.logger.info(f"Remote Docker client initialized via nginx proxy with JWT: {base_url}")
                else:
                    self.logger.info(f"Remote Docker client initialized via nginx proxy (no auth): {base_url}")

            # Mode 2: Direct TLS connection
            else:
                tls_config = self.config.daemon.get_tls_config()

                self.docker_client = docker.DockerClient(
                    base_url=base_url,
                    tls=tls_config,
                    timeout=self.config.daemon.timeout
                )

                self.logger.info(f"Remote Docker client initialized with TLS: {base_url}")

            # Test connection to remote Docker daemon
            try:
                version_info = self.docker_client.version()
                self.logger.info(f"Connected to remote Docker daemon version: {version_info.get('Version', 'unknown')}")
                self.logger.debug(f"Docker API version: {version_info.get('ApiVersion', 'unknown')}")
            except Exception as ping_error:
                self.logger.warning(f"Could not verify remote Docker connection: {ping_error}")
                self.logger.warning("Continuing anyway - build operations might still work")

        except Exception as e:
            self.logger.error(f"Failed to initialize remote Docker client: {e}")
            raise BlackAntDockerError(f"Remote Docker client initialization failed: {e}")

    def build_service(self,
                     service_name: str,
                     impl_path: str = "src/calculation/impl",
                     dockerfile_path: str = "Dockerfile",
                     tag: Optional[str] = None,
                     push_to_registry: bool = True) -> Dict[str, Any]:
        """Build and push service Docker image on REMOTE Docker daemon.

        The build context is automatically uploaded to the remote Docker daemon
        by docker-py SDK as a tar.gz archive. The build executes on BlackAnt
        infrastructure, ensuring the source code never leaves the platform.

        Args:
            service_name: Service name
            impl_path: Implementation folder path (will be uploaded)
            dockerfile_path: Dockerfile path (will be uploaded)
            tag: Image tag (default: timestamp)
            push_to_registry: Push to registry

        Returns:
            dict: Build result information
                {
                    "image_name": "env.blackant.app/systemdevelopers/service_name",
                    "tag": "v1.0.0-20250105-123456",
                    "image_id": "sha256:...",
                    "size": 125000000,
                    "pushed": True,
                    "registry_url": "env.blackant.app",
                    "build_location": "remote",
                    "remote_daemon": "http://localhost"
                }

        Raises:
            BlackAntDockerError: Build or push error
        """

        # 1. Generate tag if not provided
        if not tag:
            timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
            tag = f"v1.0.0-{timestamp}"

        # 2. Build full image name using config
        full_image = self.config.get_full_image_name(service_name, tag)

        self.logger.info(f"Building image on REMOTE Docker daemon: {full_image}")
        self.logger.info(f"Remote daemon: {self.config.daemon.base_url}")

        # 3. Input validation
        self._validate_build_inputs(service_name, impl_path, dockerfile_path)

        try:
            # 4. Prepare build context
            build_context = self._prepare_build_context(impl_path, dockerfile_path)

            # 5. Build Docker image
            image, build_logs = self._build_image(full_image, build_context, service_name)

            # 6. Push to registry
            pushed = False
            if push_to_registry:
                pushed = self._push_to_registry(full_image)

            # 7. Cleanup local build context
            try:
                shutil.rmtree(build_context, ignore_errors=True)
                self.logger.debug("Local build context cleaned up")
            except Exception as cleanup_error:
                self.logger.warning(f"Failed to cleanup build context: {cleanup_error}")

            # 8. Assemble result
            result = {
                "image_name": f"{self.registry_url}/{self.config.registry.namespace}/{service_name}",
                "tag": tag,
                "full_image": full_image,
                "image_id": image.id,
                "size": image.attrs.get('Size', 0),
                "pushed": pushed,
                "registry_url": self.registry_url,
                "created": image.attrs.get('Created'),
                "labels": image.labels or {},
                "build_location": "remote",  # Indicates remote build
                "remote_daemon": self.config.daemon.base_url,  # Remote daemon URL
                "use_nginx_proxy": self.config.daemon.use_nginx_proxy  # Connection mode
            }

            self.logger.info(f"Remote build completed successfully: {service_name}")
            return result

        except docker.errors.BuildError as e:
            error_msg = f"Docker build failed for {service_name}: {e}"
            self.logger.error(error_msg)
            raise BlackAntDockerError(error_msg)
        except Exception as e:
            error_msg = f"Unexpected error during build of {service_name}: {e}"
            self.logger.error(error_msg)
            raise BlackAntDockerError(error_msg)

    def _validate_build_inputs(self, service_name: str, impl_path: str, dockerfile_path: str) -> None:
        """Validate build input parameters.

        Args:
            service_name: Service name
            impl_path: Implementation path
            dockerfile_path: Dockerfile path

        Raises:
            BlackAntDockerError: Validation error
        """
        if not service_name or not service_name.strip():
            raise BlackAntDockerError("Service name cannot be empty")

        if not service_name.replace('-', '').replace('_', '').isalnum():
            raise BlackAntDockerError("Service name must contain only alphanumeric characters, hyphens, and underscores")

        impl_path_obj = Path(impl_path)
        if not impl_path_obj.exists():
            raise BlackAntDockerError(f"Implementation path does not exist: {impl_path}")

        if not impl_path_obj.is_dir():
            raise BlackAntDockerError(f"Implementation path is not a directory: {impl_path}")

        dockerfile_path_obj = Path(dockerfile_path)
        if not dockerfile_path_obj.exists():
            raise BlackAntDockerError(f"Dockerfile does not exist: {dockerfile_path}")

        if not dockerfile_path_obj.is_file():
            raise BlackAntDockerError(f"Dockerfile path is not a file: {dockerfile_path}")

    def _prepare_build_context(self, impl_path: str, dockerfile_path: str) -> str:
        """Prepare build context.

        Args:
            impl_path: Implementation folder
            dockerfile_path: Dockerfile path

        Returns:
            str: Temporary build context path

        Raises:
            BlackAntDockerError: Context preparation error
        """
        try:
            # Create temporary build context
            temp_context = tempfile.mkdtemp(prefix="blackant_build_")
            context_path = Path(temp_context)

            self.logger.debug(f"Created build context: {context_path}")

            # Determine project root (where Dockerfile is located)
            dockerfile_src = Path(dockerfile_path).resolve()
            project_root = dockerfile_src.parent

            # Copy Dockerfile
            dockerfile_dst = context_path / "Dockerfile"
            shutil.copy2(dockerfile_src, dockerfile_dst)

            # Detect build mode: full project or simple build
            src_path = project_root / "src"
            has_src_directory = src_path.exists() and src_path.is_dir()

            if has_src_directory:
                # FULL PROJECT MODE (e.g., test_full_state_machine_e2e.py)
                # Copy entire src/ directory - contains everything including impl
                shutil.copytree(src_path, context_path / "src", dirs_exist_ok=True)
                self.logger.debug("Full project mode: Copied src/ directory")

                # Copy project-level files
                requirements_files = ["requirements.txt", "requirements.debug.txt"]
                for req_file in requirements_files:
                    req_src = project_root / req_file
                    if req_src.exists():
                        shutil.copy2(req_src, context_path / req_file)
                        self.logger.debug(f"Copied {req_file}")

                entrypoint_src = project_root / "docker-entrypoint.sh"
                if entrypoint_src.exists():
                    shutil.copy2(entrypoint_src, context_path / "docker-entrypoint.sh")
                    self.logger.debug("Copied docker-entrypoint.sh")

            else:
                # SIMPLE BUILD MODE (e.g., test_remote_docker_build.py)
                # Copy only impl/ directory - minimal build context
                impl_src = Path(impl_path).resolve()
                impl_dst = context_path / "impl"
                if impl_src.exists() and impl_src.is_dir():
                    shutil.copytree(impl_src, impl_dst, dirs_exist_ok=True)
                    self.logger.debug(f"Simple build mode: Copied impl/ from {impl_src}")
                else:
                    raise BlackAntDockerError(f"Implementation path not found: {impl_path}")

            # Create .dockerignore file if it doesn't exist
            dockerignore_path = context_path / ".dockerignore"
            if not dockerignore_path.exists():
                dockerignore_content = """
__pycache__/
*.pyc
*.pyo
*.pyd
.git/
.gitignore
*.md
.pytest_cache/
.coverage
"""
                dockerignore_path.write_text(dockerignore_content.strip())

            self.logger.debug(f"Build context prepared with {len(list(context_path.rglob('*')))} files")
            return str(context_path)

        except Exception as e:
            error_msg = f"Failed to prepare build context: {e}"
            self.logger.error(error_msg)
            raise BlackAntDockerError(error_msg)

    def _build_image(self, full_image: str, build_context: str, service_name: str) -> tuple:
        """Execute Docker image build on REMOTE daemon.

        The docker-py SDK automatically handles:
        1. Creating tar.gz archive from build_context path
        2. Uploading to remote Docker daemon via configured connection
        3. Executing build on remote infrastructure
        4. Streaming build logs back to client

        Args:
            full_image: Full image name with tag
            build_context: LOCAL build context path (will be uploaded)
            service_name: Service name

        Returns:
            tuple: (image object, build logs)

        Raises:
            docker.errors.BuildError: Build error
        """
        self.logger.info(f"Starting REMOTE Docker build for {service_name}")
        self.logger.info(f"Uploading build context to remote daemon...")

        # Build arguments using config
        build_args = self.config.get_build_args(service_name, {
            "BUILD_DATE": datetime.now().isoformat(),
            "CALCULATION_NAME": service_name  # Required by Dockerfile
        })

        # Build labels using config
        labels = self.config.get_build_labels(service_name)

        # Execute REMOTE Docker build
        # The docker-py SDK automatically:
        # - Creates tar.gz archive from build_context
        # - Uploads to remote Docker daemon
        # - Executes build remotely
        # - Streams logs back
        image, build_logs = self.docker_client.images.build(
            path=build_context,  # LOCAL path - SDK uploads it automatically!
            tag=full_image,
            rm=self.config.build.rm,
            forcerm=self.config.build.forcerm,
            pull=self.config.build.pull,
            buildargs=build_args,
            labels=labels,
            timeout=self.config.build.timeout,
            platform="linux/amd64"  # Ensure consistent platform
        )

        # Process build logs
        for log in build_logs:
            if 'stream' in log:
                log_line = log['stream'].strip()
                if log_line:
                    self.logger.debug(f"Remote build: {log_line}")
            elif 'error' in log:
                self.logger.error(f"Remote build error: {log['error']}")

        self.logger.info(f"Remote image built successfully: {image.id[:12]}")
        return image, build_logs

    def _push_to_registry(self, image_name: str) -> bool:
        """Push image to Docker registry.

        If SDK Services is configured, delegates push to sdk-services backend.
        Otherwise, pushes directly using Docker client with local credentials.

        Args:
            image_name: Full image name with tag

        Returns:
            bool: Successful push
        """
        # Check if SDK Services should handle the push
        if self.config.sdk_services.use_for_registry and self.config.sdk_services.url:
            return self._push_via_sdk_services(image_name)

        # Direct push via Docker client
        return self._push_direct(image_name)

    def _push_via_sdk_services(self, image_name: str) -> bool:
        """Push image via SDK Services endpoint.

        Delegates push to sdk-services backend, keeping registry credentials server-side.

        Args:
            image_name: Full image name with tag

        Returns:
            bool: Successful push
        """
        try:
            import requests

            push_url = self.config.sdk_services.registry_push_url
            self.logger.info(f"Pushing image via SDK Services: {push_url}")

            # Get auth token if available
            headers = {"Content-Type": "application/json"}
            if self.auth:
                token = self.auth.get_token()
                if token:
                    headers["Authorization"] = f"Bearer {token}"

            # Call SDK Services endpoint
            response = requests.post(
                push_url,
                json={"image_name": image_name},
                headers=headers,
                timeout=self.config.sdk_services.timeout,
                verify=False  # For dev environments with self-signed certs
            )

            if response.status_code == 200:
                result = response.json()
                if result.get("success"):
                    self.logger.info(f"Image pushed via SDK Services: {image_name}")
                    return True
                else:
                    error_msg = result.get("message", "Unknown error")
                    self.logger.error(f"SDK Services push failed: {error_msg}")
                    return False
            else:
                self.logger.error(f"SDK Services push failed with status {response.status_code}")
                return False

        except Exception as e:
            self.logger.error(f"SDK Services push error: {e}")
            return False

    def _push_direct(self, image_name: str) -> bool:
        """Push image directly via Docker client.

        Uses local registry credentials from configuration.

        Args:
            image_name: Full image name with tag

        Returns:
            bool: Successful push
        """
        try:
            self.logger.info(f"Pushing image to registry: {image_name}")

            # Registry login if credentials available
            credentials = self.config.get_registry_credentials()
            if credentials:
                self.logger.debug(f"Logging in to registry: {self.registry_url}")
                self.docker_client.login(**credentials)

            # Execute push
            push_response = self.docker_client.images.push(
                image_name,
                stream=True,
                decode=True
            )

            # Process push logs
            for line in push_response:
                if 'status' in line:
                    status = line['status']
                    if 'id' in line:
                        self.logger.debug(f"Push {line['id']}: {status}")
                    else:
                        self.logger.debug(f"Push: {status}")

                if 'error' in line:
                    error_msg = f"Push error: {line['error']}"
                    self.logger.error(error_msg)
                    raise BlackAntDockerError(error_msg)

                if 'errorDetail' in line:
                    error_detail = line['errorDetail'].get('message', 'Unknown error')
                    error_msg = f"Push error detail: {error_detail}"
                    self.logger.error(error_msg)
                    raise BlackAntDockerError(error_msg)

            self.logger.info(f"Image pushed successfully: {image_name}")
            return True

        except docker.errors.APIError as e:
            error_msg = f"Docker API error during push: {e}"
            self.logger.error(error_msg)
            return False
        except Exception as e:
            error_msg = f"Unexpected error during push: {e}"
            self.logger.error(error_msg)
            return False

    def list_local_images(self, service_prefix: str = None) -> list:
        """List local Docker images.

        Args:
            service_prefix: Service name prefix for filtering

        Returns:
            list: List of image information
        """
        try:
            images = self.docker_client.images.list()
            result = []

            for image in images:
                for tag in image.tags:
                    if service_prefix and service_prefix not in tag:
                        continue

                    result.append({
                        "id": image.id,
                        "tag": tag,
                        "created": image.attrs.get('Created'),
                        "size": image.attrs.get('Size', 0),
                        "labels": image.labels or {}
                    })

            return result

        except Exception as e:
            self.logger.error(f"Failed to list local images: {e}")
            return []

    def remove_image(self, image_name: str, force: bool = False) -> bool:
        """Remove Docker image.

        Args:
            image_name: Image name or ID
            force: Force removal

        Returns:
            bool: Successful removal
        """
        try:
            self.docker_client.images.remove(image_name, force=force)
            self.logger.info(f"Image removed: {image_name}")
            return True

        except docker.errors.ImageNotFound:
            self.logger.warning(f"Image not found: {image_name}")
            return False
        except Exception as e:
            self.logger.error(f"Failed to remove image {image_name}: {e}")
            return False

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

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit - cleanup resources."""
        try:
            if hasattr(self, 'docker_client'):
                self.docker_client.close()
        except Exception as e:
            self.logger.warning(f"Error closing Docker client: {e}")