"""Docker artefact generator for sovereign agent bundles.

Produces production-quality Dockerfiles, docker-compose configurations,
and .dockerignore files from a BundleManifest and a DockerConfig.

Design principles applied
--------------------------
- Multi-stage builds to keep the final image small.
- Non-root user (``agentuser``) for runtime security.
- Health-check support with a configurable command.
- Dependency layer cached separately from application code.
- OCI-standard LABEL annotations for provenance tracing.

Classes
-------
- DockerConfig      Frozen dataclass of Docker build parameters.
- DockerGenerator   Stateless generator; all methods are pure functions.
"""
from __future__ import annotations

import re
import textwrap
from dataclasses import dataclass, field

from agent_sovereign.bundler.manifest import BundleManifest, BundleSovereigntyLevel


# ---------------------------------------------------------------------------
# Path safety helpers
# ---------------------------------------------------------------------------

_SAFE_PATH_RE = re.compile(r'^[a-zA-Z0-9_./\-]+$')


def _validate_path(path: str) -> str:
    """Validate path is safe for Dockerfile COPY instruction.

    Parameters
    ----------
    path:
        The path string to validate.

    Returns
    -------
    str
        The validated path, unchanged.

    Raises
    ------
    ValueError
        If the path contains characters that are unsafe for embedding in
        a Dockerfile COPY instruction.
    """
    if not _SAFE_PATH_RE.match(path):
        raise ValueError(f"Unsafe path for Dockerfile: {path!r}")
    return path


# ---------------------------------------------------------------------------
# Configuration value object
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class DockerConfig:
    """Docker build and runtime configuration.

    Attributes
    ----------
    base_image:
        Base image for the runtime stage (e.g. ``"python:3.11-slim"``).
    python_version:
        Python version string used in the builder stage
        (e.g. ``"3.11"``).
    expose_ports:
        List of ports to expose from the container.  Commonly ``[8080]``
        for HTTP inference endpoints.
    env_vars:
        Environment variables injected into the image via ``ENV``
        directives.  Secrets must *not* be passed here; use runtime
        secret mounts instead.
    healthcheck_cmd:
        Shell command for Docker HEALTHCHECK.  ``None`` disables the
        directive.  Example: ``"curl -f http://localhost:8080/health"``
    labels:
        OCI image label key/value pairs.
    """

    base_image: str = "python:3.11-slim"
    python_version: str = "3.11"
    expose_ports: list[int] = field(default_factory=lambda: [8080])
    env_vars: dict[str, str] = field(default_factory=dict)
    healthcheck_cmd: str | None = None
    labels: dict[str, str] = field(default_factory=dict)


# ---------------------------------------------------------------------------
# Generator
# ---------------------------------------------------------------------------


