"""ReBAC (Relationship-Based Access Control) CLI commands.

Manage authorization relationships using Zanzibar-style ReBAC.
Enables team-based permissions, hierarchical access, and dynamic inheritance.
"""

import sys
from typing import Any

import click
from rich.table import Table

from nexus.cli.utils import (
    add_backend_options,
    add_context_options,
    console,
    get_filesystem,
    handle_error,
)


@click.group(name="rebac")
def rebac() -> None:
    """Relationship-Based Access Control (ReBAC) commands.

    Manage authorization relationships using Zanzibar-style ReBAC.
    Enables team-based permissions, hierarchical access, and dynamic inheritance.

    Examples:
        nexus rebac create agent alice member-of group eng-team
        nexus rebac check agent alice read file file123
        nexus rebac expand read file file123
        nexus rebac delete <tuple-id>
    """
    pass


@rebac.command(name="create")
@click.argument("subject_type", type=str)
@click.argument("subject_id", type=str)
@click.argument("relation", type=str)
@click.argument("object_type", type=str)
@click.argument("object_id", type=str)
@click.option("--expires", type=str, default=None, help="Expiration time (ISO format)")
# Note: --zone-id is provided by @add_context_options decorator
@click.option(
    "--subject-relation",
    type=str,
    default=None,
    help="Subject relation for userset-as-subject (e.g., 'member' for group#member)",
)
@click.option(
    "--wildcard",
    is_flag=True,
    help="Use wildcard subject (*:*) for public access (overrides subject_type/subject_id)",
)
@click.option(
    "--column-config",
    type=str,
    default=None,
    help='JSON column config for dynamic_viewer (e.g., \'{"mode":"whitelist","visible_columns":["name","email"]}\')',
)
@add_backend_options
@add_context_options
def rebac_create(
    subject_type: str,
    subject_id: str,
    relation: str,
    object_type: str,
    object_id: str,
    expires: str | None,
    subject_relation: str | None,
    wildcard: bool,
    column_config: str | None,
    remote_url: str | None,
    remote_api_key: str | None,
    operation_context: dict[str, Any],
) -> None:
    """Create a relationship tuple.

    Creates a (subject, relation, object) tuple representing a relationship.
    Supports multi-zone isolation via --zone-id or NEXUS_ZONE_ID env var.

    Advanced Features:
        --subject-relation: Grant to entire groups (userset-as-subject)
        --wildcard: Grant public access to anyone
        --column-config: Column-level permissions for dynamic_viewer

    Examples:
        # Basic: Alice is member of eng-team
        nexus rebac create agent alice member-of group eng-team

        # Multi-zone: Eng-team owns file123
        nexus rebac create group eng-team owner-of file file123 --zone-id org_acme

        # Userset-as-subject: Grant all eng-team members editor access
        nexus rebac create group eng-team editor-of file readme.txt --subject-relation member

        # Wildcard: Public read access to readme
        nexus rebac create '*' '*' viewer-of file readme.txt --wildcard

        # Temporary access (expires in 1 hour)
        nexus rebac create agent bob viewer-of file secret --expires 2025-12-31T23:59:59

        # Dynamic viewer with column-level permissions (CSV only)
        nexus rebac create agent alice dynamic_viewer file /data/users.csv --column-config '{"hidden_columns":["password"],"aggregations":{"age":"mean"},"visible_columns":["name","email"]}'
    """
    import asyncio

    asyncio.run(
        _async_rebac_create(
            subject_type,
            subject_id,
            relation,
            object_type,
            object_id,
            expires,
            subject_relation,
            wildcard,
            column_config,
            remote_url,
            remote_api_key,
            operation_context,
        )
    )


