"""
Module: dashboard_command
Description: Dashboard management commands — scaffold, develop, build, validate, publish

Implements:
    - docs/cli/reference.md#dashboard-commands
    - docs/dashboards/manifest.md
    - docs/dashboards/publishing.md#build-contract
    - docs/api/rest.md#dashboards
"""

import json
import logging
import re
import subprocess
import tarfile
from io import BytesIO
from pathlib import Path
from typing import Any, Optional

import httpx
import typer
import yaml
from jinja2 import Environment, PackageLoader
from rich.table import Table

from huitzo_cli.claude_env import prompt_claude_setup
from huitzo_cli.http_utils import get_auth, parse_error
from huitzo_cli.interactive import guarded_prompt
from huitzo_cli.output import OutputContext, get_output

logger = logging.getLogger(__name__)
dashboard_app = typer.Typer(help="Dashboard operations")


def _lookup_dashboard_id(slug: str, headers: dict[str, str], api_url: str) -> str | None:
    """Look up dashboard ID by slug. Returns None if not found.

    Raises httpx.ConnectError / httpx.TimeoutException on network failures
    so callers can distinguish "not found" from "could not connect".
    """
    if not KEBAB_CASE_PATTERN.match(slug):
        raise ValueError(f"Invalid slug: {slug}")
    resp = httpx.get(
        f"{api_url}/api/v1/dashboards/{slug}",
        headers=headers,
        timeout=30,
    )

    if resp.status_code == 404:
        return None
    if resp.status_code != 200:
        raise RuntimeError(f"Dashboard lookup failed: HTTP {resp.status_code}")

    try:
        data: dict[str, Any] = resp.json().get("data", {})
        result: str | None = data.get("id")
        return result
    except (ValueError, json.JSONDecodeError):
        return None


# --- Constants ---

MANIFEST_FILE = "huitzo-dashboard.yaml"
REQUIRED_DASHBOARD_FIELDS = ("name", "namespace", "version", "description")
SEMVER_PATTERN = re.compile(r"^\d+\.\d+\.\d+$")
UUID_PATTERN = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
KEBAB_CASE_PATTERN = re.compile(r"^[a-z][a-z0-9]*(-[a-z0-9]+)*$")
NAMESPACE_PATTERN = re.compile(r"^[a-z][a-z0-9]{1,19}$")
SAFE_ID_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+$")
MAX_BUNDLE_SIZE_MB = 50


# --- Helpers ---


def _load_manifest(project_root: Path) -> dict[str, Any]:
    """Load and return parsed huitzo-dashboard.yaml."""
    manifest_path = project_root / MANIFEST_FILE
    if not manifest_path.exists():
        raise FileNotFoundError(f"{MANIFEST_FILE} not found in {project_root}")
    with manifest_path.open() as f:
        result = yaml.safe_load(f)
        if not isinstance(result, dict):
            raise ValueError(f"{MANIFEST_FILE} is empty or not a valid YAML mapping")
        return result


