"""REST endpoints for the Mnemosyne ↔ Daedalus team control plane.

Mounted under ``/mcp_server/api/teams/`` — see §7 of
``docs/DAEDALUS_PALLAS_INTEGRATION_v1.md``. Every endpoint is
``IsAuthenticated``-gated and scoped to ``request.user``: the user that
created a team via ``POST /`` is the only user that can read, mutate,
or rotate it. Non-owners receive 404 (not 403) so a team's existence
isn't disclosed across users. Endpoints are designed to be idempotent
where possible:

* ``POST /``                — create a team by UUID; a second POST with
                              the same id returns 200 without a new JWT.
* ``DELETE /{id}/``        — soft-delete (``active=False``). 204 even if
                              the team was already inactive.
* ``PUT /{id}/workspaces/`` — replace the team's workspace assignment
                              set; idempotent.
* ``POST /{id}/rotate/``    — generate a new ``active_jti`` and sign a
                              fresh JWT. Old JWT invalid immediately.
* ``GET /{id}/``           — read-only detail (no JWT).

All mutating endpoints log an audit line at INFO — these are the audit
points ``docs/DAEDALUS_PALLAS_INTEGRATION_v1.md`` §13.4 refers to.
"""

from __future__ import annotations

import logging

from django.db import transaction
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

from ..models import Team, TeamWorkspaceAssignment
from ..teams import TeamJWTError, mint_team_jwt
from .serializers import (
    TeamCreateRequestSerializer,
    TeamPublicSerializer,
    TeamWorkspacesUpdateSerializer,
)

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _get_team(team_id, user):
    """Load a team by UUID *for this user* or None.

    Returns None both when the team does not exist and when it exists
    but is owned by another user. Callers 404 on None — they must not
    distinguish the two cases.
    """
    return Team.objects.filter(pk=team_id, owner=user).first()


def _mint_with_fresh_jti(team: Team) -> str:
    """Rotate the team's jti and return a freshly-signed JWT."""
    team.rotate_jti()
    return mint_team_jwt(team)


# ---------------------------------------------------------------------------
# POST /mcp_server/api/teams/
# ---------------------------------------------------------------------------


@api_view(["POST"])
@permission_classes([IsAuthenticated])
def team_create(request):
    """Create a team. Idempotent on ``id``.

    * Fresh id → 201 with ``jwt`` surfaced exactly once.
    * Existing id → 200 with the team's current state, **no** ``jwt``.
      To get a new JWT for an existing team, call ``/rotate/``.
    """
    serializer = TeamCreateRequestSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    data = serializer.validated_data

    existing = Team.objects.filter(pk=data["id"]).first()
    if existing is not None:
        if existing.owner_id != request.user.id:
            # Same id, different owner. Surfacing existence would leak
            # one user's team id to another; treat as a generic conflict.
            logger.warning(
                "team_create owner_conflict team_id=%s caller=%s",
                existing.id, request.user.id,
            )
            return Response(
                {"detail": "Team id is already in use."},
                status=status.HTTP_409_CONFLICT,
            )
        # Idempotent: surface current state without a new JWT. If the
        # caller wants to reactivate a soft-deleted team they must do
        # so explicitly via the admin or a future endpoint; re-POST is
        # not "undelete" because that would silently return a new JWT
        # on every retry storm.
        logger.info(
            "team_create idempotent_hit team_id=%s name=%s active=%s",
            existing.id, existing.name, existing.active,
        )
        return Response(
            TeamPublicSerializer(existing).data, status=status.HTTP_200_OK
        )

    try:
        with transaction.atomic():
            team = Team.objects.create(
                id=data["id"], name=data["name"], owner=request.user
            )
            jwt_string = _mint_with_fresh_jti(team)
    except TeamJWTError as exc:
        # Rolling back the create is fine — we have no signing key yet
        # and cannot hand the caller a usable credential.
        logger.error(
            "team_create mint_failed team_id=%s reason=%s",
            data["id"], exc,
        )
        return Response(
            {"detail": str(exc)},
            status=status.HTTP_503_SERVICE_UNAVAILABLE,
        )

    logger.info(
        "team_create created team_id=%s name=%s jti=%s",
        team.id, team.name, team.active_jti,
    )
    payload = TeamPublicSerializer(team).data
    payload["jwt"] = jwt_string
    return Response(payload, status=status.HTTP_201_CREATED)


# ---------------------------------------------------------------------------
# DELETE /mcp_server/api/teams/{id}/   +   GET /mcp_server/api/teams/{id}/
# ---------------------------------------------------------------------------


