"""Chat API views.

Endpoints (DM):
  GET  /chat/users/                                            — other tenant members
  GET  /chat/conversations/                                    — current user's DM conversations
  GET  /chat/conversations/with/<uid>/                         — get-or-create DM + messages
  POST /chat/conversations/<id>/messages/                      — send DM (text or file)
  POST /chat/messages/<id>/read/                               — mark DM message read
  PATCH|DELETE /chat/conversations/<conv_id>/messages/<msg_id>/ — edit / delete DM

Endpoints (Groups / Channels):
  GET  /chat/groups/                                           — list groups/channels I'm in
  POST /chat/groups/                                           — create group or channel
  GET  /chat/groups/<id>/                                      — group detail + messages
  POST /chat/groups/<id>/messages/                             — post to group/channel
  PATCH|DELETE /chat/groups/<group_id>/messages/<msg_id>/      — edit / delete group message
"""

from __future__ import annotations

import io

from django.contrib.auth import get_user_model
from django.utils import timezone
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response

from simorgh.apps.memberships.models import Membership, MembershipStatus

from .models import (
    DirectConversation,
    DirectMessage,
    GroupConversation,
    GroupMember,
    GroupMessage,
)
from .serializers import (
    AttachmentSerializer,
    ChatUserSerializer,
    ConversationSerializer,
    DirectMessageSerializer,
    GroupConversationSerializer,
    GroupMemberSerializer,
    GroupMessageSerializer,
)

User = get_user_model()


def _require_tenant(request: Request) -> object:
    tenant = getattr(request, "tenant", None)
    if tenant is None:
        from simorgh.apps.tenants.resolver import resolve_tenant
        tenant = resolve_tenant(request)
        if tenant is not None:
            request.tenant = tenant
    if tenant is None:
        from rest_framework.exceptions import PermissionDenied
        raise PermissionDenied("tenant required (set X-Tenant header)")
    return tenant


def _upload_chat_file(request: Request, tenant) -> tuple[str | None, Response | None]:
    """Validate and persist the uploaded file via the central storage service.

    Returns ``(attachment_id, None)`` on success or ``(None, error_response)``
    on failure.
    """
    uploaded_file = request.FILES.get("file")
    if not uploaded_file:
        return None, None

    from simorgh.apps.organizations.models import OrganizationNode
    from simorgh.apps.storage.providers import StorageError
    from simorgh.apps.storage.services import get_or_create_system_folder, store_file

    root_node = (
        OrganizationNode.objects.filter(tenant=tenant, parent__isnull=True)
        .order_by("depth", "pk")
        .first()
    )
    if root_node is None:
        return None, Response(
            {"detail": "No organization node found for this tenant."},
            status=status.HTTP_400_BAD_REQUEST,
        )

    chat_folder = get_or_create_system_folder(tenant.pk, "Chat")

    try:
        meta = store_file(
            filename=uploaded_file.name,
            content=io.BytesIO(uploaded_file.read()),
            content_type=uploaded_file.content_type or "",
            tenant_id=tenant.pk,
            organization_node_id=root_node.pk,
            uploaded_by_id=request.user.pk,
            folder_id=str(chat_folder.pk),
            app_context="chat",
        )
    except StorageError as exc:
        return None, Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)

    return str(meta.pk), None


# ---------------------------------------------------------------------------
# Users
# ---------------------------------------------------------------------------


@api_view(["GET"])
@permission_classes([IsAuthenticated])
def users_list(request: Request) -> Response:
    """Other members of the same tenant."""
    tenant = _require_tenant(request)
    user_ids = (
        Membership.objects.filter(tenant=tenant, status=MembershipStatus.ACTIVE)
        .values_list("users", flat=True)
        .distinct()
    )
    users = (
        User.objects.filter(pk__in=user_ids)
        .exclude(pk=request.user.pk)
        .order_by("first_name", "last_name", "email")
    )
    return Response(ChatUserSerializer(users, many=True).data)


# ---------------------------------------------------------------------------
# Conversations
# ---------------------------------------------------------------------------