def _validate_manifest(
    manifest: dict[str, Any], project_root: Path, output: OutputContext
) -> list[str]:
    """Validate manifest and build output, return list of errors."""
    errors: list[str] = []
    warnings: list[str] = []

    # --- Dashboard section ---
    dashboard = manifest.get("dashboard")
    if not dashboard or not isinstance(dashboard, dict):
        errors.append("Missing or invalid 'dashboard' section")
        return errors

    for field in REQUIRED_DASHBOARD_FIELDS:
        if field not in dashboard:
            errors.append(f"Missing required field: dashboard.{field}")

    name = dashboard.get("name", "")
    if name and not KEBAB_CASE_PATTERN.match(name):
        errors.append(f"dashboard.name must be kebab-case: '{name}'")
    if name and not (3 <= len(name) <= 50):
        errors.append(f"dashboard.name must be 3-50 characters: '{name}' ({len(name)} chars)")

    namespace = dashboard.get("namespace", "")
    if namespace and not NAMESPACE_PATTERN.match(namespace):
        errors.append(
            f"dashboard.namespace must be lowercase, no dashes, 2-20 characters: '{namespace}'"
        )

    version = dashboard.get("version", "")
    if version and not SEMVER_PATTERN.match(version):
        errors.append(f"dashboard.version must be valid semver: '{version}'")

    min_sdk = dashboard.get("min_sdk_version")
    if min_sdk and not SEMVER_PATTERN.match(min_sdk):
        errors.append(f"dashboard.min_sdk_version must be valid semver: '{min_sdk}'")

    visibility = dashboard.get("visibility", "private")
    valid_visibilities = ("public", "unlisted", "organization", "private")
    if visibility not in valid_visibilities:
        errors.append(f"dashboard.visibility must be one of {valid_visibilities}: '{visibility}'")

    description = dashboard.get("description", "")
    if description and len(description) < 10:
        errors.append(f"dashboard.description must be ≥10 characters ({len(description)} chars)")
    if description and len(description) > 200:
        errors.append(f"dashboard.description must be ≤200 characters ({len(description)} chars)")

    # --- Build output ---
    build_config = manifest.get("build", {})
    entry_point = build_config.get("entry_point", "main.js")
    output_dir = build_config.get("output_directory", "dist")

    # Path traversal check on manifest-provided paths
    bundle_path = (project_root / output_dir / entry_point).resolve()
    if not str(bundle_path).startswith(str(project_root.resolve())):
        errors.append(f"entry_point path escapes project root: {output_dir}/{entry_point}")
        return errors

    if not bundle_path.exists():
        warnings.append(
            f"Bundle not found: {output_dir}/{entry_point} (run 'huitzo dashboard build' first)"
        )
    else:
        # Check bundle size
        size_mb = bundle_path.stat().st_size / (1024 * 1024)
        if size_mb > MAX_BUNDLE_SIZE_MB:
            errors.append(f"Bundle too large: {size_mb:.1f} MB (limit: {MAX_BUNDLE_SIZE_MB} MB)")
        else:
            # Check for mount and unmount exports (basic word-boundary check)
            bundle_text = bundle_path.read_text(errors="replace")
            if not re.search(r"\bmount\b", bundle_text):
                errors.append(
                    f"Bundle {output_dir}/{entry_point} does not appear to export 'mount'"
                )
            if not re.search(r"\bunmount\b", bundle_text):
                errors.append(
                    f"Bundle {output_dir}/{entry_point} does not appear to export 'unmount'"
                )

    # Print warnings (non-fatal)
    for w in warnings:
        output.print(f"[yellow]⚠  {w}[/yellow]")

    return errors


# --- Commands ---