async def _async_rebac_create(
    subject_type: str,
    subject_id: str,
    relation: str,
    object_type: str,
    object_id: str,
    expires: str | None,
    subject_relation: str | None,
    wildcard: bool,
    column_config: str | None,
    remote_url: str | None,
    remote_api_key: str | None,
    operation_context: dict[str, Any],
) -> None:
    try:
        nx = await get_filesystem(remote_url, remote_api_key)

        # Parse expiration time if provided
        expires_at = None
        if expires:
            from datetime import datetime

            try:
                expires_at = datetime.fromisoformat(expires)
            except ValueError:
                console.print(f"[nexus.error]Error:[/nexus.error] Invalid date format: {expires}")
                console.print("Use ISO format: 2025-12-31T23:59:59")
                nx.close()
                sys.exit(1)

        # Get zone_id from operation_context (set by @add_context_options)
        zone = operation_context.get("zone")

        # Parse column_config JSON if provided
        column_config_dict = None
        if column_config:
            import json

            try:
                column_config_dict = json.loads(column_config)
            except json.JSONDecodeError as e:
                console.print(
                    f"[nexus.error]Error:[/nexus.error] Invalid JSON in column-config: {e}"
                )
                nx.close()
                sys.exit(1)

        # Build subject tuple
        subject_tuple: tuple[str, str] | tuple[str, str, str]
        if wildcard:
            # Wildcard subject for public access
            subject_tuple = ("*", "*")
            subject_display = "*:*"
        elif subject_relation:
            # Userset-as-subject (3-tuple)
            subject_tuple = (subject_type, subject_id, subject_relation)
            subject_display = f"{subject_type}:{subject_id}#{subject_relation}"
        else:
            # Regular subject (2-tuple)
            subject_tuple = (subject_type, subject_id)
            subject_display = f"{subject_type}:{subject_id}"

        # Create tuple
        # SECURITY: Pass operation_context for execute permission enforcement
        # Only owners (execute permission) can create permissions on files
        rebac_svc = nx.service("rebac")
        assert rebac_svc is not None, "ReBAC service not available"
        tuple_id = rebac_svc.rebac_create_sync(
            subject=subject_tuple,
            relation=relation,
            object=(object_type, object_id),
            expires_at=expires_at,
            zone_id=zone,
            column_config=column_config_dict,
            context=operation_context,
        )

        nx.close()

        console.print("[nexus.success]✓[/nexus.success] Created relationship tuple")
        console.print(f"  Tuple ID: [nexus.value]{tuple_id}[/nexus.value]")
        console.print(f"  Subject: [nexus.warning]{subject_display}[/nexus.warning]")
        if wildcard:
            console.print("    [nexus.muted](wildcard - public access)[/nexus.muted]")
        elif subject_relation:
            console.print(
                f"    [nexus.muted](userset-as-subject: all '{subject_relation}' of {subject_type}:{subject_id})[/nexus.muted]"
            )
        console.print(f"  Relation: [nexus.identity]{relation}[/nexus.identity]")
        console.print(f"  Object: [nexus.warning]{object_type}:{object_id}[/nexus.warning]")
        if zone:
            console.print(f"  Zone: [nexus.reference]{zone}[/nexus.reference]")
        if expires_at:
            console.print(f"  Expires: [nexus.muted]{expires_at.isoformat()}[/nexus.muted]")
        if column_config_dict:
            console.print("  Column Config:")
            if column_config_dict.get("hidden_columns"):
                console.print(
                    f"    Hidden Columns: [nexus.error]{', '.join(column_config_dict['hidden_columns'])}[/nexus.error]"
                )
            if column_config_dict.get("visible_columns"):
                console.print(
                    f"    Visible Columns: [nexus.success]{', '.join(column_config_dict['visible_columns'])}[/nexus.success]"
                )
            if column_config_dict.get("aggregations"):
                console.print("    Aggregations:")
                for col, op in column_config_dict["aggregations"].items():
                    console.print(f"      {col}: [nexus.warning]{op}[/nexus.warning]")

    except Exception as e:
        handle_error(e)


@rebac.command(name="list")
@click.option("--subject-type", type=str, help="Filter by subject type")
@click.option("--subject-id", type=str, help="Filter by subject ID")
@click.option("--object-type", type=str, help="Filter by object type")
@click.option("--object-id", type=str, help="Filter by object ID")
@click.option("--relation", type=str, help="Filter by relation")
@click.option(
    "--format",
    "output_format",
    type=click.Choice(["table", "json", "compact"]),
    default="table",
    help="Output format",
)
@click.option("--limit", type=int, help="Limit number of results")
@add_backend_options
def rebac_list_cmd(
    subject_type: str | None,
    subject_id: str | None,
    object_type: str | None,
    object_id: str | None,
    relation: str | None,
    output_format: str,
    limit: int | None,
    remote_url: str | None,
    remote_api_key: str | None,
) -> None:
    """List relationship tuples with optional filters.

    Examples:
        # List all tuples
        nexus rebac list

        # List tuples for a specific file
        nexus rebac list --object-type file --object-id /workspace/test.txt

        # List all tuples for user alice
        nexus rebac list --subject-type user --subject-id alice

        # List all editor relations
        nexus rebac list --relation direct_editor

        # Compact format
        nexus rebac list --format compact

        # JSON output
        nexus rebac list --format json
    """
    import asyncio

    asyncio.run(
        _async_rebac_list_cmd(
            subject_type,
            subject_id,
            object_type,
            object_id,
            relation,
            output_format,
            limit,
            remote_url,
            remote_api_key,
        )
    )


