"""Image building from container state."""

from __future__ import annotations

import logging
import tempfile
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING, Iterator

import docker
from docker.errors import APIError, ImageNotFound, NotFound

from gaol.build.dockerfile import generate_dockerfile
from gaol.build.exceptions import (
    ContainerNotFoundError,
    ImageBuildError,
    ImageNotFoundError,
    ImageTagError,
)
from gaol.build.models import (
    BuildMethod,
    BuildResult,
    ImageInfo,
    ImageLayer,
)

if TYPE_CHECKING:
    pass

logger = logging.getLogger(__name__)

# Label prefix for gaol-built images
LABEL_PREFIX = "dev.gaol"
BUILD_REPO_PREFIX = "gaol-build"


class ImageBuilder:
    """Build Docker images from container state.

    Supports two methods:
    - commit: Fast, uses docker commit to snapshot container
    - dockerfile: Reproducible, generates and builds Dockerfile
    """

    def __init__(self) -> None:
        """Initialize image builder."""
        self._client: docker.DockerClient | None = None

    @property
    def client(self) -> docker.DockerClient:
        """Get or create Docker client."""
        if self._client is None:
            self._client = docker.from_env()
        return self._client

    def build(
        self,
        container_name: str,
        tag: str = "latest",
        repository: str | None = None,
        method: BuildMethod = BuildMethod.COMMIT,
        labels: dict[str, str] | None = None,
        include_packages: bool = True,
    ) -> BuildResult:
        """Build image from container.

        Args:
            container_name: Source container name
            tag: Image tag
            repository: Repository name (default: gaol-build/{container})
            method: Build method (commit or dockerfile)
            labels: Additional labels to add
            include_packages: Include packages in Dockerfile (dockerfile method only)

        Returns:
            BuildResult with outcome
        """
        result = BuildResult(
            success=False,
            started_at=datetime.now(),
            tags=[],
        )

        if repository is None:
            repository = f"{BUILD_REPO_PREFIX}/{container_name}"

        full_tag = f"{repository}:{tag}"

        try:
            if method == BuildMethod.COMMIT:
                image_id = self._build_with_commit(
                    container_name, repository, tag, labels
                )
            else:
                image_id = self._build_with_dockerfile(
                    container_name,
                    repository,
                    tag,
                    labels,
                    include_packages,
                )

            result.success = True
            result.image_id = image_id
            result.tags = [full_tag]

        except ContainerNotFoundError:
            result.error = f"Container not found: {container_name}"
        except ImageBuildError as e:
            result.error = str(e)
            result.build_log = e.build_log
        except Exception as e:
            result.error = f"Build failed: {e}"
            logger.exception("Image build failed")

        result.finished_at = datetime.now()
        return result

    def _build_with_commit(
        self,
        container_name: str,
        repository: str,
        tag: str,
        labels: dict[str, str] | None,
    ) -> str:
        """Build image using docker commit.

        Fast method that snapshots the container's current state.
        """
        try:
            container = self.client.containers.get(container_name)
        except NotFound:
            raise ContainerNotFoundError(container_name)

        # Build labels
        commit_labels = {
            f"{LABEL_PREFIX}.built-from": container_name,
            f"{LABEL_PREFIX}.build-method": "commit",
            f"{LABEL_PREFIX}.build-time": datetime.now().isoformat(),
        }
        if labels:
            commit_labels.update(labels)

        try:
            image = container.commit(
                repository=repository,
                tag=tag,
                message=f"Built from container {container_name} by gaol",
                conf={"Labels": commit_labels},
            )
            return image.id
        except APIError as e:
            raise ImageBuildError(container_name, f"Docker commit failed: {e}") from e

    def _build_with_dockerfile(
        self,
        container_name: str,
        repository: str,
        tag: str,
        labels: dict[str, str] | None,
        include_packages: bool,
    ) -> str:
        """Build image using generated Dockerfile.

        Slower but more reproducible method.
        """
        # Generate Dockerfile
        dockerfile = generate_dockerfile(
            container_name,
            runtime="docker",
            include_packages=include_packages,
        )

        # Write to temp directory
        with tempfile.TemporaryDirectory() as build_dir:
            dockerfile_path = Path(build_dir) / "Dockerfile"
            dockerfile_path.write_text(dockerfile.render())

            # Build labels
            build_labels = {
                f"{LABEL_PREFIX}.built-from": container_name,
                f"{LABEL_PREFIX}.build-method": "dockerfile",
                f"{LABEL_PREFIX}.build-time": datetime.now().isoformat(),
            }
            if labels:
                build_labels.update(labels)

            try:
                image, logs = self.client.images.build(
                    path=build_dir,
                    dockerfile="Dockerfile",
                    tag=f"{repository}:{tag}",
                    labels=build_labels,
                    rm=True,
                )

                return image.id

            except docker.errors.BuildError as e:
                build_log = [str(log) for log in e.build_log]
                raise ImageBuildError(
                    container_name,
                    f"Dockerfile build failed: {e.msg}",
                    build_log=build_log,
                ) from e
            except APIError as e:
                raise ImageBuildError(
                    container_name, f"Docker build failed: {e}"
                ) from e

    def tag_image(
        self,
        source_ref: str,
        repository: str,
        tag: str = "latest",
    ) -> bool:
        """Add a tag to an existing image.

        Args:
            source_ref: Source image reference (name:tag or ID)
            repository: New repository name
            tag: New tag

        Returns:
            True if successful
        """
        try:
            image = self.client.images.get(source_ref)
        except ImageNotFound:
            raise ImageNotFoundError(source_ref)

        try:
            return image.tag(repository, tag=tag)
        except APIError as e:
            raise ImageTagError(
                source_ref, f"{repository}:{tag}", str(e)
            ) from e

    def get_image_info(self, image_ref: str) -> ImageInfo:
        """Get information about an image.

        Args:
            image_ref: Image reference (name:tag or ID)

        Returns:
            ImageInfo with details
        """
        try:
            image = self.client.images.get(image_ref)
            image.reload()
        except ImageNotFound:
            raise ImageNotFoundError(image_ref)

        attrs = image.attrs

        # Parse creation time
        created_at = None
        if attrs.get("Created"):
            try:
                # Docker uses ISO format
                created_str = attrs["Created"]
                # Handle nanoseconds
                if "." in created_str:
                    created_str = created_str.split(".")[0]
                created_at = datetime.fromisoformat(created_str.replace("Z", ""))
            except Exception:
                pass

        # Get layers from history
        layers = []
        history = image.history()
        for entry in history:
            layer_created = None
            if entry.get("Created"):
                try:
                    layer_created = datetime.fromtimestamp(entry["Created"])
                except Exception:
                    pass

            layers.append(
                ImageLayer(
                    id=entry.get("Id", "")[:12] if entry.get("Id") else "<missing>",
                    created_at=layer_created,
                    created_by=entry.get("CreatedBy", ""),
                    size_bytes=entry.get("Size", 0),
                    comment=entry.get("Comment"),
                )
            )

        return ImageInfo(
            id=image.id,
            tags=image.tags or [],
            size_bytes=attrs.get("Size", 0),
            created_at=created_at,
            layers=layers,
            architecture=attrs.get("Architecture"),
            os=attrs.get("Os", "linux"),
            labels=attrs.get("Config", {}).get("Labels", {}),
        )

    def list_built_images(self) -> list[ImageInfo]:
        """List all images built by gaol.

        Returns:
            List of ImageInfo for gaol-built images
        """
        images = self.client.images.list(
            filters={"label": f"{LABEL_PREFIX}.built-from"}
        )

        result = []
        for image in images:
            try:
                info = self.get_image_info(image.id)
                result.append(info)
            except Exception:
                pass

        return result

    def delete_image(self, image_ref: str, force: bool = False) -> bool:
        """Delete an image.

        Args:
            image_ref: Image reference
            force: Force removal even if in use

        Returns:
            True if deleted
        """
        try:
            self.client.images.remove(image_ref, force=force)
            return True
        except ImageNotFound:
            raise ImageNotFoundError(image_ref)
        except APIError as e:
            logger.error(f"Failed to delete image: {e}")
            return False