@dashboard_app.command()
def new(
    ctx: typer.Context,
    name: Optional[str] = typer.Argument(None, help="Dashboard name"),
    author: Optional[str] = typer.Option(None, "--author", help="Author name"),
) -> None:
    """Create a new dashboard project.

    Scaffolds a React + TypeScript dashboard with Vite library mode,
    mount/unmount module contract, and a local dev harness.
    """
    output = get_output(ctx)

    if not name:
        name = guarded_prompt(ctx, "Dashboard name")

    # Validate name before any filesystem operations
    if not KEBAB_CASE_PATTERN.match(name):
        output.print(f"[red]❌ Dashboard name must be kebab-case: '{name}'[/red]")
        output.print("Example: my-dashboard, claims-viewer, analytics-hub")
        raise typer.Exit(1)

    project_root = Path.cwd() / name

    if project_root.exists():
        output.print(f"[red]❌ Directory '{name}' already exists[/red]")
        raise typer.Exit(1)

    # Resolve the user's namespace for the manifest scope
    namespace = ""
    try:
        auth, token, api_url = get_auth()
        user_info = auth.get_current_user()
        if user_info:
            namespace = user_info.get("tenantSlug", "")
    except (typer.Exit, httpx.HTTPError, OSError) as exc:
        logger.debug("Namespace auto-detection failed (will prompt): %s", exc)

    if not namespace:
        namespace = guarded_prompt(ctx, "Namespace (your Huitzo username or org slug)")
    if not NAMESPACE_PATTERN.match(namespace):
        output.print(f"[red]❌ Invalid namespace: '{namespace}'[/red]")
        raise typer.Exit(1)

    project_root.mkdir(parents=True, exist_ok=True)
    src_dir = project_root / "src"
    src_dir.mkdir(parents=True, exist_ok=True)

    # Set up Jinja2
    env = Environment(loader=PackageLoader("huitzo_cli", "templates/dashboard"))

    context = {"dashboard_name": name, "namespace": namespace}

    # Files to create — order: root files, then src/ files
    files = {
        # Root
        "huitzo-dashboard.yaml.j2": project_root / MANIFEST_FILE,
        "package.json.j2": project_root / "package.json",
        "vite.config.ts.j2": project_root / "vite.config.ts",
        "index.html.j2": project_root / "index.html",
        "tsconfig.json.j2": project_root / "tsconfig.json",
        "tsconfig.node.json.j2": project_root / "tsconfig.node.json",
        # Source
        "main.tsx.j2": src_dir / "main.tsx",
        "dev.tsx.j2": src_dir / "dev.tsx",
        "App.tsx.j2": src_dir / "App.tsx",
        "index.css.j2": src_dir / "index.css",
        "App.css.j2": src_dir / "App.css",
    }

    for template_name, target_path in files.items():
        template = env.get_template(template_name)
        rendered = template.render(**context)
        target_path.write_text(rendered)

    # Create .gitignore
    (project_root / ".gitignore").write_text("node_modules/\n.env.local\ndist/\n.DS_Store\n")

    # Create docs directory structure
    docs_dir = project_root / "docs"
    docs_components_dir = docs_dir / "components"
    docs_pages_dir = docs_dir / "pages"
    docs_guides_dir = docs_dir / "guides"
    docs_components_dir.mkdir(parents=True, exist_ok=True)
    docs_pages_dir.mkdir(parents=True, exist_ok=True)
    docs_guides_dir.mkdir(parents=True, exist_ok=True)

    (docs_dir / "README.md").write_text(
        f"---\ntitle: {name}\ntags: [overview]\ncategory: overview\norder: 1\n---\n\n"
        f"# {name}\n\nDashboard for Huitzo Hub.\n\n"
        "## Pages\n\nSee [pages/](pages/) for available pages.\n\n"
        "## Components\n\nSee [components/](components/) for reusable components.\n"
    )
    (docs_pages_dir / "README.md").write_text(
        "---\ntitle: Pages\ntags: [pages]\ncategory: pages\norder: 1\n---\n\n"
        "# Pages\n\nDashboard pages:\n\n"
        "| Page | Description |\n|------|-------------|\n"
    )
    (docs_components_dir / "README.md").write_text(
        "---\ntitle: Components\ntags: [components]\ncategory: components\norder: 1\n---\n\n"
        "# Components\n\nReusable components:\n\n"
        "| Component | Description |\n|-----------|-------------|\n"
    )
    (docs_guides_dir / "README.md").write_text(
        "---\ntitle: Guides\ntags: [guides]\ncategory: guides\norder: 1\n---\n\n"
        "# Guides\n\n- [Getting Started](getting-started.md)\n"
    )
    (docs_guides_dir / "getting-started.md").write_text(
        "---\ntitle: Getting Started\ntags: [guide, quickstart]\ncategory: guides\norder: 2\n---\n\n"
        f"# Getting Started with {name}\n\n## Development\n\n```bash\nnpm install\nhuitzo dashboard dev\n```\n"
    )

    output.print(f"\n[green]✅ Dashboard '{name}' created![/green]")
    output.print("\n[bold]Next steps:[/bold]")
    output.print(f"  cd {name}")
    output.print("  npm install")
    output.print("  huitzo dashboard dev")

    output.success({"name": name, "namespace": namespace, "path": str(project_root)})

    # Claude Code environment setup (opt-in)
    prompt_claude_setup(project_root, default_profile="dashboard-only", has_venv=False)


@dashboard_app.command()
def dev(ctx: typer.Context) -> None:
    """Start dashboard development server.

    Starts Vite dev server with hot module replacement.
    """
    output = get_output(ctx)
    output.print("[bold]Starting dashboard dev server...[/bold]")

    try:
        result = subprocess.run(["npm", "run", "dev"], check=False)
        if result.returncode != 0:
            raise typer.Exit(result.returncode)
    except FileNotFoundError:
        output.print("[red]npm not found. Install Node.js from nodejs.org[/red]")
        raise typer.Exit(1)

    output.success({"status": "stopped"})


