"""Django admin registrations for the mcp_server app.

Three surfaces are exposed:

* :class:`UserTokenAdmin` — read/edit opaque bearer tokens. Token
  creation still goes through the self-service dashboard so the
  plaintext can be shown exactly once; admin gets a filtered
  ``allowed_libraries`` picker so operators can scope an existing
  token without leaving the admin.
* :class:`TeamAdmin` + :class:`TeamWorkspaceAssignmentInline` — read /
  soft-delete / rotate / reattach Pallas teams. Creation usually comes
  from Daedalus (``POST /mcp_server/api/teams/``) but the admin is the
  break-glass path when Daedalus is offline.
* :class:`LibraryMembershipAdmin` — manage who can grant each
  Neo4j-resident Library into a ``UserToken.allowed_libraries``.

See ``docs/DAEDALUS_PALLAS_INTEGRATION_v1.md`` for the overall model.
"""

from __future__ import annotations

from django import forms
from django.contrib import admin, messages

from .models import (
    LibraryMembership,
    MCPSigningKey,
    Team,
    TeamWorkspaceAssignment,
    UserToken,
)
from .teams import TeamJWTError, mint_team_jwt


# ---------------------------------------------------------------------------
# Helpers for the Library picker
# ---------------------------------------------------------------------------


def _library_choices_for_user(user) -> list[tuple[str, str]]:
    """Return ``[(uid, label), …]`` of Libraries the user may grant.

    A user may grant a Library into a token's ``allowed_libraries`` iff
    they hold ``owner`` or ``manager`` membership on it (see §4.1 of the
    design doc). Superusers bypass the filter so they can mint tokens on
    behalf of any principal.

    The label is ``"<name> [<library_type>]"`` when the Library node
    exists in Neo4j at render time, otherwise the bare UID — the
    membership row can legitimately outlive the Neo4j node during an
    unclean delete, and we shouldn't crash the admin in that case.
    """
    if user is None:
        return []

    if getattr(user, "is_superuser", False):
        uids = set(_neo4j_library_map().keys())
    else:
        uids = set(
            LibraryMembership.objects
            .filter(
                user=user,
                role__in=(
                    LibraryMembership.Role.OWNER,
                    LibraryMembership.Role.MANAGER,
                ),
            )
            .values_list("library_uid", flat=True)
        )

    labels = _neo4j_library_map()
    return sorted(
        [(uid, labels.get(uid, uid)) for uid in uids],
        key=lambda pair: pair[1].lower(),
    )


def _neo4j_library_map() -> dict[str, str]:
    """``{uid: display_label}`` for every Neo4j ``Library`` node.

    Fails softly if Neo4j is unreachable — the admin page still renders
    with whatever membership rows exist, using the raw UIDs as labels.
    """
    try:
        from library.models import Library

        out: dict[str, str] = {}
        for lib in Library.nodes.all():
            uid = getattr(lib, "uid", None)
            if not uid:
                continue
            name = getattr(lib, "name", uid)
            lib_type = getattr(lib, "library_type", "")
            out[uid] = f"{name} [{lib_type}]" if lib_type else name
        return out
    except Exception:  # pragma: no cover - Neo4j offline paths
        return {}


class _LibraryPickerField(forms.MultipleChoiceField):
    """``MultipleChoiceField`` whose choices are rebuilt per-request.

    Django ``ModelForm`` instantiates fields at class-definition time,
    but the set of grantable libraries depends on the request user.
    We override :meth:`_bound_choices` indirectly by setting
    ``self.choices`` in :meth:`UserTokenAdminForm.__init__`.
    """

    def __init__(self, *args, **kwargs):
        kwargs.setdefault("required", False)
        kwargs.setdefault(
            "help_text",
            "Libraries this token may read. Empty = zero libraries (fail-closed).",
        )
        kwargs.setdefault("widget", forms.CheckboxSelectMultiple)
        super().__init__(*args, choices=[], **kwargs)


class UserTokenAdminForm(forms.ModelForm):
    """``UserToken`` admin form with a membership-filtered library picker.

    The underlying field is a ``JSONField(list)``; the form substitutes
    a checkbox multi-select that writes the same JSON list shape. We
    bind the choices against the *token's* user rather than the admin
    viewer — that matches the scoping rule in the design doc (whether
    the GRANTEE may hold these libraries, not whether the admin can).
    """

    allowed_libraries = _LibraryPickerField()

    class Meta:
        model = UserToken
        fields = [
            "user",
            "name",
            "is_active",
            "expires_at",
            "allowed_tools",
            "allowed_libraries",
        ]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        user = None
        if self.instance and self.instance.pk:
            user = self.instance.user
        elif self.initial.get("user"):
            user = self.initial.get("user")
        self.fields["allowed_libraries"].choices = _library_choices_for_user(user)

        if self.instance and self.instance.pk:
            self.fields["allowed_libraries"].initial = list(
                self.instance.allowed_libraries or []
            )

    def clean_allowed_libraries(self):
        # Multi-choice returns a list; we want it verbatim as JSON.
        value = self.cleaned_data.get("allowed_libraries") or []
        return list(value)