@api_view(["GET"])
@permission_classes([IsAuthenticated])
def conversations_list(request: Request) -> Response:
    tenant = _require_tenant(request)
    me = request.user.pk
    convs = (
        DirectConversation.objects.filter(tenant=tenant)
        .filter(user1_id=me) | DirectConversation.objects.filter(tenant=tenant).filter(user2_id=me)
    ).order_by("-last_message_at", "-created_at").select_related("user1", "user2").distinct()
    return Response(ConversationSerializer(convs, many=True, context={"request": request}).data)


@api_view(["GET"])
@permission_classes([IsAuthenticated])
def conversation_with_user(request: Request, user_id: int) -> Response:
    """Get-or-create a DM conversation and return its messages."""
    tenant = _require_tenant(request)
    me = request.user.pk

    # Make sure the other user is a member of this tenant
    if not User.objects.filter(pk=user_id).exists():
        return Response({"detail": "User not found."}, status=status.HTTP_404_NOT_FOUND)

    conv, _ = DirectConversation.get_or_create_between(tenant.pk, me, user_id)

    # Mark all unread messages from the other side as read
    DirectMessage.objects.filter(
        conversation=conv, is_read=False
    ).exclude(sender_id=me).update(is_read=True)

    messages_qs = conv.messages.select_related("sender", "attachment").order_by("created_at")
    return Response(
        {
            "conversation_id": conv.pk,
            "messages": DirectMessageSerializer(messages_qs, many=True, context={"request": request}).data,
        }
    )


@api_view(["POST"])
@permission_classes([IsAuthenticated])
def send_message(request: Request, conversation_id: int) -> Response:
    """Send a message to an existing conversation.

    Accepts multipart/form-data (file upload) or JSON:
      body        — optional text (required if no file)
      attachment_id — optional UUID of a previously uploaded FileMetadata
      reply_to    — optional message ID to reply to
    """
    tenant = _require_tenant(request)
    me = request.user.pk

    try:
        conv = DirectConversation.objects.get(pk=conversation_id, tenant=tenant)
    except DirectConversation.DoesNotExist:
        return Response({"detail": "Conversation not found."}, status=status.HTTP_404_NOT_FOUND)

    if conv.user1_id != me and conv.user2_id != me:
        return Response({"detail": "Forbidden."}, status=status.HTTP_403_FORBIDDEN)

    body = (request.data.get("body") or "").strip()
    attachment_id = request.data.get("attachment_id") or None
    reply_to_id = request.data.get("reply_to") or None

    # Handle inline file upload (multipart)
    if request.FILES.get("file") and not attachment_id:
        attachment_id, err = _upload_chat_file(request, tenant)
        if err is not None:
            return err

    if not body and not attachment_id:
        return Response({"detail": "body or file is required."}, status=status.HTTP_400_BAD_REQUEST)

    attachment = None
    if attachment_id:
        from simorgh.apps.storage.models import FileMetadata
        try:
            attachment = FileMetadata.objects.get(pk=attachment_id, tenant=tenant)
        except FileMetadata.DoesNotExist:
            return Response({"detail": "Attachment not found."}, status=status.HTTP_404_NOT_FOUND)

    # Validate reply_to
    reply_to = None
    if reply_to_id:
        try:
            reply_to = DirectMessage.objects.get(pk=reply_to_id, conversation=conv)
        except DirectMessage.DoesNotExist:
            return Response({"detail": "Reply target message not found."}, status=status.HTTP_404_NOT_FOUND)

    msg = DirectMessage.objects.create(
        conversation=conv,
        sender_id=me,
        body=body,
        attachment=attachment,
        reply_to=reply_to,
    )
    conv.last_message_at = msg.created_at
    conv.save(update_fields=("last_message_at",))
    return Response(
        DirectMessageSerializer(msg, context={"request": request}).data,
        status=status.HTTP_201_CREATED,
    )