async def _async_rebac_list_cmd(
    subject_type: str | None,
    subject_id: str | None,
    object_type: str | None,
    object_id: str | None,
    relation: str | None,
    output_format: str,
    limit: int | None,
    remote_url: str | None,
    remote_api_key: str | None,
) -> None:
    try:
        import json

        nx = await get_filesystem(remote_url, remote_api_key)

        # Build filters
        subject = None
        if subject_type and subject_id:
            subject = (subject_type, subject_id)

        obj = None
        if object_type and object_id:
            obj = (object_type, object_id)

        # List tuples
        rebac_svc = nx.service("rebac")
        assert rebac_svc is not None, "ReBAC service not available"
        tuples = rebac_svc.rebac_list_tuples_sync(
            subject=subject,
            object=obj,
            relation=relation,
        )

        nx.close()

        # Apply limit if specified
        if limit and limit > 0:
            tuples = tuples[:limit]

        # Display results
        if not tuples:
            console.print("[nexus.warning]No tuples found[/nexus.warning]")
            return

        if output_format == "json":
            console.print(json.dumps(tuples, indent=2, default=str))
        elif output_format == "compact":
            for t in tuples:
                subj = f"{t['subject_type']}:{t['subject_id']}"
                if t.get("subject_relation"):
                    subj += f"#{t['subject_relation']}"
                obj_str = f"{t['object_type']}:{t['object_id']}"
                console.print(f"{subj} → {t['relation']} → {obj_str}")
        else:
            # Table format
            table = Table(title=f"ReBAC Tuples ({len(tuples)} found)")
            table.add_column("Tuple ID", style="nexus.muted", no_wrap=True)
            table.add_column("Subject", style="nexus.warning")
            table.add_column("Relation", style="nexus.identity")
            table.add_column("Object", style="nexus.value")
            table.add_column("Zone", style="nexus.reference")

            for t in tuples:
                # Format subject
                subj = f"{t['subject_type']}:{t['subject_id']}"
                if t.get("subject_relation"):
                    subj += f"#{t['subject_relation']}"

                # Format object
                obj_str = f"{t['object_type']}:{t['object_id']}"

                # Truncate IDs for display
                tuple_id = t["tuple_id"]
                if len(tuple_id) > 36:
                    tuple_id = tuple_id[:8] + "..." + tuple_id[-8:]

                table.add_row(
                    tuple_id,
                    subj,
                    t["relation"],
                    obj_str,
                    t.get("zone_id") or "-",
                )

            console.print(table)

    except Exception as e:
        handle_error(e)


@rebac.command(name="delete")
@click.argument("tuple_id", type=str)
@add_backend_options
def rebac_delete_cmd(
    tuple_id: str,
    remote_url: str | None,
    remote_api_key: str | None,
) -> None:
    """Delete a relationship tuple.

    Examples:
        nexus rebac delete 550e8400-e29b-41d4-a716-446655440000
    """
    import asyncio

    asyncio.run(_async_rebac_delete_cmd(tuple_id, remote_url, remote_api_key))


async def _async_rebac_delete_cmd(
    tuple_id: str,
    remote_url: str | None,
    remote_api_key: str | None,
) -> None:
    try:
        nx = await get_filesystem(remote_url, remote_api_key)

        rebac_svc = nx.service("rebac")
        assert rebac_svc is not None, "ReBAC service not available"
        deleted = rebac_svc.rebac_delete_sync(tuple_id)

        nx.close()

        if deleted:
            console.print(
                f"[nexus.success]✓[/nexus.success] Deleted relationship tuple [nexus.value]{tuple_id}[/nexus.value]"
            )
        else:
            console.print(f"[nexus.warning]Tuple not found:[/nexus.warning] {tuple_id}")

    except Exception as e:
        handle_error(e)


@rebac.command(name="check")
@click.argument("subject_type", type=str)
@click.argument("subject_id", type=str)
@click.argument("permission", type=str)
@click.argument("object_type", type=str)
@click.argument("object_id", type=str)
@add_backend_options
@add_context_options
def rebac_check_cmd(
    subject_type: str,
    subject_id: str,
    permission: str,
    object_type: str,
    object_id: str,
    remote_url: str | None,
    remote_api_key: str | None,
    operation_context: dict[str, Any],
) -> None:
    """Check if subject has permission on object.

    Uses graph traversal and caching to determine if permission is granted.

    Examples:
        # Does alice have read permission on file123?
        nexus rebac check agent alice read file file123

        # Does bob have write permission on workspace?
        nexus rebac check agent bob write workspace main

        # Does eng-team have owner permission on project?
        nexus rebac check group eng-team owner file project-folder
    """
    import asyncio

    asyncio.run(
        _async_rebac_check_cmd(
            subject_type,
            subject_id,
            permission,
            object_type,
            object_id,
            remote_url,
            remote_api_key,
            operation_context,
        )
    )


async def _async_rebac_check_cmd(
    subject_type: str,
    subject_id: str,
    permission: str,
    object_type: str,
    object_id: str,
    remote_url: str | None,
    remote_api_key: str | None,
    operation_context: dict[str, Any],
) -> None:
    try:
        nx = await get_filesystem(remote_url, remote_api_key)

        # Check permission (pass zone_id from --zone-id or NEXUS_ZONE_ID)
        zone = operation_context.get("zone")
        rebac_svc = nx.service("rebac")
        assert rebac_svc is not None, "ReBAC service not available"
        granted = rebac_svc.rebac_check_sync(
            subject=(subject_type, subject_id),
            permission=permission,
            object=(object_type, object_id),
            zone_id=zone,
        )

        nx.close()

        # Display result
        if granted:
            console.print("[nexus.success]✓ GRANTED[/nexus.success]")
            console.print(
                f"  [nexus.warning]{subject_type}:{subject_id}[/nexus.warning] has [nexus.identity]{permission}[/nexus.identity] on [nexus.warning]{object_type}:{object_id}[/nexus.warning]"
            )
        else:
            console.print("[nexus.error]✗ DENIED[/nexus.error]")
            console.print(
                f"  [nexus.warning]{subject_type}:{subject_id}[/nexus.warning] does NOT have [nexus.identity]{permission}[/nexus.identity] on [nexus.warning]{object_type}:{object_id}[/nexus.warning]"
            )

    except Exception as e:
        handle_error(e)