@dashboard_app.command()
def build(ctx: typer.Context) -> None:
    """Build dashboard for production.

    Creates optimized production build in dist/ directory using Vite library mode.
    Output is a self-contained ESM module (dist/main.js).
    """
    output = get_output(ctx)
    output.print("[bold]Building dashboard for production...[/bold]")

    try:
        result = subprocess.run(["npm", "run", "build"], check=False)
        if result.returncode == 0:
            output.print("[green]✅ Build successful. Output in dist/[/green]")
        else:
            raise typer.Exit(result.returncode)
    except FileNotFoundError:
        output.print("[red]❌ npm not found[/red]")
        raise typer.Exit(1)

    output.success({"status": "built", "output_dir": "dist"})


@dashboard_app.command()
def validate(ctx: typer.Context) -> None:
    """Validate dashboard manifest and build output.

    Checks huitzo-dashboard.yaml, build output, and module contract
    without publishing.
    """
    output = get_output(ctx)
    project_root = Path.cwd()

    # Load manifest
    try:
        manifest = _load_manifest(project_root)
    except FileNotFoundError:
        output.print(f"[red]❌ {MANIFEST_FILE} not found[/red]")
        output.print("Run 'huitzo dashboard new' to create a dashboard project.")
        raise typer.Exit(1)
    except ValueError as exc:
        output.print(f"[red]❌ {exc}[/red]")
        raise typer.Exit(1)

    output.print(f"[bold]Validating {MANIFEST_FILE}...[/bold]\n")

    dashboard = manifest.get("dashboard", {})

    # Show what we found
    table = Table(show_header=True)
    table.add_column("Check", style="bold")
    table.add_column("Value")
    table.add_column("Status")

    name = dashboard.get("name", "")
    version = dashboard.get("version", "")
    namespace = dashboard.get("namespace", "")

    table.add_row("dashboard.name", name, _check_mark(bool(name)))
    table.add_row("dashboard.version", version, _check_mark(bool(version)))
    table.add_row("dashboard.namespace", namespace, _check_mark(bool(namespace)))
    table.add_row(
        "dashboard.description",
        _truncate(dashboard.get("description", ""), 40),
        _check_mark(bool(dashboard.get("description"))),
    )

    # Build output
    build_config = manifest.get("build", {})
    entry_point = build_config.get("entry_point", "main.js")
    output_dir = build_config.get("output_directory", "dist")
    bundle_path = project_root / output_dir / entry_point

    if bundle_path.exists():
        size_kb = bundle_path.stat().st_size / 1024
        table.add_row(
            f"{output_dir}/{entry_point}",
            f"{size_kb:.0f} KB",
            _check_mark(True),
        )
    else:
        table.add_row(f"{output_dir}/{entry_point}", "not found", "[yellow]⚠[/yellow]")

    output.print(table)

    # Run validation
    errors = _validate_manifest(manifest, project_root, output)

    if errors:
        output.print(f"\n[red]❌ {len(errors)} error(s) found:[/red]")
        for err in errors:
            output.print(f"  [red]• {err}[/red]")
        raise typer.Exit(1)

    output.print("\n[green]✅ All checks passed.[/green]")
    output.success({"name": name, "version": version, "valid": True})


