"""Agent packaging entrypoint."""

from __future__ import annotations

import importlib.util
import json
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

from boxy_agent.capabilities import CapabilityCatalog
from boxy_agent.compiler.compile import compile_agent
from boxy_agent.compiler.models import CompiledAgent, PackagedAgent


class PackagingError(RuntimeError):
    """Raised when an agent package cannot be built."""


def package_agent(
    project_dir: Path,
    output_dir: Path,
    *,
    capability_catalog: CapabilityCatalog | None = None,
) -> PackagedAgent:
    """Compile and package an agent project into an installable wheel."""
    compiled = compile_agent(
        project_dir=project_dir,
        output_dir=output_dir,
        capability_catalog=capability_catalog,
    )

    destination = output_dir.resolve()
    destination.mkdir(parents=True, exist_ok=True)

    with tempfile.TemporaryDirectory(prefix="boxy-agent-package-") as temp_dir:
        stage_dir = Path(temp_dir) / "stage"
        shutil.copytree(compiled.project_dir, stage_dir)

        root_package = compiled.manifest.entrypoint.module.split(".")[0]
        manifest_module = f"{root_package}.boxy_agent_compiled_manifest"
        _inject_manifest_module(stage_dir, root_package=root_package, compiled=compiled)

        wheel_path = _build_wheel(stage_dir=stage_dir, output_dir=destination)

    return PackagedAgent(
        compiled=compiled,
        wheel_path=wheel_path,
        manifest_module=manifest_module,
    )


def _inject_manifest_module(
    stage_dir: Path,
    *,
    root_package: str,
    compiled: CompiledAgent,
) -> None:
    source_layout = stage_dir / "src" / root_package
    flat_layout = stage_dir / root_package
    # Support both `src/` and flat setuptools layouts produced by scaffolded or custom projects.
    package_dir = source_layout if source_layout.exists() else flat_layout
    if not package_dir.exists():
        raise PackagingError(
            "Could not determine package directory for manifest injection. "
            f"Expected one of: {source_layout} or {flat_layout}"
        )

    manifest_module_path = package_dir / "boxy_agent_compiled_manifest.py"
    manifest_payload = json.dumps(compiled.manifest.to_dict(), indent=2, sort_keys=True)
    # Embedding the manifest in the wheel makes runtime metadata access independent from
    # external compiled artifacts that may not be shipped with the install.
    manifest_module_path.write_text(
        "# Auto-generated by boxy_agent.package_agent\n"
        "from __future__ import annotations\n\n"
        "import json\n\n"
        f"COMPILED_AGENT_MANIFEST = json.loads({manifest_payload!r})\n",
        encoding="utf-8",
    )


def _build_wheel(*, stage_dir: Path, output_dir: Path) -> Path:
    if importlib.util.find_spec("build") is None:
        raise PackagingError(
            "Packaging dependencies are not installed. Install boxy-agent with the "
            "'packaging' extra to use `boxy-agent package`."
        )
    with tempfile.TemporaryDirectory(prefix="boxy-agent-wheel-") as wheel_temp_dir:
        temp_output_dir = Path(wheel_temp_dir)
        # Build from the current environment so packaging does not depend on network access
        # to recreate an isolated build env during each invocation.
        command = [
            sys.executable,
            "-m",
            "build",
            "--wheel",
            "--no-isolation",
            "--outdir",
            str(temp_output_dir),
        ]
        result = subprocess.run(
            command,
            cwd=stage_dir,
            text=True,
            capture_output=True,
            check=False,
        )
        if result.returncode != 0:
            raise PackagingError(
                f"Wheel build failed.\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}"
            )

        built_wheels = sorted(temp_output_dir.glob("*.whl"), key=lambda path: path.stat().st_mtime)
        if not built_wheels:
            raise PackagingError("Wheel build completed but no wheel artifact was produced")

        built_wheel = built_wheels[-1]
        destination = output_dir / built_wheel.name
        if destination.exists():
            destination.unlink()
        shutil.move(str(built_wheel), destination)
        return destination.resolve()