@api_view(["GET", "DELETE"])
@permission_classes([IsAuthenticated])
def team_detail(request, team_id):
    """Soft-delete the team (``DELETE``) or read its state (``GET``).

    The DELETE path sets ``active=False`` and clears ``active_jti`` so
    any token still in use is rejected on the next request. Row stays
    in the database for audit; call POST ``/`` with the same id to
    re-materialize a fresh team if needed (operator decision).
    """
    team = _get_team(team_id, request.user)
    if team is None:
        return Response(
            {"detail": "Team not found."}, status=status.HTTP_404_NOT_FOUND
        )

    if request.method == "GET":
        return Response(TeamPublicSerializer(team).data)

    # DELETE
    was_active = team.active
    team.deactivate()
    logger.info(
        "team_delete team_id=%s was_active=%s",
        team.id, was_active,
    )
    return Response(status=status.HTTP_204_NO_CONTENT)


# ---------------------------------------------------------------------------
# PUT /mcp_server/api/teams/{id}/workspaces/
# ---------------------------------------------------------------------------


@api_view(["PUT"])
@permission_classes([IsAuthenticated])
def team_workspaces(request, team_id):
    """Replace the team's workspace assignment set.

    Idempotent: the stored set equals ``workspace_ids`` after the call.
    Diff is computed in-DB so we don't thrash rows that already match —
    only the net-add and net-remove rows are touched.
    """
    team = _get_team(team_id, request.user)
    if team is None:
        return Response(
            {"detail": "Team not found."}, status=status.HTTP_404_NOT_FOUND
        )

    serializer = TeamWorkspacesUpdateSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    target = set(serializer.validated_data["workspace_ids"])

    with transaction.atomic():
        current = set(
            team.workspace_assignments.values_list("workspace_id", flat=True)
        )
        to_add = target - current
        to_remove = current - target

        if to_remove:
            team.workspace_assignments.filter(
                workspace_id__in=to_remove
            ).delete()
        if to_add:
            TeamWorkspaceAssignment.objects.bulk_create(
                [
                    TeamWorkspaceAssignment(team=team, workspace_id=ws)
                    for ws in to_add
                ],
                ignore_conflicts=True,
            )

    logger.info(
        "team_workspaces_updated team_id=%s added=%d removed=%d total=%d",
        team.id, len(to_add), len(to_remove), len(target),
    )
    return Response(
        {"workspace_ids": sorted(target)},
        status=status.HTTP_200_OK,
    )


# ---------------------------------------------------------------------------
# POST /mcp_server/api/teams/{id}/rotate/
# ---------------------------------------------------------------------------


@api_view(["POST"])
@permission_classes([IsAuthenticated])
def team_rotate(request, team_id):
    """Generate a fresh ``active_jti`` and JWT.

    Upsert-on-missing: if no ``Team`` exists for this id, create one
    owned by the caller and mint its first JWT. This eliminates the
    ordering trap where Daedalus calls rotate before its provisioning
    flow has POSTed the team — the operator clicks "Rotate JWT" and
    things just work. The placeholder ``name`` is the team id; an
    operator can rename later via admin or the create endpoint.

    A pre-existing team owned by a *different* user returns 409 (same
    shape as ``team_create``'s collision branch) — never disclose
    cross-user existence.

    The previously-issued JWT stops validating immediately — the auth
    middleware compares the incoming ``jti`` against ``Team.active_jti``
    on every request.

    Does NOT reactivate a soft-deleted team: if ``active=False``,
    returns 409 so the operator is forced to go through the explicit
    create/readd flow rather than quietly resurrecting a team.
    """
    team = Team.objects.filter(pk=team_id).first()
    if team is None:
        team = Team.objects.create(
            id=team_id,
            name=str(team_id),
            owner=request.user,
        )
        logger.info(
            "team_rotate upserted_missing team_id=%s owner=%s",
            team.id, request.user.username,
        )
    elif team.owner_id != request.user.id:
        logger.warning(
            "team_rotate owner_conflict team_id=%s caller=%s",
            team.id, request.user.id,
        )
        return Response(
            {"detail": "Team id is already in use."},
            status=status.HTTP_409_CONFLICT,
        )
    elif not team.active:
        return Response(
            {"detail": "Team is inactive; cannot rotate. Recreate it instead."},
            status=status.HTTP_409_CONFLICT,
        )

    try:
        jwt_string = _mint_with_fresh_jti(team)
    except TeamJWTError as exc:
        logger.error(
            "team_rotate mint_failed team_id=%s reason=%s",
            team.id, exc,
        )
        return Response(
            {"detail": str(exc)},
            status=status.HTTP_503_SERVICE_UNAVAILABLE,
        )

    logger.info(
        "team_rotate team_id=%s new_jti=%s",
        team.id, team.active_jti,
    )
    return Response({"jwt": jwt_string}, status=status.HTTP_200_OK)