@dashboard_app.command()
def publish(
    ctx: typer.Context,
    dry_run: bool = typer.Option(False, "--dry-run", help="Simulate without publishing"),
) -> None:
    """Publish dashboard to Huitzo Hub.

    Validates, creates a tarball from dist/, and uploads to the registry.
    Use --dry-run to see what would be uploaded without publishing.
    """
    output = get_output(ctx)
    project_root = Path.cwd()

    # Load and validate manifest
    try:
        manifest = _load_manifest(project_root)
    except FileNotFoundError:
        output.print(f"[red]❌ {MANIFEST_FILE} not found[/red]")
        raise typer.Exit(1)
    except ValueError as exc:
        output.print(f"[red]❌ {exc}[/red]")
        raise typer.Exit(1)

    dashboard = manifest.get("dashboard", {})
    name = dashboard.get("name", "unknown")
    version = dashboard.get("version", "0.0.0")

    output.print(f"[bold]Publishing {name}@{version}...[/bold]\n")

    # Validate first
    errors = _validate_manifest(manifest, project_root, output)
    if errors:
        output.print(f"[red]❌ Validation failed ({len(errors)} error(s)):[/red]")
        for err in errors:
            output.print(f"  [red]• {err}[/red]")
        raise typer.Exit(1)

    output.print("[green]✅ Validation passed[/green]")

    # Check bundle exists
    build_config = manifest.get("build", {})
    output_dir = build_config.get("output_directory", "dist")
    dist_path = project_root / output_dir

    if not dist_path.exists():
        output.print(f"[red]❌ Build output not found: {output_dir}/[/red]")
        output.print("Run 'huitzo dashboard build' first.")
        raise typer.Exit(1)

    # Create tarball in memory, skipping symlinks for safety
    tarball_buf = BytesIO()
    resolved_root = project_root.resolve()
    with tarfile.open(fileobj=tarball_buf, mode="w:gz") as tar:
        for item in dist_path.rglob("*"):
            if item.is_symlink():
                output.print(f"[yellow]⚠  Skipping symlink: {item.name}[/yellow]")
                continue
            if not str(item.resolve()).startswith(str(resolved_root)):
                continue
            tar.add(str(item), arcname=str(item.relative_to(dist_path)))

    tarball_size = tarball_buf.tell()
    size_kb = tarball_size / 1024

    output.print(f"[green]✅ Tarball created ({size_kb:.0f} KB)[/green]")

    # Show contents
    tarball_buf.seek(0)
    with tarfile.open(fileobj=tarball_buf, mode="r:gz") as tar:
        output.print(f"\n[bold]Bundle contents ({len(tar.getnames())} files):[/bold]")
        for member in tar.getmembers():
            if member.isfile():
                file_size = member.size / 1024
                output.print(f"  {member.name} ({file_size:.0f} KB)")

    if dry_run:
        output.print("\n[yellow]Dry run — nothing was uploaded.[/yellow]")
        output.success({"name": name, "version": version, "dry_run": True, "size_kb": size_kb})
        return

    # Authenticate and upload
    _, token, api_url = get_auth()
    headers = {"Authorization": f"Bearer {token}"}

    namespace = dashboard.get("namespace", "")
    scope = namespace
    visibility = dashboard.get("visibility", "private")

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

    dashboard_id = None
    if resp.status_code == 201:
        output.print("[green]Dashboard registered[/green]")
        try:
            dashboard_id = resp.json().get("data", {}).get("id")
        except (ValueError, json.JSONDecodeError):
            pass  # Falls through to lookup path
    elif resp.status_code == 409:
        output.print("Dashboard already registered")
    else:
        output.print(f"[red]Failed to register dashboard: {parse_error(resp)}[/red]")
        raise typer.Exit(1)

    # If 409 (already exists), look up dashboard ID by slug
    if not dashboard_id:
        try:
            dashboard_id = _lookup_dashboard_id(name, headers, api_url)
        except httpx.HTTPError as e:
            output.print(f"[red]Could not connect to backend at {api_url}: {e}[/red]")
            raise typer.Exit(1)
        except RuntimeError as e:
            output.print(f"[red]{e}[/red]")
            raise typer.Exit(1)

    if not dashboard_id:
        output.print(
            "[red]Could not resolve dashboard ID after registration. "
            "This may be a transient error — please retry.[/red]"
        )
        raise typer.Exit(1)

    # Validate dashboard_id format before URL construction (defense-in-depth)
    if not SAFE_ID_PATTERN.match(dashboard_id):
        output.print("[red]Invalid dashboard ID received from backend[/red]")
        raise typer.Exit(1)

    # Upload tarball
    tarball_name = f"{name}-{version}.tar.gz"
    output.print(f"[bold]Uploading {tarball_name} (v{version})...[/bold]")
    tarball_buf.seek(0)
    try:
        resp = httpx.post(
            f"{api_url}/api/v1/dashboards/{dashboard_id}/versions",
            files={"bundle": (tarball_name, tarball_buf, "application/gzip")},
            data={"version": version},
            headers=headers,
            timeout=120,
        )
    except httpx.HTTPError as e:
        output.print(f"[red]Could not connect to backend at {api_url}: {e}[/red]")
        raise typer.Exit(1)

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

    output.print(f"\n[green]Published {name}@{version} successfully[/green]")
    output.print(f"Hub URL: https://hub.huitzo.com/d/{name}")
    output.success({"name": name, "version": version, "url": f"https://hub.huitzo.com/d/{name}"})


