"""``a2a`` CLI: scaffold, validate, build, deploy."""
from __future__ import annotations

import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
from importlib import resources
from pathlib import Path
from typing import Any

import typer
import yaml
from rich.console import Console
from rich.panel import Panel

from . import credentials
from .api_client import ApiError, ControlPlaneClient
from .loader import load_agent_class
from .manifests import INGRESS_HOST_TEMPLATE, NAMESPACE, render_manifests

app = typer.Typer(
    add_completion=False,
    no_args_is_help=True,
    help="Build, package, and deploy A2A agents.",
)
console = Console()

DEFAULT_REGISTRY_HOST = "registry.a2acloud.io"
SDK_PYPI_PACKAGE = "a2a_pack"


# --------------------------------------------------------------------------- #
# helpers                                                                     #
# --------------------------------------------------------------------------- #


def _fail(msg: str, code: int = 1) -> None:
    console.print(f"[bold red]error:[/] {msg}")
    raise typer.Exit(code)


def _read_yaml(path: Path) -> dict[str, Any]:
    if not path.exists():
        _fail(f"missing {path.name} (run `a2a init` first)")
    return yaml.safe_load(path.read_text()) or {}


def _render_template(template: str, /, **vars: Any) -> str:
    text = (
        resources.files("a2a_pack.cli.templates")
        .joinpath(template)
        .read_text(encoding="utf-8")
    )
    # tiny mustache-style substitution; avoids pulling Jinja just for {{ x }}
    for k, v in vars.items():
        text = text.replace("{{ " + k + " }}", str(v))
    return text


def _slug_to_class(slug: str) -> str:
    parts = re.split(r"[-_\s]+", slug.strip())
    return "".join(p[:1].upper() + p[1:].lower() for p in parts if p) or "MyAgent"


def _git_short_sha(project: Path) -> str | None:
    try:
        out = subprocess.run(
            ["git", "-C", str(project), "rev-parse", "--short=12", "HEAD"],
            capture_output=True,
            text=True,
            check=True,
        ).stdout.strip()
        return out or None
    except (subprocess.CalledProcessError, FileNotFoundError):
        return None


def _sdk_source_dir() -> Path:
    """Locate the on-disk a2a_pack source for inclusion in the build context."""
    pkg_dir = Path(__file__).resolve().parents[1]  # .../a2a_pack
    project_root = pkg_dir.parent  # .../apps/a2a
    if not (project_root / "pyproject.toml").exists():
        _fail(f"could not find a2a-pack source root from {pkg_dir}")
    return project_root


