"""Build orchestration for OpenShift binary builds."""

from __future__ import annotations

import hashlib
import json
import shutil
import subprocess
import sys
import tempfile
import time
from pathlib import Path
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from paude.agents.base import Agent

from paude.backends.openshift.config import OpenShiftConfig
from paude.backends.openshift.exceptions import (
    BuildFailedError,
    OcTimeoutError,
    OpenShiftError,
)
from paude.backends.openshift.oc import OcClient


class BuildOrchestrator:
    """Orchestrates OpenShift binary builds for container images.

    Handles creating BuildConfigs, starting builds, waiting for completion,
    and retrieving image references.
    """

    def __init__(
        self,
        oc: OcClient,
        namespace: str,
        config: OpenShiftConfig,
    ) -> None:
        """Initialize the BuildOrchestrator.

        Args:
            oc: OcClient instance for running oc commands.
            namespace: Kubernetes namespace for builds.
            config: OpenShift configuration with build resources.
        """
        self._oc = oc
        self._namespace = namespace
        self._config = config

    def create_build_config(
        self,
        config_hash: str,
        name_prefix: str = "paude",
    ) -> None:
        """Create a BuildConfig and ImageStream for binary builds.

        If the BuildConfig already exists, this is a no-op.

        Args:
            config_hash: Hash of the configuration for naming.
            name_prefix: Prefix for build config name (default: "paude").
        """
        bc_name = f"{name_prefix}-{config_hash}"

        result = self._oc.run(
            "get",
            "buildconfig",
            bc_name,
            "-n",
            self._namespace,
            check=False,
        )
        if result.returncode == 0:
            print(
                f"BuildConfig/{bc_name} already exists, reusing...",
                file=sys.stderr,
            )
            return

        print(f"Creating BuildConfig/{bc_name}...", file=sys.stderr)

        is_spec: dict[str, Any] = {
            "apiVersion": "image.openshift.io/v1",
            "kind": "ImageStream",
            "metadata": {
                "name": bc_name,
                "namespace": self._namespace,
                "labels": {
                    "app": "paude",
                    "paude.io/config-hash": config_hash,
                },
            },
        }

        bc_spec: dict[str, Any] = {
            "apiVersion": "build.openshift.io/v1",
            "kind": "BuildConfig",
            "metadata": {
                "name": bc_name,
                "namespace": self._namespace,
                "labels": {
                    "app": "paude",
                    "paude.io/config-hash": config_hash,
                },
            },
            "spec": {
                "output": {
                    "to": {
                        "kind": "ImageStreamTag",
                        "name": f"{bc_name}:latest",
                    },
                },
                "source": {
                    "type": "Binary",
                },
                "strategy": {
                    "type": "Docker",
                    "dockerStrategy": {},
                },
                "resources": self._config.build_resources,
            },
        }

        self._oc.run("apply", "-f", "-", input_data=json.dumps(is_spec))
        self._oc.run("apply", "-f", "-", input_data=json.dumps(bc_spec))

    def start_binary_build(
        self,
        config_hash: str,
        context_dir: Path,
        session_name: str | None = None,
        name_prefix: str = "paude",
    ) -> str:
        """Start a binary build and return the build name.

        Args:
            config_hash: Hash of the configuration for naming.
            context_dir: Path to the build context directory.
            session_name: Optional session name to label the build with.
            name_prefix: Prefix for build config name (default: "paude").

        Returns:
            Name of the started build (e.g., "paude-abc123-1").
        """
        bc_name = f"{name_prefix}-{config_hash}"

        print(
            f"Starting build from {context_dir}...",
            file=sys.stderr,
        )

        result = self._oc.run(
            "start-build",
            bc_name,
            f"--from-dir={context_dir}",
            "-n",
            self._namespace,
            timeout=120,
        )

        build_name = result.stdout.strip()
        if build_name.startswith("build.build.openshift.io/"):
            build_name = build_name.split("/")[1]
        elif build_name.startswith("build/"):
            build_name = build_name.split("/")[1]

        build_name = build_name.strip('"').replace(" started", "")
        print(f"Build {build_name} started", file=sys.stderr)

        if session_name:
            self._oc.run(
                "label",
                "build",
                build_name,
                f"paude.io/session-name={session_name}",
                "-n",
                self._namespace,
                check=False,
            )

        return build_name

    def wait_for_build(self, build_name: str, timeout: int = 600) -> None:
        """Wait for a build to complete, streaming logs.

        Args:
            build_name: Name of the build to wait for.
            timeout: Timeout in seconds.

        Raises:
            BuildFailedError: If the build fails.
            OcTimeoutError: If the build times out.
        """
        print(f"Waiting for build {build_name} to complete...", file=sys.stderr)
        print("--- Build Logs ---", file=sys.stderr)

        log_proc = subprocess.Popen(
            ["oc", "logs", "-f", f"build/{build_name}", "-n", self._namespace]
            + (["--context", self._config.context] if self._config.context else []),
            stdout=sys.stderr,
            stderr=sys.stderr,
        )

        start_time = time.time()
        while time.time() - start_time < timeout:
            result = self._oc.run(
                "get",
                "build",
                build_name,
                "-n",
                self._namespace,
                "-o",
                "jsonpath={.status.phase}",
                check=False,
            )
            if result.returncode != 0:
                time.sleep(2)
                continue

            phase = result.stdout.strip()

            if phase == "Complete":
                log_proc.terminate()
                print("--- End Build Logs ---", file=sys.stderr)
                print(f"Build {build_name} completed successfully.", file=sys.stderr)
                return

            if phase in ("Failed", "Error", "Cancelled"):
                log_proc.terminate()
                print("--- End Build Logs ---", file=sys.stderr)

                reason_result = self._oc.run(
                    "get",
                    "build",
                    build_name,
                    "-n",
                    self._namespace,
                    "-o",
                    "jsonpath={.status.message}",
                    check=False,
                )
                reason = (
                    reason_result.stdout.strip()
                    if reason_result.returncode == 0
                    else phase
                )

                logs_result = self._oc.run(
                    "logs",
                    f"build/{build_name}",
                    "-n",
                    self._namespace,
                    "--tail=50",
                    check=False,
                )
                logs = logs_result.stdout if logs_result.returncode == 0 else None

                raise BuildFailedError(build_name, reason, logs)

            time.sleep(5)

        log_proc.terminate()
        raise OcTimeoutError(
            f"Build {build_name} did not complete within {timeout} seconds"
        )

    def get_imagestream_reference(
        self,
        config_hash: str,
        name_prefix: str = "paude",
    ) -> str:
        """Get the internal image reference from an ImageStream.

        Args:
            config_hash: Hash of the configuration for naming.
            name_prefix: Prefix for imagestream name (default: "paude").

        Returns:
            Internal image reference for pod image pulls.
        """
        is_name = f"{name_prefix}-{config_hash}"

        result = self._oc.run(
            "get",
            "imagestream",
            is_name,
            "-n",
            self._namespace,
            "-o",
            "jsonpath={.status.dockerImageRepository}",
        )
        repo = result.stdout.strip()
        if not repo:
            repo = (
                f"image-registry.openshift-image-registry.svc:5000/"
                f"{self._namespace}/{is_name}"
            )

        return f"{repo}:latest"

    def ensure_image_via_build(
        self,
        config: Any,
        workspace: Path,
        script_dir: Path | None = None,
        force_rebuild: bool = False,
        session_name: str | None = None,
        agent: Agent | None = None,
    ) -> str:
        """Ensure an image is available via OpenShift binary build.

        Args:
            config: PaudeConfig (or None for default image).
            workspace: Workspace directory.
            script_dir: Path to paude script directory (for dev mode).
            force_rebuild: Force rebuild even if image exists.
            session_name: Optional session name to label the build with.
            agent: Agent instance for Dockerfile generation.

        Returns:
            Internal image reference for pod image pulls.
        """
        from paude.container.image import prepare_build_context

        if config is None:
            from paude.config.models import PaudeConfig

            config = PaudeConfig(config_file=None)

        build_ctx = prepare_build_context(
            config,
            workspace=workspace,
            script_dir=script_dir,
            platform="linux/amd64",
            for_remote_build=True,
            agent=agent,
        )

        try:
            config_hash = build_ctx.config_hash
            is_name = f"paude-{config_hash}"

            if not force_rebuild:
                result = self._oc.run(
                    "get",
                    "imagestreamtag",
                    f"{is_name}:latest",
                    "-n",
                    self._namespace,
                    check=False,
                )
                if result.returncode == 0:
                    print(
                        f"Image {is_name}:latest already exists, reusing...",
                        file=sys.stderr,
                    )
                    return self.get_imagestream_reference(config_hash)

            self.create_build_config(config_hash)

            build_name = self.start_binary_build(
                config_hash,
                build_ctx.context_dir,
                session_name=session_name,
            )

            self.wait_for_build(build_name)

            return self.get_imagestream_reference(config_hash)

        finally:
            shutil.rmtree(build_ctx.context_dir, ignore_errors=True)

    def ensure_proxy_image_via_build(
        self,
        script_dir: Path,
        force_rebuild: bool = False,
        session_name: str | None = None,
    ) -> str:
        """Ensure the proxy image is available via OpenShift binary build.

        Args:
            script_dir: Path to paude script directory containing containers/proxy/.
            force_rebuild: Force rebuild even if image exists.
            session_name: Optional session name to label the build with.

        Returns:
            Internal image reference for proxy pod image pulls.
        """
        name_prefix = "paude-proxy"

        proxy_dir = script_dir / "containers" / "proxy"
        if not proxy_dir.is_dir():
            raise OpenShiftError(f"Proxy container directory not found: {proxy_dir}")

        dockerfile_path = proxy_dir / "Dockerfile"
        if not dockerfile_path.exists():
            raise OpenShiftError(f"Proxy Dockerfile not found: {dockerfile_path}")

        proxy_files = sorted(f for f in proxy_dir.iterdir() if f.is_file())
        hash_content = ""
        for filepath in proxy_files:
            hash_content += filepath.read_text()
        config_hash = hashlib.sha256(hash_content.encode()).hexdigest()[:12]
        is_name = f"{name_prefix}-{config_hash}"

        if not force_rebuild:
            result = self._oc.run(
                "get",
                "imagestreamtag",
                f"{is_name}:latest",
                "-n",
                self._namespace,
                check=False,
            )
            if result.returncode == 0:
                print(
                    f"Proxy image {is_name}:latest already exists, reusing...",
                    file=sys.stderr,
                )
                return self.get_imagestream_reference(config_hash, name_prefix)

        context_dir = Path(tempfile.mkdtemp(prefix="paude-proxy-build-"))
        try:
            for src in proxy_files:
                shutil.copy2(src, context_dir / src.name)

            self.create_build_config(config_hash, name_prefix)

            build_name = self.start_binary_build(
                config_hash,
                context_dir,
                session_name=session_name,
                name_prefix=name_prefix,
            )

            self.wait_for_build(build_name)

            return self.get_imagestream_reference(config_hash, name_prefix)

        finally:
            shutil.rmtree(context_dir, ignore_errors=True)

    def delete_session_builds(self, session_name: str) -> None:
        """Delete builds associated with a session.

        Args:
            session_name: Session name to identify builds to delete.
        """
        self._oc.run(
            "delete",
            "build",
            "-n",
            self._namespace,
            "-l",
            f"paude.io/session-name={session_name}",
            check=False,
        )