@api_view(["POST"])
@permission_classes([IsAuthenticated])
def mark_read(request: Request, message_id: int) -> Response:
    me = request.user.pk
    try:
        msg = DirectMessage.objects.select_related("conversation").get(pk=message_id)
    except DirectMessage.DoesNotExist:
        return Response(status=status.HTTP_404_NOT_FOUND)
    conv = msg.conversation
    if conv.user1_id != me and conv.user2_id != me:
        return Response(status=status.HTTP_403_FORBIDDEN)
    if not msg.is_read and msg.sender_id != me:
        msg.is_read = True
        msg.save(update_fields=("is_read",))
    return Response(status=status.HTTP_204_NO_CONTENT)


# ---------------------------------------------------------------------------
# Groups & Channels
# ---------------------------------------------------------------------------


@api_view(["GET", "POST"])
@permission_classes([IsAuthenticated])
def groups_list_create(request: Request) -> Response:
    """List groups/channels the current user belongs to, or create a new one."""
    tenant = _require_tenant(request)
    me = request.user.pk

    if request.method == "GET":
        kind = request.query_params.get("kind")  # "group" | "channel" | absent
        qs = (
            GroupConversation.objects.filter(tenant=tenant, members__user_id=me)
            .order_by("-last_message_at", "-created_at")
            .distinct()
        )
        if kind in ("group", "channel"):
            qs = qs.filter(kind=kind)
        return Response(
            GroupConversationSerializer(qs, many=True, context={"request": request}).data
        )

    # POST — create
    name = (request.data.get("name") or "").strip()
    if not name:
        return Response({"detail": "name is required."}, status=status.HTTP_400_BAD_REQUEST)

    kind = request.data.get("kind", "group")
    if kind not in ("group", "channel"):
        return Response(
            {"detail": "kind must be 'group' or 'channel'."},
            status=status.HTTP_400_BAD_REQUEST,
        )

    description = (request.data.get("description") or "").strip()
    member_ids = list(request.data.get("member_ids") or [])

    group = GroupConversation.objects.create(
        tenant=tenant,
        name=name,
        kind=kind,
        description=description,
        created_by_id=me,
    )
    GroupMember.objects.create(group=group, user_id=me, role="admin")
    for uid in member_ids:
        try:
            uid_int = int(uid)
        except (TypeError, ValueError):
            continue
        if uid_int != me:
            GroupMember.objects.get_or_create(
                group=group,
                user_id=uid_int,
                defaults={"role": "member"},
            )

    return Response(
        GroupConversationSerializer(group, context={"request": request}).data,
        status=status.HTTP_201_CREATED,
    )


@api_view(["GET"])
@permission_classes([IsAuthenticated])
def group_detail(request: Request, group_id: int) -> Response:
    """Return group metadata + messages, and mark messages as read for the caller."""
    tenant = _require_tenant(request)
    me = request.user.pk

    try:
        group = GroupConversation.objects.get(pk=group_id, tenant=tenant)
    except GroupConversation.DoesNotExist:
        return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)

    if not group.members.filter(user_id=me).exists():
        return Response({"detail": "Forbidden."}, status=status.HTTP_403_FORBIDDEN)

    # Mark as read
    GroupMember.objects.filter(group=group, user_id=me).update(last_read_at=timezone.now())

    messages_qs = group.messages.select_related("sender", "attachment").order_by("created_at")
    return Response({
        "group": GroupConversationSerializer(group, context={"request": request}).data,
        "messages": GroupMessageSerializer(messages_qs, many=True, context={"request": request}).data,
    })