@admin.register(UserToken)
class UserTokenAdmin(admin.ModelAdmin):
    form = UserTokenAdminForm

    list_display = [
        "name",
        "user",
        "is_active",
        "masked_token",
        "library_count",
        "expires_at",
        "last_used_at",
        "created_at",
    ]
    list_filter = ["is_active"]
    search_fields = ["name", "user__email", "user__username"]
    readonly_fields = ["token_hash", "last_used_at", "created_at", "updated_at"]
    fieldsets = (
        (None, {"fields": ("user", "name", "is_active")}),
        (
            "Scope",
            {
                "fields": ("allowed_tools", "allowed_libraries", "expires_at"),
                "description": (
                    "``allowed_libraries`` is fail-closed: empty = the token "
                    "can read no libraries. Picker shows libraries the token's "
                    "user has owner/manager membership on."
                ),
            },
        ),
        (
            "Token (hashed at rest — plaintext is shown only once at creation)",
            {"fields": ("token_hash",)},
        ),
        ("Audit", {"fields": ("last_used_at", "created_at", "updated_at")}),
    )

    @admin.display(description="Token")
    def masked_token(self, obj):
        return obj.get_masked_token()

    @admin.display(description="Libraries")
    def library_count(self, obj):
        return len(obj.allowed_libraries or [])

    def has_add_permission(self, request):
        # Tokens must be created via the dashboard or management command
        # so the plaintext can be surfaced to the user. Adding via admin
        # would persist a hash with no plaintext ever shown.
        return False


# ---------------------------------------------------------------------------
# MCPSigningKey
# ---------------------------------------------------------------------------


@admin.register(MCPSigningKey)
class MCPSigningKeyAdmin(admin.ModelAdmin):
    list_display = ["kid", "is_active", "created_at", "retired_at", "note"]
    list_filter = ["is_active"]
    search_fields = ["kid", "note"]
    readonly_fields = ["created_at", "retired_at"]
    fields = ["kid", "secret_hex", "is_active", "note", "created_at", "retired_at"]


# ---------------------------------------------------------------------------
# LibraryMembership
# ---------------------------------------------------------------------------


@admin.register(LibraryMembership)
class LibraryMembershipAdmin(admin.ModelAdmin):
    list_display = ["user", "library_uid", "role", "created_at"]
    list_filter = ["role"]
    search_fields = ["user__username", "user__email", "library_uid"]
    autocomplete_fields = ["user"]
    readonly_fields = ["created_at"]


# ---------------------------------------------------------------------------
# Team + TeamWorkspaceAssignment
# ---------------------------------------------------------------------------


class TeamWorkspaceAssignmentInline(admin.TabularInline):
    model = TeamWorkspaceAssignment
    extra = 0
    readonly_fields = ["created_at"]
    fields = ["workspace_id", "created_at"]


@admin.register(Team)
class TeamAdmin(admin.ModelAdmin):
    list_display = ["name", "id", "active", "workspace_count", "updated_at"]
    list_filter = ["active"]
    search_fields = ["name", "id"]
    readonly_fields = [
        "id",
        "active_jti",
        "created_at",
        "updated_at",
    ]
    fields = [
        "id",
        "name",
        "active",
        "active_jti",
        "created_at",
        "updated_at",
    ]
    inlines = [TeamWorkspaceAssignmentInline]
    actions = ["action_rotate_jwt", "action_deactivate"]

    @admin.display(description="Workspaces")
    def workspace_count(self, obj: Team) -> int:
        return obj.workspace_assignments.count()

    @admin.action(description="Rotate JWT (mint new, reveal once)")
    def action_rotate_jwt(self, request, queryset):
        revealed = []
        for team in queryset:
            if not team.active:
                self.message_user(
                    request,
                    f"Skipped inactive team {team.name}",
                    level=messages.WARNING,
                )
                continue
            team.rotate_jti()
            try:
                jwt_string = mint_team_jwt(team)
            except TeamJWTError as exc:
                self.message_user(
                    request,
                    f"Failed to mint JWT for {team.name}: {exc}",
                    level=messages.ERROR,
                )
                continue
            revealed.append((team.name, jwt_string))

        for name, jwt_string in revealed:
            # Messages are surfaced in the admin banner. Operator is
            # expected to copy the JWT immediately — there is no retrieval
            # path afterward.
            self.message_user(
                request,
                f"{name}: {jwt_string}",
                level=messages.SUCCESS,
            )

    @admin.action(description="Deactivate (soft-delete)")
    def action_deactivate(self, request, queryset):
        for team in queryset:
            team.deactivate()
        self.message_user(
            request,
            f"Deactivated {queryset.count()} team(s). Their JWTs are now invalid.",
            level=messages.SUCCESS,
        )


@admin.register(TeamWorkspaceAssignment)
class TeamWorkspaceAssignmentAdmin(admin.ModelAdmin):
    list_display = ["team", "workspace_id", "created_at"]
    list_filter = ["team"]
    search_fields = ["team__name", "workspace_id"]
    readonly_fields = ["created_at"]