@rebac.command(name="expand")
@click.argument("permission", type=str)
@click.argument("object_type", type=str)
@click.argument("object_id", type=str)
@add_backend_options
@add_context_options
def rebac_expand_cmd(
    permission: str,
    object_type: str,
    object_id: str,
    remote_url: str | None,
    remote_api_key: str | None,
    operation_context: dict[str, Any],
) -> None:
    """Find all subjects with a given permission on an object.

    Uses recursive graph traversal to find all subjects.

    Examples:
        # Who has read permission on file123?
        nexus rebac expand read file file123

        # Who has write permission on workspace?
        nexus rebac expand write workspace main

        # Who owns the project folder?
        nexus rebac expand owner file project-folder
    """
    import asyncio

    asyncio.run(
        _async_rebac_expand_cmd(
            permission,
            object_type,
            object_id,
            remote_url,
            remote_api_key,
            operation_context,
        )
    )


async def _async_rebac_expand_cmd(
    permission: str,
    object_type: str,
    object_id: str,
    remote_url: str | None,
    remote_api_key: str | None,
    operation_context: dict[str, Any],
) -> None:
    try:
        nx = await get_filesystem(remote_url, remote_api_key)

        # Expand permission (pass zone_id from --zone-id or NEXUS_ZONE_ID)
        zone = operation_context.get("zone")
        rebac_svc = nx.service("rebac")
        assert rebac_svc is not None, "ReBAC service not available"
        subjects = rebac_svc.rebac_expand_sync(
            permission=permission,
            object=(object_type, object_id),
            zone_id=zone,
        )

        nx.close()

        # Display results
        if not subjects:
            console.print(
                f"[nexus.warning]No subjects found with[/nexus.warning] [nexus.identity]{permission}[/nexus.identity] [nexus.warning]on[/nexus.warning] [nexus.value]{object_type}:{object_id}[/nexus.value]"
            )
            return

        console.print(
            f"[nexus.success]Found {len(subjects)} subjects[/nexus.success] with [nexus.identity]{permission}[/nexus.identity] on [nexus.value]{object_type}:{object_id}[/nexus.value]"
        )
        console.print()

        table = Table(title=f"Subjects with '{permission}' permission")
        table.add_column("Subject Type", style="nexus.warning")
        table.add_column("Subject ID", style="nexus.value")

        for subj_type, subj_id in sorted(subjects):
            table.add_row(subj_type, subj_id)

        console.print(table)

    except Exception as e:
        handle_error(e)


@rebac.command(name="explain")
@click.argument("subject_type", type=str)
@click.argument("subject_id", type=str)
@click.argument("permission", type=str)
@click.argument("object_type", type=str)
@click.argument("object_id", type=str)
@click.option("--verbose", "-v", is_flag=True, help="Show detailed path information")
@click.option(
    "--zone-id",
    type=str,
    default=None,
    help="Zone ID for multi-zone isolation (e.g., 'org_acme'). Can also be set via NEXUS_ZONE_ID env var.",
)
@add_backend_options
def rebac_explain_cmd(
    subject_type: str,
    subject_id: str,
    permission: str,
    object_type: str,
    object_id: str,
    verbose: bool,
    zone_id: str | None,
    remote_url: str | None,
    remote_api_key: str | None,
) -> None:
    """Explain why a subject has or doesn't have permission on an object.

    This command traces through the permission graph to show exactly why
    a permission check succeeded or failed. Useful for debugging access issues.

    Examples:
        # Why does alice have read permission on file123?
        nexus rebac explain agent alice read file file123

        # Why doesn't bob have write permission?
        nexus rebac explain agent bob write workspace main

        # Show detailed path information
        nexus rebac explain agent alice read file file123 --verbose

        # Explain within a specific zone
        nexus rebac explain agent alice read file file123 --zone-id org_acme
    """
    import asyncio

    asyncio.run(
        _async_rebac_explain_cmd(
            subject_type,
            subject_id,
            permission,
            object_type,
            object_id,
            verbose,
            zone_id,
            remote_url,
            remote_api_key,
        )
    )