@api_view(["POST"])
@permission_classes([IsAuthenticated])
def send_group_message(request: Request, group_id: int) -> Response:
    """Post a message to a group or channel."""
    tenant = _require_tenant(request)
    me = request.user.pk

    try:
        group = GroupConversation.objects.get(pk=group_id, tenant=tenant)
    except GroupConversation.DoesNotExist:
        return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)

    membership = group.members.filter(user_id=me).first()
    if not membership:
        return Response({"detail": "Forbidden."}, status=status.HTTP_403_FORBIDDEN)

    # Channel: only admins may post
    if group.kind == "channel" and membership.role != "admin":
        return Response(
            {"detail": "Only admins can post in channels."},
            status=status.HTTP_403_FORBIDDEN,
        )

    body = (request.data.get("body") or "").strip()
    attachment_id = request.data.get("attachment_id") or None
    reply_to_id = request.data.get("reply_to") or None

    if request.FILES.get("file") and not attachment_id:
        attachment_id, err = _upload_chat_file(request, tenant)
        if err is not None:
            return err

    if not body and not attachment_id:
        return Response(
            {"detail": "body or file is required."},
            status=status.HTTP_400_BAD_REQUEST,
        )

    attachment = None
    if attachment_id:
        from simorgh.apps.storage.models import FileMetadata
        try:
            attachment = FileMetadata.objects.get(pk=attachment_id, tenant=tenant)
        except FileMetadata.DoesNotExist:
            return Response({"detail": "Attachment not found."}, status=status.HTTP_404_NOT_FOUND)

    # Validate reply_to
    reply_to = None
    if reply_to_id:
        try:
            reply_to = GroupMessage.objects.get(pk=reply_to_id, group=group)
        except GroupMessage.DoesNotExist:
            return Response({"detail": "Reply target message not found."}, status=status.HTTP_404_NOT_FOUND)

    msg = GroupMessage.objects.create(
        group=group,
        sender_id=me,
        body=body,
        attachment=attachment,
        reply_to=reply_to,
    )
    group.last_message_at = msg.created_at
    group.save(update_fields=("last_message_at",))
    return Response(
        GroupMessageSerializer(msg, context={"request": request}).data,
        status=status.HTTP_201_CREATED,
    )


# ---------------------------------------------------------------------------
# Edit & Delete — Direct Messages
# ---------------------------------------------------------------------------


@api_view(["PATCH", "DELETE"])
@permission_classes([IsAuthenticated])
def dm_message_detail(request: Request, conversation_id: int, message_id: int) -> Response:
    """Edit (PATCH) or soft-delete (DELETE) a direct message.

    PATCH  — only the sender may edit; requires non-empty ``body``.
    DELETE — the sender may soft-delete their own message; a superuser may
             delete any message.
    """
    tenant = _require_tenant(request)
    me = request.user.pk

    try:
        conv = DirectConversation.objects.get(pk=conversation_id, tenant=tenant)
    except DirectConversation.DoesNotExist:
        return Response({"detail": "Conversation not found."}, status=status.HTTP_404_NOT_FOUND)

    if conv.user1_id != me and conv.user2_id != me:
        return Response({"detail": "Forbidden."}, status=status.HTTP_403_FORBIDDEN)

    try:
        msg = DirectMessage.objects.get(pk=message_id, conversation=conv)
    except DirectMessage.DoesNotExist:
        return Response({"detail": "Message not found."}, status=status.HTTP_404_NOT_FOUND)

    if msg.is_deleted:
        return Response({"detail": "Message already deleted."}, status=status.HTTP_410_GONE)

    if request.method == "PATCH":
        if msg.sender_id != me:
            return Response(
                {"detail": "You can only edit your own messages."},
                status=status.HTTP_403_FORBIDDEN,
            )
        body = (request.data.get("body") or "").strip()
        if not body:
            return Response({"detail": "body is required."}, status=status.HTTP_400_BAD_REQUEST)
        msg.body = body
        msg.edited_at = timezone.now()
        msg.save(update_fields=("body", "edited_at"))
        return Response(DirectMessageSerializer(msg, context={"request": request}).data)

    # DELETE
    is_admin = request.user.is_superuser
    if msg.sender_id != me and not is_admin:
        return Response(
            {"detail": "You can only delete your own messages."},
            status=status.HTTP_403_FORBIDDEN,
        )
    msg.is_deleted = True
    msg.body = ""
    msg.attachment = None
    msg.save(update_fields=("is_deleted", "body", "attachment"))
    return Response(status=status.HTTP_204_NO_CONTENT)