@dashboard_app.command()
def grant(
    ctx: typer.Context,
    dashboard_name: str = typer.Argument(..., help="Dashboard name (slug)"),
    tenant_id: str = typer.Argument(
        ..., help="Tenant UUID to grant access (org-slug resolution not yet available)"
    ),
) -> None:
    """Grant a tenant access to an organization-visibility dashboard.

    Requires authentication and dashboard ownership.

    Note: Currently accepts tenant UUIDs directly. Organization-name-to-UUID
    resolution will be available when the backend adds
    GET /api/v1/organizations?slug=<name>.
    """
    output = get_output(ctx)

    if not KEBAB_CASE_PATTERN.match(dashboard_name):
        output.print(f"[red]Invalid dashboard name: '{dashboard_name}' (must be kebab-case)[/red]")
        raise typer.Exit(1)
    if not UUID_PATTERN.match(tenant_id):
        output.print(f"[red]Invalid tenant UUID: '{tenant_id}'[/red]")
        raise typer.Exit(1)

    _, token, api_url = get_auth()
    headers = {"Authorization": f"Bearer {token}"}

    # Look up dashboard ID by slug
    output.print(f"[bold]Looking up dashboard '{dashboard_name}'...[/bold]")
    try:
        dash_id = _lookup_dashboard_id(dashboard_name, headers, api_url)
    except httpx.HTTPError as e:
        output.print(f"[red]Could not connect to backend at {api_url}: {e}[/red]")
        raise typer.Exit(1)
    except RuntimeError as e:
        output.print(f"[red]{e}[/red]")
        raise typer.Exit(1)
    if not dash_id:
        output.print(f"[red]Dashboard '{dashboard_name}' not found[/red]")
        raise typer.Exit(1)

    # Validate dash_id format before URL construction (defense-in-depth)
    if not SAFE_ID_PATTERN.match(dash_id):
        output.print("[red]Invalid dashboard ID received from backend[/red]")
        raise typer.Exit(1)

    # Grant access
    output.print(f"[bold]Granting tenant {tenant_id} access...[/bold]")
    try:
        resp = httpx.post(
            f"{api_url}/api/v1/dashboards/{dash_id}/grant",
            json={"tenant_ids": [tenant_id]},
            headers=headers,
            timeout=30,
        )
    except httpx.HTTPError as e:
        output.print(f"[red]Could not connect to backend at {api_url}: {e}[/red]")
        raise typer.Exit(1)

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

    try:
        data = resp.json().get("data", {})
    except (ValueError, json.JSONDecodeError):
        data = {}
    output.print(f"[green]Granted access ({data.get('granted', 0)} tenant(s))[/green]")
    output.success(
        {"dashboard": dashboard_name, "tenant_id": tenant_id, "granted": data.get("granted", 0)}
    )


@dashboard_app.command()
def revoke(
    ctx: typer.Context,
    dashboard_name: str = typer.Argument(..., help="Dashboard name"),
    tenant_id: str = typer.Argument(..., help="Tenant UUID to revoke access"),
) -> None:
    """Revoke a tenant's access to a dashboard.

    Note: Backend revoke endpoint (DELETE /api/v1/dashboards/{id}/grant)
    is not yet implemented.
    """
    output = get_output(ctx)
    output.print("[yellow]Backend revoke endpoint not yet implemented[/yellow]")
    output.print("Track progress at: https://github.com/Huitzo-Inc/huitzo/issues")
    raise typer.Exit(1)


@dashboard_app.command()
def share(
    ctx: typer.Context,
    dashboard_name: str = typer.Argument(..., help="Dashboard name"),
    expires: str = typer.Option("never", "--expires", help="Link expiration (1d, 7d, 30d, never)"),
) -> None:
    """Generate a shareable link for unlisted dashboards.

    Note: Backend share endpoint is not yet implemented.
    """
    output = get_output(ctx)
    output.print("[yellow]Backend share endpoint not yet implemented[/yellow]")
    output.print("Track progress at: https://github.com/Huitzo-Inc/huitzo/issues")
    raise typer.Exit(1)


# --- Utilities ---


def _check_mark(ok: bool) -> str:
    """Return a green check or red X."""
    return "[green]✓[/green]" if ok else "[red]✗[/red]"


def _truncate(text: str, max_len: int) -> str:
    """Truncate text with ellipsis."""
    if len(text) <= max_len:
        return text
    return text[: max_len - 3] + "..."