async def _async_rebac_explain_cmd(
    subject_type: str,
    subject_id: str,
    permission: str,
    object_type: str,
    object_id: str,
    verbose: bool,
    zone_id: str | None,
    remote_url: str | None,
    remote_api_key: str | None,
) -> None:
    try:
        import json
        import os

        nx = await get_filesystem(remote_url, remote_api_key)

        # Resolve zone_id from option or environment
        resolved_zone_id = zone_id or os.getenv("NEXUS_ZONE_ID")

        # Get explanation
        explain_kwargs: dict[str, Any] = {
            "subject": (subject_type, subject_id),
            "permission": permission,
            "object": (object_type, object_id),
        }
        if resolved_zone_id:
            explain_kwargs["zone_id"] = resolved_zone_id

        rebac_svc = nx.service("rebac")
        assert rebac_svc is not None, "ReBAC service not available"
        explanation = rebac_svc.rebac_explain_sync(
            **explain_kwargs,
        )

        nx.close()

        # Display result header with metadata
        result = explanation["result"]
        cached = explanation["cached"]
        metadata = explanation.get("metadata", {})

        # Header
        if result:
            console.print("[nexus.success]✓ GRANTED[/nexus.success]")
        else:
            console.print("[nexus.error]✗ DENIED[/nexus.error]")

        # Metadata
        console.print()
        console.print(
            f"[nexus.muted]Subject:[/nexus.muted]      [nexus.warning]{subject_type}:{subject_id}[/nexus.warning]"
        )
        console.print(
            f"[nexus.muted]Permission:[/nexus.muted]   [nexus.identity]{permission}[/nexus.identity]"
        )
        console.print(
            f"[nexus.muted]Resource:[/nexus.muted]     [nexus.value]{object_type}:{object_id}[/nexus.value]"
        )
        if metadata:
            console.print(
                f"[nexus.muted]Timestamp:[/nexus.muted]   {metadata.get('timestamp', 'N/A')}"
            )
            console.print(
                f"[nexus.muted]Request ID:[/nexus.muted]  {metadata.get('request_id', 'N/A')}"
            )

        # Display schema rule if available
        console.print()
        successful_path = explanation.get("successful_path")
        if successful_path and "expanded_to" in successful_path:
            expanded = successful_path["expanded_to"]
            console.print(
                f"[bold]Schema Rule:[/bold] {permission}([nexus.value]{object_type}[/nexus.value]) = {' ∪ '.join(expanded)}"
            )

        # Display detailed explanation
        console.print()
        if cached:
            console.print("[bold]Reason:[/bold] Result from cache")
        elif successful_path:
            console.print("[bold]Proof:[/bold]")
            _display_proof_tree(successful_path, depth=0, step_number=[1])
            console.print()
            console.print("[bold]Conclusion:[/bold]")
            via = successful_path.get("via_userset") or successful_path.get("via_union_member")
            if via:
                console.print(
                    f"  {via}([nexus.value]{object_type}:{object_id}[/nexus.value]) = [nexus.success]true[/nexus.success] ⇒ {permission}(...) = [nexus.success]true[/nexus.success] ⇒ [nexus.success]GRANTED[/nexus.success]"
                )
            else:
                console.print(
                    f"  {permission}([nexus.value]{object_type}:{object_id}[/nexus.value]) = [nexus.success]true[/nexus.success] ⇒ [nexus.success]GRANTED[/nexus.success]"
                )
        elif not result:
            console.print("[bold]Reason:[/bold] No valid permission path found")
            paths = explanation.get("paths", [])
            if paths and verbose:
                console.print()
                console.print("[bold]Attempted paths:[/bold]")
                for i, path in enumerate(paths, 1):
                    console.print(
                        f"  {i}. {path.get('permission')} on {path.get('object')} - [nexus.error]NOT FOUND[/nexus.error]"
                    )
        else:
            console.print("[bold]Reason:[/bold] Permission granted (no path information)")

        # Display verbose info if requested
        if verbose:
            console.print()
            console.print("[bold]Full Path Details (JSON):[/bold]")
            console.print(json.dumps(explanation, indent=2, default=str))

    except Exception as e:
        handle_error(e)


def _format_tuple(tuple_info: dict) -> str:
    """Format a tuple for display.

    Args:
        tuple_info: Tuple dictionary with subject, relation, object

    Returns:
        Formatted tuple string
    """
    subj_type = tuple_info.get("subject_type", "?")
    subj_id = tuple_info.get("subject_id", "?")
    subj_rel = tuple_info.get("subject_relation")
    relation = tuple_info.get("relation", "?")
    obj_type = tuple_info.get("object_type", "?")
    obj_id = tuple_info.get("object_id", "?")

    if subj_rel:
        subject = f"({subj_type}, {subj_id}, {relation}, {subj_type}:{subj_id}#{subj_rel})"
    else:
        subject = f"{subj_type}:{subj_id}"

    return f"({subject}, {relation}, {obj_type}:{obj_id})"