# ---------------------------------------------------------------------------
# Edit & Delete — Group Messages
# ---------------------------------------------------------------------------


@api_view(["PATCH", "DELETE"])
@permission_classes([IsAuthenticated])
def group_message_detail(request: Request, group_id: int, message_id: int) -> Response:
    """Edit (PATCH) or soft-delete (DELETE) a group / channel message.

    PATCH  — only the sender may edit; requires non-empty ``body``.
    DELETE — the sender OR a group admin OR a superuser may delete.
    """
    tenant = _require_tenant(request)
    me = request.user.pk

    try:
        group = GroupConversation.objects.get(pk=group_id, tenant=tenant)
    except GroupConversation.DoesNotExist:
        return Response({"detail": "Group not found."}, status=status.HTTP_404_NOT_FOUND)

    membership = group.members.filter(user_id=me).first()
    if not membership:
        return Response({"detail": "Forbidden."}, status=status.HTTP_403_FORBIDDEN)

    try:
        msg = GroupMessage.objects.get(pk=message_id, group=group)
    except GroupMessage.DoesNotExist:
        return Response({"detail": "Message not found."}, status=status.HTTP_404_NOT_FOUND)

    if msg.is_deleted:
        return Response({"detail": "Message already deleted."}, status=status.HTTP_410_GONE)

    if request.method == "PATCH":
        if msg.sender_id != me:
            return Response(
                {"detail": "You can only edit your own messages."},
                status=status.HTTP_403_FORBIDDEN,
            )
        body = (request.data.get("body") or "").strip()
        if not body:
            return Response({"detail": "body is required."}, status=status.HTTP_400_BAD_REQUEST)
        msg.body = body
        msg.edited_at = timezone.now()
        msg.save(update_fields=("body", "edited_at"))
        return Response(GroupMessageSerializer(msg, context={"request": request}).data)

    # DELETE — sender, group admin, or superuser
    is_group_admin = membership.role == "admin"
    is_superuser = request.user.is_superuser
    if msg.sender_id != me and not is_group_admin and not is_superuser:
        return Response(
            {"detail": "You can only delete your own messages."},
            status=status.HTTP_403_FORBIDDEN,
        )
    msg.is_deleted = True
    msg.body = ""
    msg.attachment = None
    msg.save(update_fields=("is_deleted", "body", "attachment"))
    return Response(status=status.HTTP_204_NO_CONTENT)


# ---------------------------------------------------------------------------
# Group Members management
# ---------------------------------------------------------------------------


@api_view(["GET", "POST"])
@permission_classes([IsAuthenticated])
def group_members(request: Request, group_id: int) -> Response:
    """List members of a group/channel, or add new members.

    GET  — list all members with role info
    POST — add members by user IDs: {"member_ids": [1, 2, 3]}
    """
    tenant = _require_tenant(request)
    me = request.user.pk

    try:
        group = GroupConversation.objects.get(pk=group_id, tenant=tenant)
    except GroupConversation.DoesNotExist:
        return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)

    # Only admins can manage members
    membership = group.members.filter(user_id=me).first()
    if not membership:
        return Response({"detail": "Forbidden."}, status=status.HTTP_403_FORBIDDEN)
    if membership.role != "admin":
        return Response(
            {"detail": "Only admins can manage members."},
            status=status.HTTP_403_FORBIDDEN,
        )

    if request.method == "GET":
        members_qs = group.members.select_related("user").order_by("-role", "user__first_name")
        return Response(GroupMemberSerializer(members_qs, many=True, context={"request": request}).data)

    # POST — add members
    member_ids = list(request.data.get("member_ids") or [])
    if not member_ids:
        return Response(
            {"detail": "member_ids is required."},
            status=status.HTTP_400_BAD_REQUEST,
        )

    added = []
    existing_ids = set(group.members.values_list("user_id", flat=True))
    for uid in member_ids:
        try:
            uid_int = int(uid)
        except (TypeError, ValueError):
            continue
        if uid_int in existing_ids:
            continue
        GroupMember.objects.create(group=group, user_id=uid_int, role="member")
        added.append(uid_int)
        existing_ids.add(uid_int)

    members_qs = group.members.select_related("user").order_by("-role", "user__first_name")
    return Response(
        {
            "added": added,
            "members": GroupMemberSerializer(members_qs, many=True, context={"request": request}).data,
        },
        status=status.HTTP_200_OK,
    )