def build_image(
    container_name: str,
    tag: str = "latest",
    repository: str | None = None,
    method: str = "commit",
    labels: dict[str, str] | None = None,
    include_packages: bool = True,
) -> BuildResult:
    """Build image from container.

    High-level function for building images.

    Args:
        container_name: Source container name
        tag: Image tag
        repository: Repository name
        method: Build method ("commit" or "dockerfile")
        labels: Additional labels
        include_packages: Include packages (dockerfile method)

    Returns:
        BuildResult with outcome
    """
    build_method = BuildMethod.COMMIT if method == "commit" else BuildMethod.DOCKERFILE
    builder = ImageBuilder()
    return builder.build(
        container_name,
        tag=tag,
        repository=repository,
        method=build_method,
        labels=labels,
        include_packages=include_packages,
    )


def get_image_info(image_ref: str) -> ImageInfo:
    """Get information about an image.

    Args:
        image_ref: Image reference

    Returns:
        ImageInfo with details
    """
    builder = ImageBuilder()
    return builder.get_image_info(image_ref)


def get_image_history(image_ref: str) -> list[ImageLayer]:
    """Get layer history for an image.

    Args:
        image_ref: Image reference

    Returns:
        List of ImageLayer objects
    """
    info = get_image_info(image_ref)
    return info.layers