def _run(cmd: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
    console.print(f"[dim]$ {' '.join(cmd)}[/]")
    return subprocess.run(cmd, check=True, text=True, **kwargs)


# --------------------------------------------------------------------------- #
# auth                                                                        #
# --------------------------------------------------------------------------- #


def _client(api: str | None = None) -> ControlPlaneClient:
    api_url = credentials.resolve_api_url(api)
    creds = credentials.load()
    token = creds.token if creds is not None else None
    return ControlPlaneClient(api_url, token=token)


@app.command()
def signup(
    email: str = typer.Option(..., "--email", "-e", prompt=True),
    password: str = typer.Option(
        ..., "--password", "-p", prompt=True, hide_input=True, confirmation_prompt=True
    ),
    api: str = typer.Option(credentials.DEFAULT_API_URL, "--api"),
) -> None:
    """Create an account on the control plane and store the JWT locally."""
    try:
        out = ControlPlaneClient(api).signup(email, password)
    except ApiError as exc:
        _fail(str(exc))
    credentials.save(api, out["access_token"], out["user"]["email"])
    console.print(f"[green]signed up[/] as [cyan]{out['user']['email']}[/] @ {api}")


@app.command()
def login(
    email: str = typer.Option(..., "--email", "-e", prompt=True),
    password: str = typer.Option(..., "--password", "-p", prompt=True, hide_input=True),
    api: str = typer.Option(credentials.DEFAULT_API_URL, "--api"),
) -> None:
    """Authenticate with the control plane and cache the JWT."""
    try:
        out = ControlPlaneClient(api).login(email, password)
    except ApiError as exc:
        _fail(str(exc))
    credentials.save(api, out["access_token"], out["user"]["email"])
    console.print(f"[green]logged in[/] as [cyan]{out['user']['email']}[/]")


@app.command()
def logout() -> None:
    """Forget the cached JWT."""
    cleared = credentials.clear()
    console.print("[green]logged out[/]" if cleared else "(not logged in)")


@app.command()
def whoami() -> None:
    """Show the currently logged-in user."""
    creds = credentials.load()
    if creds is None:
        _fail("not logged in (run `a2a login` or `a2a signup`)")
    try:
        me = _client().me()
    except ApiError as exc:
        _fail(str(exc))
    console.print(f"[cyan]{me['email']}[/]  ({creds.api_url})")


@app.command(name="agents")
def list_agents(
    api: str | None = typer.Option(None, "--api"),
) -> None:
    """List agents visible to the current user."""
    try:
        rows = _client(api).list_agents()
    except ApiError as exc:
        _fail(str(exc))
    if not rows:
        console.print("(no agents)")
        return
    for r in rows:
        url = r.get("url") or "-"
        console.print(
            f"  [cyan]{r['name']}[/]  v{r['version']}  [{r['status']}]  {url}"
        )


# --------------------------------------------------------------------------- #
# init                                                                        #
# --------------------------------------------------------------------------- #


@app.command()
def init(
    name: str = typer.Argument(..., help="Agent / project slug, e.g. research-agent"),
    description: str = typer.Option("A new A2A agent", "--description", "-d"),
    target: Path = typer.Option(Path("."), "--target", "-t", help="Parent dir for the new project"),
) -> None:
    """Scaffold a new agent project."""
    project = target / name
    if project.exists():
        _fail(f"{project} already exists")
    project.mkdir(parents=True)
    class_name = _slug_to_class(name)

    files = {
        "agent.py": _render_template(
            "agent.py.tmpl",
            name=name,
            class_name=class_name,
            description=description,
        ),
        "a2a.yaml": _render_template(
            "a2a.yaml.tmpl", name=name, class_name=class_name
        ),
        "requirements.txt": _render_template("requirements.txt.tmpl"),
    }
    for relpath, content in files.items():
        target_path = project / relpath
        target_path.parent.mkdir(parents=True, exist_ok=True)
        target_path.write_text(content)
        console.print(f"  [green]+[/] {project}/{relpath}")

    console.print(
        Panel.fit(
            f"[bold]{name}[/] scaffolded at [cyan]{project}[/]\n\n"
            "next:\n"
            f"  cd {project}\n"
            "  python -m pip install -r requirements.txt\n"
            "  a2a card        # see what your agent declares\n"
            "  a2a deploy      # ship it",
            title="ok",
        )
    )


# --------------------------------------------------------------------------- #
# validate / card / run                                                       #
# --------------------------------------------------------------------------- #


@app.command()
def validate(
    project: Path = typer.Option(Path("."), "--project", "-p"),
) -> None:
    """Load the agent and print its Card schema. Exits non-zero on errors."""
    cfg = _read_yaml(project / "a2a.yaml")
    cls = load_agent_class(cfg["entrypoint"], project_dir=project)
    console.print(f"[green]ok[/] {cls.name} v{cls.version} ({len(cls._skills)} skills)")


@app.command()
def card(
    project: Path = typer.Option(Path("."), "--project", "-p"),
) -> None:
    """Print the Agent Card JSON for the project's agent."""
    cfg = _read_yaml(project / "a2a.yaml")
    cls = load_agent_class(cfg["entrypoint"], project_dir=project)
    console.print_json(cls().card().model_dump_json())


@app.command()
def run(
    entrypoint: str = typer.Option(..., "--entrypoint", "-e", help="module:Class"),
    host: str = typer.Option("0.0.0.0", "--host"),
    port: int = typer.Option(8000, "--port"),
    project: Path = typer.Option(Path("."), "--project", "-p"),
) -> None:
    """Run the agent's HTTP server locally (used inside the container too)."""
    from ..serve import serve

    cls = load_agent_class(entrypoint, project_dir=project)
    serve(cls(), host=host, port=port)


@app.command(name="mcp-url")
def mcp_url(
    name: str | None = typer.Argument(
        None, help="Agent name (defaults to a2a.yaml in --project)"
    ),
    project: Path = typer.Option(Path("."), "--project", "-p"),
    api: str | None = typer.Option(None, "--api"),
) -> None:
    """Print the MCP connect config for a deployed agent.

    Every shipped agent auto-exposes ``POST /mcp`` (Streamable HTTP).
    This command prints the JSON snippet you paste into Claude Code,
    Cursor, or any other MCP client to wire up the agent.
    """
    if name is None:
        cfg = _read_yaml(project / "a2a.yaml")
        name = cfg["name"]
    try:
        rows = _client(api).list_agents()
    except ApiError as exc:
        _fail(str(exc))
    row = next((r for r in rows if r["name"] == name), None)
    if row is None:
        _fail(f"agent {name!r} not found (run `a2a deploy` first)")
    url = row.get("url")
    if not url:
        _fail(f"agent {name!r} has no public URL yet")

    snippet = {
        "mcpServers": {
            name: {
                "type": "http",
                "url": f"{url.rstrip('/')}/mcp",
            }
        }
    }
    console.print_json(json.dumps(snippet))


# --------------------------------------------------------------------------- #
# build / deploy                                                              #
# --------------------------------------------------------------------------- #


def _resolve_image_ref(name: str, version: str, project: Path, registry: str) -> str:
    sha = _git_short_sha(project)
    tag = f"{version}-{sha}" if sha else version
    return f"{registry}/agents/{name}:{tag}"


def _stage_build_context(project: Path, dst: Path) -> None:
    """Copy project files + bundled SDK source into a temp build dir."""
    for item in project.iterdir():
        if item.name in {".git", ".venv", "__pycache__", "_a2a_sdk"}:
            continue
        target = dst / item.name
        if item.is_dir():
            shutil.copytree(item, target, ignore=shutil.ignore_patterns("__pycache__"))
        else:
            shutil.copy2(item, target)
    sdk_src = _sdk_source_dir()
    sdk_dst = dst / "_a2a_sdk"
    shutil.copytree(
        sdk_src,
        sdk_dst,
        ignore=shutil.ignore_patterns(".venv", "dist", "build", "__pycache__", "*.egg-info", ".pytest_cache"),
    )


@app.command()
def build(
    project: Path = typer.Option(Path("."), "--project", "-p"),
    registry: str = typer.Option(DEFAULT_REGISTRY_HOST, "--registry"),
    push: bool = typer.Option(False, "--push", help="Also push the built image"),
) -> None:
    """Build (and optionally push) the container image for the agent."""
    cfg = _read_yaml(project / "a2a.yaml")
    image = _resolve_image_ref(cfg["name"], cfg["version"], project, registry)

    with tempfile.TemporaryDirectory(prefix="a2a-build-") as tmp:
        ctx = Path(tmp)
        _stage_build_context(project, ctx)
        _run(["docker", "build", "-t", image, str(ctx)])

    console.print(f"[green]built[/] [cyan]{image}[/]")
    if push:
        _run(["docker", "push", image])
        console.print(f"[green]pushed[/] [cyan]{image}[/]")


def _make_tarball(project: Path) -> bytes:
    """Tar.gz the user's project directory.

    Excludes platform/dev artifacts so build images stay small. The
    platform stamps in Dockerfile/workflow/manifests on the server side.
    """
    import io
    import tarfile

    excluded_dirs = {
        "__pycache__",
        ".venv",
        ".git",
        ".pytest_cache",
        ".mypy_cache",
        "node_modules",
        "dist",
        "build",
        ".gitea",
        "deploy",
    }
    excluded_files = {"Dockerfile", ".dockerignore"}

    buf = io.BytesIO()
    with tarfile.open(fileobj=buf, mode="w:gz") as tar:
        for root, dirs, files in os.walk(project):
            dirs[:] = [d for d in dirs if d not in excluded_dirs]
            for fname in files:
                if fname in excluded_files or fname.endswith(".pyc"):
                    continue
                fpath = Path(root) / fname
                arcname = fpath.relative_to(project)
                tar.add(fpath, arcname=str(arcname))
    return buf.getvalue()


def _wait_for_url(url: str, timeout: int = 180) -> bool:
    import time

    import httpx

    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        try:
            with httpx.Client(timeout=3.0) as c:
                if c.get(f"{url}/healthz").status_code == 200:
                    return True
        except Exception:  # noqa: BLE001
            pass
        time.sleep(4)
    return False


@app.command()
def deploy(
    project: Path = typer.Option(Path("."), "--project", "-p"),
    public: bool | None = typer.Option(None, "--public/--private"),
    wait: bool = typer.Option(True, "--wait/--no-wait", help="Poll until URL is live"),
    api: str | None = typer.Option(None, "--api", help="Override control plane URL"),
) -> None:
    """Ship the agent.

    Tarballs your source, uploads to the control plane, and prints the
    URL when it's live. No local docker. No git. No knowledge of how the
    platform builds or deploys.
    """
    creds = credentials.load()
    if creds is None:
        _fail("not logged in (run `a2a signup` or `a2a login`)")

    cfg = _read_yaml(project / "a2a.yaml")
    cls = load_agent_class(cfg["entrypoint"], project_dir=project)
    is_public = (
        public if public is not None else bool(cfg.get("expose", {}).get("public", True))
    )
    description = cfg.get("description", cls.description or "")

    console.print("[dim]packaging source...[/]")
    tarball = _make_tarball(project)
    size_kb = len(tarball) / 1024
    console.print(f"[dim]uploading {size_kb:.1f}KB to {credentials.resolve_api_url(api)}...[/]")

    client = _client(api)
    try:
        out = client.from_tarball(
            name=cls.name,
            version=cfg["version"],
            entrypoint=cfg["entrypoint"],
            description=description,
            public=is_public,
            tarball=tarball,
        )
    except ApiError as exc:
        _fail(str(exc))

    summary = {
        "agent": out["name"],
        "version": out["version"],
        "status": out["status"],
        "url": out.get("url"),
    }
    console.print(
        Panel.fit(
            json.dumps(summary, indent=2),
            title="[bold green]shipped[/]",
        )
    )

    url = out.get("url")
    if wait and url:
        console.print(f"[dim]waiting for {url} ...[/]")
        ready = _wait_for_url(url)
        if ready:
            console.print(f"[green]live[/]: {url}")
        else:
            console.print(
                f"[yellow]still building[/]; check `a2a agents` or curl {url} in a bit"
            )


# Used as `python -m a2a_pack.cli.main`
if __name__ == "__main__":  # pragma: no cover
    app()