@api_view(["POST"])
@permission_classes([IsAuthenticated])
def group_add_all_members(request: Request, group_id: int) -> Response:
    """Add all tenant members to the group/channel."""
    tenant = _require_tenant(request)
    me = request.user.pk

    try:
        group = GroupConversation.objects.get(pk=group_id, tenant=tenant)
    except GroupConversation.DoesNotExist:
        return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)

    membership = group.members.filter(user_id=me).first()
    if not membership:
        return Response({"detail": "Forbidden."}, status=status.HTTP_403_FORBIDDEN)
    if membership.role != "admin":
        return Response(
            {"detail": "Only admins can manage members."},
            status=status.HTTP_403_FORBIDDEN,
        )

    tenant_user_ids = set(
        Membership.objects.filter(tenant=tenant, status=MembershipStatus.ACTIVE)
        .values_list("users", flat=True)
        .distinct()
    )
    existing_ids = set(group.members.values_list("user_id", flat=True))
    new_ids = tenant_user_ids - existing_ids

    members_to_create = [
        GroupMember(group=group, user_id=uid, role="member")
        for uid in new_ids
    ]
    GroupMember.objects.bulk_create(members_to_create, ignore_conflicts=True)

    members_qs = group.members.select_related("user").order_by("-role", "user__first_name")
    return Response({
        "added_count": len(new_ids),
        "members": GroupMemberSerializer(members_qs, many=True, context={"request": request}).data,
    })


@api_view(["DELETE"])
@permission_classes([IsAuthenticated])
def group_remove_member(request: Request, group_id: int, user_id: int) -> Response:
    """Remove a member from a group/channel."""
    tenant = _require_tenant(request)
    me = request.user.pk

    try:
        group = GroupConversation.objects.get(pk=group_id, tenant=tenant)
    except GroupConversation.DoesNotExist:
        return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)

    membership = group.members.filter(user_id=me).first()
    if not membership:
        return Response({"detail": "Forbidden."}, status=status.HTTP_403_FORBIDDEN)

    # Only admins can remove members, or a user can leave
    if membership.role != "admin" and user_id != me:
        return Response(
            {"detail": "Only admins can remove other members."},
            status=status.HTTP_403_FORBIDDEN,
        )

    # Cannot remove the last admin
    if user_id == me and membership.role == "admin":
        admin_count = group.members.filter(role="admin").count()
        if admin_count <= 1:
            return Response(
                {"detail": "Cannot remove the last admin. Assign another admin first."},
                status=status.HTTP_400_BAD_REQUEST,
            )

    deleted_count, _ = group.members.filter(user_id=user_id).delete()
    if deleted_count == 0:
        return Response({"detail": "Member not found."}, status=status.HTTP_404_NOT_FOUND)

    return Response(status=status.HTTP_204_NO_CONTENT)


# ---------------------------------------------------------------------------
# Forward message
# ---------------------------------------------------------------------------


