"""
Module: registry_command
Description: Pack registry operations (publish, install, list, run)

Implements:
    - docs/cli/reference.md#huitzo-publish
    - docs/cli/reference.md#huitzo-list
    - docs/cli/reference.md#huitzo-run
"""

import importlib.metadata
import json
import subprocess
import sys
from pathlib import Path
from typing import Any

import httpx
import typer
import yaml
from rich.console import Console
from rich.syntax import Syntax
from rich.table import Table

from huitzo_cli.auth import AuthClient
from huitzo_cli.commands.build import _do_build
from huitzo_cli.config import ConfigManager
from huitzo_cli.http_utils import parse_error

console = Console()


def _get_auth() -> tuple[AuthClient, str, str]:
    """Get auth client, token, and API URL. Raises Exit if not authenticated."""
    config = ConfigManager()
    auth = AuthClient(config)
    token = auth.get_token()
    if not token:
        console.print("[red]Not authenticated. Run: huitzo login[/red]")
        raise typer.Exit(1)
    api_url = auth.get_api_url()
    return auth, token, api_url


def _load_pack_manifest() -> dict[str, Any]:
    """Load huitzo.yaml from current directory."""
    manifest_path = Path.cwd() / "huitzo.yaml"
    if not manifest_path.exists():
        console.print("[red]No huitzo.yaml found. Are you in a pack directory?[/red]")
        raise typer.Exit(1)
    with open(manifest_path) as f:
        data: dict[str, Any] = yaml.safe_load(f)
        return data


def publish() -> None:
    """Publish pack to Huitzo backend.

    Builds a wheel (if needed), creates the pack on the backend,
    and uploads the wheel as a new version.
    """
    manifest = _load_pack_manifest()
    auth, token, api_url = _get_auth()

    pack = manifest.get("pack", {})
    pack_name = pack.get("name", "")
    namespace = pack.get("namespace", pack_name)
    visibility = pack.get("visibility", "private")
    version = pack.get("version", "0.0.0")
    headers = {"Authorization": f"Bearer {token}"}

    # Find or build wheel
    dist_dir = Path.cwd() / "dist"
    wheels = list(dist_dir.glob("*.whl")) if dist_dir.exists() else []

    if not wheels:
        _do_build(format_type="wheel")
        wheels = list(dist_dir.glob("*.whl")) if dist_dir.exists() else []

    if not wheels:
        console.print("[red]No wheel found in dist/. Run: huitzo pack build[/red]")
        raise typer.Exit(1)

    wheel_path = sorted(wheels, key=lambda p: p.stat().st_mtime)[-1]

    # Create pack (409 = already exists, that's OK)
    # Send scope WITHOUT @ prefix — the backend adds it (packs.py:91)
    scope = namespace
    console.print(f"[bold]Registering pack @{scope}/{pack_name}...[/bold]")
    try:
        resp = httpx.post(
            f"{api_url}/api/v1/packs",
            json={"scope": scope, "name": pack_name, "visibility": visibility},
            headers=headers,
            timeout=30,
        )
    except httpx.HTTPError as e:
        console.print(f"[red]Could not connect to backend at {api_url}: {e}[/red]")
        raise typer.Exit(1)

    pack_id = None
    if resp.status_code == 201:
        console.print("[green]Pack created[/green]")
        pack_id = resp.json().get("data", {}).get("id")
    elif resp.status_code == 409:
        console.print("Pack already registered")
    else:
        console.print(f"[red]Failed to register pack: {parse_error(resp)}[/red]")
        raise typer.Exit(1)

    # If 409 (already exists), look up the pack_id by iterating owned packs.
    # Note: the backend list endpoint does not support query-param filtering and
    # excludes unlisted packs, so this fallback only works for non-unlisted packs.
    if not pack_id:
        # API returns scope without @ prefix; compare directly
        try:
            resp = httpx.get(
                f"{api_url}/api/v1/packs",
                headers=headers,
                timeout=30,
            )
        except httpx.HTTPError as e:
            console.print(f"[red]Could not connect to backend at {api_url}: {e}[/red]")
            raise typer.Exit(1)

        if resp.status_code != 200:
            console.print("[red]Failed to list packs[/red]")
            raise typer.Exit(1)

        try:
            packs = resp.json().get("data", [])
        except (ValueError, json.JSONDecodeError):
            console.print("[red]Unexpected response from server[/red]")
            raise typer.Exit(1)

        for p in packs:
            if p["scope"] == scope and p["name"] == pack_name:
                pack_id = p["id"]
                break

    if not pack_id:
        console.print(
            "[red]Could not find pack after creation. "
            "If the pack has unlisted visibility, the list API cannot resolve it.[/red]"
        )
        raise typer.Exit(1)

    # Upload wheel
    console.print(f"[bold]Uploading {wheel_path.name} (v{version})...[/bold]")
    try:
        with open(wheel_path, "rb") as f:
            resp = httpx.post(
                f"{api_url}/api/v1/packs/{pack_id}/versions",
                files={"wheel": (wheel_path.name, f, "application/octet-stream")},
                data={"version": version},
                headers=headers,
                timeout=120,
            )
    except httpx.HTTPError as e:
        console.print(f"[red]Could not connect to backend at {api_url}: {e}[/red]")
        raise typer.Exit(1)

    if resp.status_code != 201:
        console.print(f"[red]Upload failed: {parse_error(resp)}[/red]")
        raise typer.Exit(1)

    data = resp.json().get("data", {})
    commands = data.get("commands", [])

    console.print(f"[green]Published v{version} successfully[/green]")
    if commands:
        console.print("[bold]Registered commands:[/bold]")
        for cmd in commands:
            console.print(f"  {cmd['namespace']}")


