from __future__ import annotations

from dataclasses import dataclass
from typing import Optional

from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.core.exceptions import PermissionDenied, ValidationError
from django.db import models
from django.db.models import Count
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

from vibes_auth.messaging.models import ChatMessage, ChatThread, SenderType, ThreadStatus
from vibes_auth.models import User

THREAD_GROUP_PREFIX = "thread:"
STAFF_INBOX_GROUP = "staff:inbox"


@dataclass
class MessageDTO:
    id: str
    thread: str
    text: str
    sender_type: str
    sender_user: Optional[str]
    sent_at: str


def _broadcast_thread_event(thread: ChatThread, event: dict) -> None:
    layer = get_channel_layer()
    async_to_sync(layer.group_send)(f"{THREAD_GROUP_PREFIX}{thread.uuid}", event)


def _broadcast_staff_inbox(event: dict) -> None:
    layer = get_channel_layer()
    async_to_sync(layer.group_send)(STAFF_INBOX_GROUP, event)


def get_or_create_user_thread(user: User) -> ChatThread:
    thread, _ = ChatThread.objects.get_or_create(user=user, defaults={"email": user.email or ""})
    return thread


def create_anon_thread(email: str) -> ChatThread:
    if not email or "@" not in email:
        raise ValidationError({"email": _("Valid email is required for anonymous chats.")})
    thread = ChatThread.objects.create(email=email)
    _broadcast_staff_inbox({"type": "staff.thread.created", "thread_id": str(thread.uuid)})
    return thread


def send_message(thread: ChatThread, *, sender_user: Optional[User], sender_type: SenderType, text: str) -> ChatMessage:
    if not text or len(text) > 1028:
        raise ValidationError({"text": _("Message must be 1..1028 characters.")})
    # Permission rules: non-staff may only write to their own thread or anon thread they initiated
    if sender_user and not sender_user.is_staff:
        if thread.user_id != sender_user.pk:
            raise PermissionDenied
    msg = ChatMessage.objects.create(
        thread=thread,
        sender_type=sender_type,
        sender_user=sender_user if sender_user and sender_user.is_authenticated else None,
        text=text,
        sent_at=timezone.now(),
    )
    thread.last_message_at = msg.sent_at
    thread.save(update_fields=["last_message_at", "modified"])

    _broadcast_thread_event(
        thread,
        {
            "type": "thread.message",
            "message": {
                "id": str(msg.uuid),
                "thread": str(thread.uuid),
                "text": msg.text,
                "sender_type": msg.sender_type,
                "sender_user": str(msg.sender_user.uuid) if msg.sender_user_id else None,
                "sent_at": msg.sent_at.isoformat(),
            },
        },
    )
    if sender_type != SenderType.STAFF:
        auto_reply(thread)
    return msg


def auto_reply(thread: ChatThread) -> None:
    # Localizable text, do not translate here
    text = _("We're searching for the operator to answer you already, hold by!")
    msg = ChatMessage.objects.create(
        thread=thread,
        sender_type=SenderType.SYSTEM,
        sender_user=None,
        text=text,
        sent_at=timezone.now(),
    )
    thread.last_message_at = msg.sent_at
    thread.save(update_fields=["last_message_at", "modified"])
    _broadcast_thread_event(
        thread,
        {
            "type": "thread.message",
            "message": {
                "id": str(msg.uuid),
                "thread": str(thread.uuid),
                "text": msg.text,
                "sender_type": msg.sender_type,
                "sender_user": None,
                "sent_at": msg.sent_at.isoformat(),
            },
        },
    )


def claim_thread(thread: ChatThread, staff_user: User) -> ChatThread:
    if not staff_user.is_staff:
        raise PermissionDenied
    if thread.assigned_to_id and not staff_user.is_superuser:
        # already assigned, cannot reassign/unassign
        raise PermissionDenied
    thread.assigned_to = staff_user
    thread.save(update_fields=["assigned_to", "modified"])
    _broadcast_staff_inbox(
        {"type": "staff.thread.assigned", "thread_id": str(thread.uuid), "user": str(staff_user.uuid)}
    )
    return thread


def reassign_thread(thread: ChatThread, superuser: User, new_staff: User) -> ChatThread:
    if not superuser.is_superuser:
        raise PermissionDenied
    if not new_staff.is_staff:
        raise ValidationError({"assigned_to": _("Assignee must be a staff user.")})
    thread.assigned_to = new_staff
    thread.save(update_fields=["assigned_to", "modified"])
    _broadcast_staff_inbox(
        {"type": "staff.thread.reassigned", "thread_id": str(thread.uuid), "user": str(new_staff.uuid)}
    )
    return thread


def close_thread(thread: ChatThread, actor: User | None) -> ChatThread:
    if actor and actor.is_staff:
        pass  # allowed
    elif actor and not actor.is_staff:
        # non-staff allowed to close own thread? Keep simple: allowed only for staff for now
        raise PermissionDenied
    thread.status = ThreadStatus.CLOSED
    thread.save(update_fields=["status", "modified"])
    _broadcast_staff_inbox({"type": "staff.thread.closed", "thread_id": str(thread.uuid)})
    _broadcast_thread_event(thread, {"type": "thread.closed", "thread_id": str(thread.uuid)})
    return thread


def find_least_load_staff() -> Optional[User]:
    qs = (
        User.objects.filter(is_staff=True, is_active=True)
        .annotate(
            open_threads=Count(
                "assigned_chat_threads", filter=models.Q(assigned_chat_threads__status=ThreadStatus.OPEN)
            )
        )
        .order_by("open_threads", "date_joined")
    )
    return qs.first()