class DockerGenerator:
    """Generates Docker artefacts from a BundleManifest and DockerConfig.

    All generator methods are stateless: the same inputs always produce
    the same output.  No filesystem I/O is performed; callers write the
    returned strings to disk as appropriate.

    Parameters
    ----------
    default_service_name:
        Fallback service name used in docker-compose if none is supplied
        to :meth:`generate_compose`.
    """

    def __init__(self, default_service_name: str = "agent") -> None:
        self._default_service_name = default_service_name

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def generate_dockerfile(
        self,
        manifest: BundleManifest,
        config: DockerConfig,
    ) -> str:
        """Generate a multi-stage Dockerfile for the bundle.

        The produced Dockerfile has two stages:

        1. **builder** — installs Python dependencies via pip.
        2. **runtime** — copies only the installed packages and agent
           code, drops root privileges, and starts the agent.

        Parameters
        ----------
        manifest:
            The BundleManifest describing the bundle's contents.
        config:
            Docker build and runtime configuration.

        Returns
        -------
        str
            The full Dockerfile content as a string.
        """
        lines: list[str] = []

        # ------ Stage 1: builder ----------------------------------------
        lines.append(f"# syntax=docker/dockerfile:1")
        lines.append(f"# Generated by agent-sovereign bundler")
        lines.append(f"# Bundle: {manifest.bundle_id}")
        lines.append(f"# Sovereignty: {manifest.sovereignty_level.value.upper()}")
        lines.append("")
        lines.append(f"FROM python:{config.python_version}-slim AS builder")
        lines.append("")
        lines.append("WORKDIR /build")
        lines.append("")
        lines.append("# Install build tools")
        lines.append("RUN apt-get update && apt-get install -y --no-install-recommends \\")
        lines.append("        build-essential \\")
        lines.append("    && rm -rf /var/lib/apt/lists/*")
        lines.append("")
        lines.append("# Copy and install Python dependencies first (layer cache)")
        lines.append("COPY requirements.txt ./")
        lines.append("RUN pip install --no-cache-dir --upgrade pip \\")
        lines.append("    && pip install --no-cache-dir --prefix=/install -r requirements.txt")
        lines.append("")

        # ------ Stage 2: runtime ----------------------------------------
        lines.append(f"FROM {config.base_image} AS runtime")
        lines.append("")
        lines.append("WORKDIR /app")
        lines.append("")

        # Labels
        merged_labels = {
            "org.opencontainers.image.title": "agent-sovereign-bundle",
            "org.opencontainers.image.version": manifest.bundle_id,
            "com.aumos.sovereignty": manifest.sovereignty_level.value,
            "com.aumos.target-platform": manifest.target_platform,
            **config.labels,
        }
        if merged_labels:
            label_pairs = " \\\n        ".join(
                f'{k}="{v}"' for k, v in sorted(merged_labels.items())
            )
            lines.append(f"LABEL {label_pairs}")
            lines.append("")

        # Environment variables
        if config.env_vars:
            for key, value in sorted(config.env_vars.items()):
                lines.append(f"ENV {key}={value!r}")
            lines.append("")

        # Sovereignty-level network restrictions for FULL bundles
        if manifest.sovereignty_level == BundleSovereigntyLevel.FULL:
            lines.append(
                "# FULL sovereignty: no external network access at runtime"
            )
            lines.append(
                "ENV AGENT_SOVEREIGN_MODE=full"
            )
            lines.append("")

        # Non-root runtime user
        lines.append("# Create non-root runtime user")
        lines.append("RUN groupadd --gid 10001 agentgroup \\")
        lines.append("    && useradd --uid 10001 --gid agentgroup \\")
        lines.append("              --no-create-home --shell /sbin/nologin agentuser")
        lines.append("")

        # Copy installed packages from builder
        lines.append("# Copy installed packages from builder")
        lines.append(
            "COPY --from=builder /install /usr/local"
        )
        lines.append("")

        # Copy agent code components
        agent_code_paths = [
            c.path for c in manifest.components if c.component_type == "agent_code"
        ]
        config_paths = [
            c.path for c in manifest.components if c.component_type == "config"
        ]
        policy_paths = [
            c.path for c in manifest.components if c.component_type == "policy"
        ]

        lines.append("# Copy application files")
        if agent_code_paths:
            for code_path in agent_code_paths:
                lines.append(f"COPY {_validate_path(code_path)} ./")
        else:
            lines.append("COPY . ./")
        lines.append("")

        if config_paths or policy_paths:
            lines.append("# Copy configuration and policy files")
            for cfg_path in config_paths:
                lines.append(f"COPY {_validate_path(cfg_path)} ./")
            for pol_path in policy_paths:
                lines.append(f"COPY {_validate_path(pol_path)} ./")
            lines.append("")

        # Set ownership
        lines.append("RUN chown -R agentuser:agentgroup /app")
        lines.append("")

        # Switch to non-root user
        lines.append("USER agentuser")
        lines.append("")

        # Expose ports
        for port in config.expose_ports:
            lines.append(f"EXPOSE {port}")
        if config.expose_ports:
            lines.append("")

        # Health check
        if config.healthcheck_cmd:
            lines.append(
                f"HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \\"
            )
            lines.append(f"    CMD {config.healthcheck_cmd}")
            lines.append("")

        # Default entrypoint
        lines.append('ENTRYPOINT ["python", "-m", "agent"]')
        lines.append("")

        return "\n".join(lines)

    def generate_compose(
        self,
        manifest: BundleManifest,
        config: DockerConfig,
        service_name: str | None = None,
    ) -> str:
        """Generate a docker-compose.yml for the bundle.

        Parameters
        ----------
        manifest:
            The BundleManifest describing the bundle's contents.
        config:
            Docker build and runtime configuration.
        service_name:
            Name of the compose service.  Falls back to
            :attr:`default_service_name` if not supplied.

        Returns
        -------
        str
            A docker-compose YAML string.
        """
        name = service_name or self._default_service_name

        lines: list[str] = []
        lines.append("# Generated by agent-sovereign bundler")
        lines.append(f"# Bundle: {manifest.bundle_id}")
        lines.append(f"# Sovereignty: {manifest.sovereignty_level.value.upper()}")
        lines.append("")
        lines.append("version: '3.9'")
        lines.append("")
        lines.append("services:")
        lines.append(f"  {name}:")
        lines.append("    build:")
        lines.append("      context: .")
        lines.append("      dockerfile: Dockerfile")
        lines.append(f"    image: {name}:latest")
        lines.append("    restart: unless-stopped")

        # Port mappings
        if config.expose_ports:
            lines.append("    ports:")
            for port in config.expose_ports:
                lines.append(f"      - \"{port}:{port}\"")

        # Environment variables
        if config.env_vars:
            lines.append("    environment:")
            for key, value in sorted(config.env_vars.items()):
                lines.append(f"      {key}: {value!r}")

        # Network restrictions for FULL sovereignty
        if manifest.sovereignty_level == BundleSovereigntyLevel.FULL:
            lines.append("    # FULL sovereignty: no external network")
            lines.append("    network_mode: none")
        else:
            lines.append("    networks:")
            lines.append("      - agent-net")

        # Health check
        if config.healthcheck_cmd:
            lines.append("    healthcheck:")
            lines.append(f"      test: [\"{config.healthcheck_cmd}\"]")
            lines.append("      interval: 30s")
            lines.append("      timeout: 10s")
            lines.append("      retries: 3")
            lines.append("      start_period: 15s")

        # Volumes for model weights (attach at runtime for large models)
        model_components = [c for c in manifest.components if c.component_type == "model"]
        if model_components:
            lines.append("    volumes:")
            for model_comp in model_components:
                lines.append(
                    f"      - ./models:/app/models  # {model_comp.name}"
                )

        # Labels
        if config.labels:
            lines.append("    labels:")
            for key, value in sorted(config.labels.items()):
                lines.append(f"      {key}: \"{value}\"")

        # Networks block (only for non-FULL bundles)
        if manifest.sovereignty_level != BundleSovereigntyLevel.FULL:
            lines.append("")
            lines.append("networks:")
            lines.append("  agent-net:")
            lines.append("    driver: bridge")

        lines.append("")
        return "\n".join(lines)

    @staticmethod
    def generate_dockerignore() -> str:
        """Generate a .dockerignore file with sensible defaults.

        Excludes version control metadata, Python caches, test artefacts,
        local environment files, and other files that must not enter the
        build context.

        Returns
        -------
        str
            Content for the ``.dockerignore`` file.
        """
        return textwrap.dedent("""\
            # Generated by agent-sovereign bundler

            # Version control
            .git
            .gitignore
            .gitattributes

            # Python caches and build artefacts
            __pycache__/
            *.py[cod]
            *.pyo
            .mypy_cache/
            .ruff_cache/
            .pytest_cache/
            *.egg-info/
            dist/
            build/
            .eggs/

            # Virtual environments
            .venv/
            venv/
            env/

            # Test artefacts
            tests/
            .coverage
            coverage.xml
            htmlcov/

            # Documentation
            docs/
            *.md

            # Local configuration and secrets
            .env
            .env.*
            *.local
            secrets/

            # CI / CD
            .github/
            .gitlab-ci.yml
            Makefile

            # IDE
            .vscode/
            .idea/
            *.swp
            *.swo

            # OS artefacts
            .DS_Store
            Thumbs.db
        """)


__all__ = [
    "DockerConfig",
    "DockerGenerator",
]