def install(
    package: str = typer.Argument(..., help="Package name or path to .whl file"),
) -> None:
    """Install a pack from registry or local file.

    Args:
        package: Package name (from registry) or path to .whl file
    """
    # Check if it's a local file
    if package.endswith(".whl"):
        path = Path(package)
        if not path.exists():
            console.print(f"[red]File not found: {package}[/red]")
            raise typer.Exit(1)

        # Validate path does not escape working directory (path traversal)
        resolved = path.resolve()
        cwd = Path.cwd().resolve()
        if not str(resolved).startswith(str(cwd)):
            console.print(
                f"[yellow]Warning: Path resolves outside working directory: {resolved}[/yellow]"
            )

        console.print(f"[bold]Installing from: {package}[/bold]")
        try:
            result = subprocess.run(
                [sys.executable, "-m", "pip", "install", str(path)], check=False
            )
            if result.returncode != 0:
                raise typer.Exit(result.returncode)
            console.print("[green]Installation successful[/green]")
        except FileNotFoundError:
            console.print("[red]pip not found[/red]")
            raise typer.Exit(1)
    else:
        # Registry install is not yet implemented — only local .whl is supported.
        console.print(f"[bold]Installing: {package}[/bold]")
        console.print("[yellow]Registry install not yet available. Use a local .whl file.[/yellow]")
        raise typer.Exit(1)


def list_packs(
    local: bool = typer.Option(False, "--local", help="List locally installed packs only"),
) -> None:
    """List packs on Huitzo.

    By default queries the remote API. Use --local to discover packs
    installed via the ``huitzo.commands`` entry-point group.
    """
    if local:
        _list_local_packs()
        return

    auth, token, api_url = _get_auth()
    headers = {"Authorization": f"Bearer {token}"}

    try:
        resp = httpx.get(
            f"{api_url}/api/v1/packs",
            headers=headers,
            timeout=30,
        )
    except httpx.HTTPError as e:
        console.print(f"[red]Could not connect to backend at {api_url}: {e}[/red]")
        raise typer.Exit(1)

    if resp.status_code != 200:
        console.print(f"[red]Failed to list packs: {parse_error(resp)}[/red]")
        raise typer.Exit(1)

    try:
        packs = resp.json().get("data", [])
    except (ValueError, json.JSONDecodeError):
        console.print("[red]Unexpected response from server[/red]")
        raise typer.Exit(1)

    if not packs:
        console.print("[yellow]No packs found[/yellow]")
        return

    table = Table(title="Your Packs")
    table.add_column("Scope", style="cyan")
    table.add_column("Name", style="green")
    table.add_column("Visibility", style="yellow")
    table.add_column("Created", style="dim")

    for p in packs:
        scope = p["scope"].removeprefix("@")
        table.add_row(
            f"@{scope}",
            p["name"],
            p.get("visibility", "private"),
            p.get("created_at", "")[:10],
        )

    console.print(table)


def _list_local_packs() -> None:
    """List locally installed packs via entry points."""
    console.print("[bold]Locally Installed Packs:[/bold]")

    huitzo_eps = importlib.metadata.entry_points(group="huitzo.commands")

    packs: dict[str, str] = {}
    for ep in huitzo_eps:
        dist_name = ep.dist.name if ep.dist else ep.name
        version = ep.dist.version if ep.dist else "unknown"
        packs[dist_name] = version

    if packs:
        for name, version in sorted(packs.items()):
            console.print(f"  {name} ({version})")
    else:
        console.print("  (none installed)")


_MAX_OUTPUT_BYTES = 2048


def run(
    pack_command: str = typer.Argument(..., help="Command namespace (e.g., @scope/pack/command)"),
    args_json: str = typer.Option("{}", "--args", help="JSON arguments"),
    raw: bool = typer.Option(False, "--raw", help="Print full raw output without truncation"),
) -> None:
    """Run a pack command via the backend API.

    Args:
        pack_command: Full command namespace (@scope/pack/command) or pack:command shorthand
    """
    auth, token, api_url = _get_auth()
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
    }

    # Parse namespace
    if ":" in pack_command and not pack_command.startswith("@"):
        # Convert pack:command shorthand to @namespace/pack/command
        parts = pack_command.split(":", 1)
        pack_command = f"@{parts[0]}/{parts[1]}"

    try:
        body = json.loads(args_json)
    except json.JSONDecodeError:
        console.print("[red]Invalid JSON args[/red]")
        raise typer.Exit(1)

    console.print(f"[bold]Executing {pack_command}...[/bold]")

    try:
        resp = httpx.post(
            f"{api_url}/api/v1/commands/{pack_command}",
            json={"args": body},
            headers=headers,
            timeout=120,
        )
    except httpx.HTTPError as e:
        console.print(f"[red]Could not connect to backend at {api_url}: {e}[/red]")
        raise typer.Exit(1)

    if resp.status_code != 200:
        console.print(f"[red]Execution failed: {parse_error(resp)}[/red]")
        raise typer.Exit(1)

    data = resp.json().get("data", {})
    output = json.dumps(data, indent=2)
    if raw or len(output) <= _MAX_OUTPUT_BYTES:
        console.print_json(output)
    else:
        syntax = Syntax(output[:_MAX_OUTPUT_BYTES], "json", theme="monokai", word_wrap=True)
        console.print(syntax)
        console.print("\n[dim][truncated — use --raw for full output][/dim]")