def _display_proof_tree(path: dict, depth: int = 0, step_number: list[int] | None = None) -> None:
    """Display a proof tree for permission explanation.

    Args:
        path: Path dictionary from explain API
        depth: Current indentation depth
        step_number: Mutable list containing current step number
    """
    if step_number is None:
        step_number = [1]

    indent = "  " * depth
    permission = path.get("permission", "?")
    obj = path.get("object", "?")
    granted = path.get("granted", False)

    # Main check
    step = step_number[0]
    step_number[0] += 1
    console.print(
        f"{indent}{step}) Check [nexus.identity]{permission}[/nexus.identity]([nexus.value]{obj}[/nexus.value])"
    )

    # Show expansion
    if "expanded_to" in path:
        relations = path["expanded_to"]
        console.print(
            f"{indent}   → Expand to {{[nexus.identity]{', '.join(relations)}[/nexus.identity]}}"
        )

    # Show union/intersection
    if "union" in path:
        union_rels = path["union"]
        console.print(f"{indent}   Schema: {permission} = {' ∪ '.join(union_rels)}")

    # Check direct tuple
    if path.get("direct_relation") and "tuple" in path:
        tuple_info = path["tuple"]
        tuple_id = tuple_info.get("tuple_id", "N/A")
        tuple_str = _format_tuple(tuple_info)
        console.print(f"{indent}   → [nexus.success]FOUND[/nexus.success] tuple: {tuple_str}")
        console.print(f"{indent}   → Tuple ID: [nexus.muted]{tuple_id}[/nexus.muted]")
    elif path.get("direct_relation"):
        console.print(f"{indent}   → [nexus.success]FOUND[/nexus.success] direct relation")

    # Show parent traversal
    if "tupleToUserset" in path:
        ttu = path["tupleToUserset"]
        tupleset = ttu.get("tupleset", "?")
        computed = ttu.get("computedUserset", "?")
        console.print(f"{indent}   Rule: {permission}(f) = ∃ p. {tupleset}(f) = p ∧ {computed}(p)")

        found_parents = ttu.get("found_parents", [])
        if found_parents:
            for parent in found_parents:
                if isinstance(parent, list | tuple) and len(parent) >= 2:
                    parent_type, parent_id = parent[0], parent[1]
                    console.print(
                        f"{indent}   • {tupleset}([nexus.value]{obj}[/nexus.value]) = [nexus.value]{parent_type}:{parent_id}[/nexus.value]  [nexus.success]✓[/nexus.success]"
                    )
                else:
                    console.print(
                        f"{indent}   • {tupleset}([nexus.value]{obj}[/nexus.value]) = [nexus.value]{parent}[/nexus.value]  [nexus.success]✓[/nexus.success]"
                    )
        else:
            console.print(f"{indent}   • No parent found → [nexus.error]SKIPPED[/nexus.error]")

    # Show status
    if "error" in path:
        console.print(f"{indent}   → [nexus.error]ERROR:[/nexus.error] {path['error']}")
    elif not granted and not path.get("sub_paths"):
        console.print(f"{indent}   → [nexus.error]NOT FOUND[/nexus.error]")

    # Display sub-paths recursively
    if "sub_paths" in path:
        for sub_path in path["sub_paths"]:
            if sub_path.get("granted"):
                console.print()
                _display_proof_tree(sub_path, depth + 1, step_number)
                break  # Only show successful path


@rebac.command(name="check-batch")
@click.argument("checks_file", type=click.Path(exists=True))
@click.option(
    "--format",
    "output_format",
    type=click.Choice(["table", "json", "summary"]),
    default="table",
    help="Output format",
)
@add_backend_options
def rebac_check_batch_cmd(
    checks_file: str,
    output_format: str,
    remote_url: str | None,
    remote_api_key: str | None,
) -> None:
    """Batch permission checks from a JSON file.

    Efficiently check multiple permissions at once. Much faster than individual checks.

    The JSON file should contain an array of check objects:
    [
        {"subject": ["agent", "alice"], "permission": "read", "object": ["file", "f1"]},
        {"subject": ["agent", "bob"], "permission": "write", "object": ["file", "f2"]}
    ]

    Examples:
        # Create checks.json with multiple checks
        echo '[
            {"subject": ["agent", "alice"], "permission": "read", "object": ["file", "f1"]},
            {"subject": ["agent", "alice"], "permission": "write", "object": ["file", "f1"]},
            {"subject": ["agent", "bob"], "permission": "read", "object": ["file", "f2"]}
        ]' > checks.json

        # Run batch check
        nexus rebac check-batch checks.json

        # JSON output
        nexus rebac check-batch checks.json --format json

        # Summary only
        nexus rebac check-batch checks.json --format summary
    """
    import asyncio

    asyncio.run(
        _async_rebac_check_batch_cmd(checks_file, output_format, remote_url, remote_api_key)
    )