@api_view(["POST"])
@permission_classes([IsAuthenticated])
def forward_message(request: Request) -> Response:
    """Forward a message to one or more DM conversations or groups.

    Body:
      source_message_id  — ID of the message to forward
      source_type        — "dm" or "group"
      target_conv_ids    — list of DM conversation IDs (optional)
      target_group_ids   — list of group/channel IDs (optional)
      body               — optional additional text to prepend
    """
    tenant = _require_tenant(request)
    me = request.user.pk

    source_msg_id = request.data.get("source_message_id")
    source_type = request.data.get("source_type")
    target_conv_ids = list(request.data.get("target_conv_ids") or [])
    target_group_ids = list(request.data.get("target_group_ids") or [])
    extra_body = (request.data.get("body") or "").strip()

    if not source_msg_id or source_type not in ("dm", "group"):
        return Response(
            {"detail": "source_message_id and source_type ('dm' or 'group') are required."},
            status=status.HTTP_400_BAD_REQUEST,
        )

    if not target_conv_ids and not target_group_ids:
        return Response(
            {"detail": "At least one target_conv_ids or target_group_ids is required."},
            status=status.HTTP_400_BAD_REQUEST,
        )

    # Resolve source message
    forward_snapshot: dict = {"source_type": source_type}
    source_body = ""
    if source_type == "dm":
        try:
            src_msg = DirectMessage.objects.select_related("sender").get(pk=source_msg_id)
        except DirectMessage.DoesNotExist:
            return Response({"detail": "Source message not found."}, status=status.HTTP_404_NOT_FOUND)
        conv = src_msg.conversation
        if conv.tenant_id != tenant.pk:
            return Response({"detail": "Forbidden."}, status=status.HTTP_403_FORBIDDEN)
        if conv.user1_id != me and conv.user2_id != me:
            return Response({"detail": "Forbidden."}, status=status.HTTP_403_FORBIDDEN)
        sender_name = f"{src_msg.sender.first_name} {src_msg.sender.last_name}".strip() or src_msg.sender.email
        forward_snapshot.update({
            "sender_name": sender_name,
            "sender_id": src_msg.sender_id,
            "body": src_msg.body,
            "created_at": str(src_msg.created_at),
        })
        source_body = src_msg.body
    else:
        try:
            src_msg = GroupMessage.objects.select_related("sender", "group").get(pk=source_msg_id)
        except GroupMessage.DoesNotExist:
            return Response({"detail": "Source message not found."}, status=status.HTTP_404_NOT_FOUND)
        if src_msg.group.tenant_id != tenant.pk:
            return Response({"detail": "Forbidden."}, status=status.HTTP_403_FORBIDDEN)
        if not src_msg.group.members.filter(user_id=me).exists():
            return Response({"detail": "Forbidden."}, status=status.HTTP_403_FORBIDDEN)
        sender_name = f"{src_msg.sender.first_name} {src_msg.sender.last_name}".strip() or src_msg.sender.email
        forward_snapshot.update({
            "sender_name": sender_name,
            "sender_id": src_msg.sender_id,
            "group_name": src_msg.group.name,
            "body": src_msg.body,
            "created_at": str(src_msg.created_at),
        })
        source_body = src_msg.body

    forward_body = extra_body if extra_body else source_body

    results = {"dm": [], "group": []}

    # Forward to DM conversations
    for cid in target_conv_ids:
        try:
            cid_int = int(cid)
        except (TypeError, ValueError):
            continue
        try:
            conv = DirectConversation.objects.get(pk=cid_int, tenant=tenant)
        except DirectConversation.DoesNotExist:
            continue
        if conv.user1_id != me and conv.user2_id != me:
            continue
        msg = DirectMessage.objects.create(
            conversation=conv,
            sender_id=me,
            body=forward_body,
            forwarded_from=forward_snapshot,
        )
        conv.last_message_at = msg.created_at
        conv.save(update_fields=("last_message_at",))
        results["dm"].append(msg.id)

    # Forward to groups
    for gid in target_group_ids:
        try:
            gid_int = int(gid)
        except (TypeError, ValueError):
            continue
        try:
            group = GroupConversation.objects.get(pk=gid_int, tenant=tenant)
        except GroupConversation.DoesNotExist:
            continue
        membership = group.members.filter(user_id=me).first()
        if not membership:
            continue
        if group.kind == "channel" and membership.role != "admin":
            continue
        msg = GroupMessage.objects.create(
            group=group,
            sender_id=me,
            body=forward_body,
            forwarded_from=forward_snapshot,
        )
        group.last_message_at = msg.created_at
        group.save(update_fields=("last_message_at",))
        results["group"].append(msg.id)

    return Response(results, status=status.HTTP_201_CREATED)