async def _async_rebac_check_batch_cmd(
    checks_file: str,
    output_format: str,
    remote_url: str | None,
    remote_api_key: str | None,
) -> None:
    try:
        import json

        nx = await get_filesystem(remote_url, remote_api_key)

        # Load checks from file
        with open(checks_file) as f:
            checks_data = json.load(f)

        if not isinstance(checks_data, list):
            console.print("[nexus.error]Error:[/nexus.error] Checks file must contain a JSON array")
            nx.close()
            sys.exit(1)

        # Convert to batch check format
        checks = []
        for i, check in enumerate(checks_data):
            try:
                subject = tuple(check["subject"])
                permission = check["permission"]
                obj = tuple(check["object"])
                checks.append((subject, permission, obj))
            except (KeyError, TypeError) as e:
                console.print(f"[nexus.error]Error:[/nexus.error] Invalid check at index {i}: {e}")
                nx.close()
                sys.exit(1)

        # Perform batch check with Rust acceleration
        console.print(
            f"[nexus.value]Checking {len(checks)} permissions (Rust acceleration enabled)...[/nexus.value]"
        )
        rebac_svc = nx.service("rebac")
        assert rebac_svc is not None, "ReBAC service not available"
        results = rebac_svc.rebac_check_batch_sync(checks)
        nx.close()

        # Output results
        if output_format == "json":
            # JSON output
            output = []
            for check, result in zip(checks_data, results, strict=False):
                output.append(
                    {
                        **check,
                        "result": result,
                    }
                )
            console.print(json.dumps(output, indent=2))

        elif output_format == "summary":
            # Summary only
            allowed_count = sum(results)
            denied_count = len(results) - allowed_count
            console.print(f"[nexus.success]✓ {allowed_count} allowed[/nexus.success]")
            console.print(f"[nexus.error]✗ {denied_count} denied[/nexus.error]")
            console.print(f"  Total: {len(results)}")

        else:
            # Table output (default)
            table = Table(title=f"Batch Check Results ({len(checks)} checks)")
            table.add_column("#", style="nexus.muted")
            table.add_column("Subject", style="nexus.warning")
            table.add_column("Permission", style="nexus.identity")
            table.add_column("Object", style="nexus.value")
            table.add_column("Result", style="bold")

            for i, ((subject, permission, obj), result) in enumerate(
                zip(checks, results, strict=False)
            ):
                result_text = (
                    "[nexus.success]✓ ALLOWED[/nexus.success]"
                    if result
                    else "[nexus.error]✗ DENIED[/nexus.error]"
                )
                table.add_row(
                    str(i + 1),
                    f"{subject[0]}:{subject[1]}",
                    permission,
                    f"{obj[0]}:{obj[1]}",
                    result_text,
                )

            console.print(table)

            # Summary
            allowed_count = sum(results)
            denied_count = len(results) - allowed_count
            console.print()
            console.print(
                f"Summary: [nexus.success]{allowed_count} allowed[/nexus.success], [nexus.error]{denied_count} denied[/nexus.error]"
            )

    except Exception as e:
        handle_error(e)


@rebac.command(name="namespace-create")
@click.argument("object_type", type=str)
@click.option(
    "--config-file", type=click.Path(exists=True), help="JSON/YAML file with namespace config"
)
@click.option(
    "--relations",
    type=str,
    multiple=True,
    help="Add relation (format: name or name:union:rel1,rel2)",
)
@click.option(
    "--permission",
    type=str,
    multiple=True,
    help="Add permission mapping (format: perm:rel1,rel2,rel3)",
)
@add_backend_options
def namespace_create(
    object_type: str,
    config_file: str | None,
    relations: tuple[str, ...],
    permission: tuple[str, ...],
    remote_url: str | None,
    remote_api_key: str | None,
) -> None:
    """Create or update a namespace configuration.

    Define custom object types with their relations and permissions.

    Examples:
        # From config file (JSON/YAML)
        nexus rebac namespace-create document --config-file document.json

        # Inline definition
        nexus rebac namespace-create project \\
          --relations owner --relations editor --relations viewer:union:editor,owner \\
          --permission read:viewer,editor,owner --permission write:editor,owner

        # Config file format (JSON):
        {
          "relations": {
            "owner": {},
            "editor": {},
            "viewer": {"union": ["editor", "owner"]}
          },
          "permissions": {
            "read": ["viewer", "editor", "owner"],
            "write": ["editor", "owner"]
          }
        }
    """
    import asyncio

    asyncio.run(
        _async_namespace_create(
            object_type, config_file, relations, permission, remote_url, remote_api_key
        )
    )


async def _async_namespace_create(
    object_type: str,
    config_file: str | None,
    relations: tuple[str, ...],
    permission: tuple[str, ...],
    remote_url: str | None,
    remote_api_key: str | None,
) -> None:
    try:
        import json

        import yaml

        nx = await get_filesystem(remote_url, remote_api_key)

        config = {}

        if config_file:
            # Load from file
            with open(config_file) as f:
                if config_file.endswith((".yaml", ".yml")):
                    config = yaml.safe_load(f)
                else:
                    config = json.load(f)
        else:
            # Build from CLI options
            config["relations"] = {}
            config["permissions"] = {}

            # Parse relations
            for rel in relations:
                if ":" in rel:
                    parts = rel.split(":")
                    name = parts[0]
                    op = parts[1]
                    values = parts[2].split(",") if len(parts) > 2 else []
                    config["relations"][name] = {op: values}
                else:
                    config["relations"][rel] = {}

            # Parse permissions
            for perm in permission:
                perm_name, rels = perm.split(":", 1)
                config["permissions"][perm_name] = rels.split(",")

        # Create namespace
        rebac_svc = nx.service("rebac")
        assert rebac_svc is not None, "ReBAC service not available"
        rebac_svc.namespace_create_sync(object_type=object_type, config=config)

        console.print(f"[nexus.success]✓[/nexus.success] Created namespace for '{object_type}'")

    except Exception as e:
        handle_error(e)


@rebac.command(name="namespace-list")
@click.option(
    "--format",
    "output_format",
    type=click.Choice(["table", "json"]),
    default="table",
    help="Output format",
)
@add_backend_options
def namespace_list(
    output_format: str,
    remote_url: str | None,
    remote_api_key: str | None,
) -> None:
    """List all registered namespace configurations.

    Examples:
        # List as table
        nexus rebac namespace-list

        # JSON output
        nexus rebac namespace-list --format json
    """
    import asyncio

    asyncio.run(_async_namespace_list(output_format, remote_url, remote_api_key))


async def _async_namespace_list(
    output_format: str,
    remote_url: str | None,
    remote_api_key: str | None,
) -> None:
    try:
        nx = await get_filesystem(remote_url, remote_api_key)

        rebac_svc = nx.service("rebac")
        assert rebac_svc is not None, "ReBAC service not available"
        namespaces = rebac_svc.namespace_list_sync()

        if output_format == "json":
            import json

            console.print(json.dumps(namespaces, indent=2))
        else:
            if not namespaces:
                console.print("[nexus.warning]No namespaces registered[/nexus.warning]")
                return

            table = Table(title="ReBAC Namespaces")
            table.add_column("Object Type", style="nexus.value")
            table.add_column("Relations", style="nexus.success")
            table.add_column("Permissions", style="nexus.identity")
            table.add_column("Created", style="nexus.muted")

            for ns in namespaces:
                relations = ", ".join(ns["config"]["relations"].keys())
                permissions = ", ".join(ns["config"]["permissions"].keys())
                created = (
                    ns["created_at"][:19]
                    if isinstance(ns["created_at"], str)
                    else str(ns["created_at"])[:19]
                )

                table.add_row(
                    ns["object_type"],
                    relations,
                    permissions,
                    created,
                )

            console.print(table)

    except Exception as e:
        handle_error(e)


@rebac.command(name="namespace-get")
@click.argument("object_type", type=str)
@click.option(
    "--format",
    "output_format",
    type=click.Choice(["yaml", "json"]),
    default="yaml",
    help="Output format",
)
@add_backend_options
def namespace_get(
    object_type: str,
    output_format: str,
    remote_url: str | None,
    remote_api_key: str | None,
) -> None:
    """Get namespace configuration for an object type.

    Examples:
        # View file namespace
        nexus rebac namespace-get file

        # JSON output
        nexus rebac namespace-get group --format json
    """
    import asyncio

    asyncio.run(_async_namespace_get(object_type, output_format, remote_url, remote_api_key))


async def _async_namespace_get(
    object_type: str,
    output_format: str,
    remote_url: str | None,
    remote_api_key: str | None,
) -> None:
    try:
        import json

        import yaml

        nx = await get_filesystem(remote_url, remote_api_key)

        rebac_svc = nx.service("rebac")
        assert rebac_svc is not None, "ReBAC service not available"
        ns = rebac_svc.get_namespace_sync(object_type)

        if ns is None:
            console.print(f"[nexus.error]✗[/nexus.error] Namespace '{object_type}' not found")
            sys.exit(1)

        if output_format == "json":
            console.print(json.dumps(ns, indent=2))
        else:
            console.print(yaml.dump(ns, default_flow_style=False))

    except Exception as e:
        handle_error(e)


@rebac.command(name="namespace-delete")
@click.argument("object_type", type=str)
@click.option("--yes", is_flag=True, help="Skip confirmation prompt")
@add_backend_options
def namespace_delete(
    object_type: str,
    yes: bool,
    remote_url: str | None,
    remote_api_key: str | None,
) -> None:
    """Delete a namespace configuration.

    WARNING: This does not delete existing tuples for this object type.

    Examples:
        # Delete with confirmation
        nexus rebac namespace-delete document

        # Skip confirmation
        nexus rebac namespace-delete document --yes
    """
    import asyncio

    asyncio.run(_async_namespace_delete(object_type, yes, remote_url, remote_api_key))


async def _async_namespace_delete(
    object_type: str,
    yes: bool,
    remote_url: str | None,
    remote_api_key: str | None,
) -> None:
    try:
        if not yes:
            confirm = input(f"Delete namespace '{object_type}'? (y/N): ")
            if confirm.lower() != "y":
                console.print("[nexus.warning]Cancelled[/nexus.warning]")
                return

        nx = await get_filesystem(remote_url, remote_api_key)

        rebac_svc = nx.service("rebac")
        assert rebac_svc is not None, "ReBAC service not available"
        deleted = rebac_svc.namespace_delete_sync(object_type)

        if deleted:
            console.print(f"[nexus.success]✓[/nexus.success] Deleted namespace '{object_type}'")
        else:
            console.print(f"[nexus.error]✗[/nexus.error] Namespace '{object_type}' not found")
            sys.exit(1)

    except Exception as e:
        handle_error(e)


def register_commands(cli: click.Group) -> None:
    """Register ReBAC commands with the CLI.

    Args:
        cli: The Click CLI group to register commands with
    """
    cli.add_command(rebac)
