# retoor <retoor@molodetz.nl>

import asyncio
import logging
from typing import Annotated, Optional
from fastapi import APIRouter, Request, Form, WebSocket, WebSocketDisconnect
from devplacepy.models import MessageForm
from fastapi.responses import HTMLResponse, JSONResponse
from devplacepy.database import get_table, db, get_users_by_uids, search_users_by_username
from devplacepy.attachments import get_attachments_batch
from devplacepy.templating import clear_messages_cache
from devplacepy.utils import (
    require_user,
    time_ago,
    is_admin,
    _user_from_session,
    _user_from_api_key,
)
from devplacepy.seo import base_seo_context
from devplacepy.responses import respond, action_result
from devplacepy.schemas import MessagesOut
from devplacepy.services.audit import record as audit
from devplacepy.services.correction import PENDING_SCOPE_KEY
from devplacepy.services.messaging import (
    message_frame,
    message_hub,
    message_relay,
    persist_message,
)

logger = logging.getLogger(__name__)
router = APIRouter()

MAX_WS_ATTACHMENTS = 5


def mark_conversation_read(user_uid: str, other_uid: str) -> None:
    if "messages" not in db.tables:
        return
    with db:
        db.query(
            "UPDATE messages SET read = 1 WHERE receiver_uid = :me AND sender_uid = :other AND read = 0",
            me=user_uid,
            other=other_uid,
        )
    clear_messages_cache(user_uid)


def get_conversations(user_uid: str):
    messages_table = get_table("messages")
    raw = list(messages_table.find(sender_uid=user_uid)) + list(
        messages_table.find(receiver_uid=user_uid)
    )
    seen = set()
    all_messages = []
    for m in raw:
        if m["uid"] not in seen:
            seen.add(m["uid"])
            all_messages.append(m)

    conversation_map = {}
    other_uids = set()
    for msg in all_messages:
        other_uid = (
            msg["receiver_uid"] if msg["sender_uid"] == user_uid else msg["sender_uid"]
        )
        other_uids.add(other_uid)
        if (
            other_uid not in conversation_map
            or msg["created_at"] > conversation_map[other_uid]["last_message_at"]
        ):
            conversation_map[other_uid] = {
                "other_user": None,
                "last_message": msg["content"],
                "last_message_at": msg["created_at"],
                "unread": msg["receiver_uid"] == user_uid and not msg["read"],
            }

    if other_uids:
        users_map = get_users_by_uids(list(other_uids))
        for uid, conv in conversation_map.items():
            conv["other_user"] = users_map.get(uid)

    conversations = sorted(
        conversation_map.values(),
        key=lambda c: c["last_message_at"],
        reverse=True,
    )
    return conversations


def get_conversation_messages(user_uid: str, other_uid: str):
    messages_table = get_table("messages")
    raw = list(messages_table.find(sender_uid=user_uid, receiver_uid=other_uid)) + list(
        messages_table.find(sender_uid=other_uid, receiver_uid=user_uid)
    )
    seen = set()
    msgs = []
    for m in raw:
        if m["uid"] not in seen:
            seen.add(m["uid"])
            msgs.append(m)
    msgs.sort(key=lambda m: m["created_at"])

    mark_conversation_read(user_uid, other_uid)

    user_ids = list({m["sender_uid"] for m in msgs} | {other_uid})
    users_map = get_users_by_uids(user_ids)
    other_user = users_map.get(other_uid)

    result = []
    msg_uids = [m["uid"] for m in msgs]
    attachments_map = get_attachments_batch("message", msg_uids) if msg_uids else {}
    for m in msgs:
        result.append(
            {
                "message": m,
                "sender": users_map.get(m["sender_uid"]),
                "is_mine": m["sender_uid"] == user_uid,
                "time_ago": time_ago(m["created_at"]),
                "attachments": attachments_map.get(m["uid"], []),
            }
        )
    return result, other_user


@router.get("", response_class=HTMLResponse)
async def messages_page(request: Request, with_uid: str = None, search: str = ""):
    user = require_user(request)
    conversations = get_conversations(user["uid"])

    if search:
        users_table = get_table("users")
        found = users_table.find_one(username=search)
        if found:
            with_uid = found["uid"]

    current_conversation = None
    messages = []
    other_user = None
    other_online = False
    other_last_seen = None
    if with_uid:
        messages, other_user = get_conversation_messages(user["uid"], with_uid)
        current_conversation = with_uid
        other_online = message_hub.is_online(with_uid)
        other_last_seen = message_hub.last_seen(with_uid)
        audit.record(
            request,
            "message.read_on_view",
            user=user,
            target_type="user",
            target_uid=with_uid,
            target_label=other_user.get("username") if other_user else None,
            metadata={"message_count": len(messages)},
            summary=f"{user['username']} read messages from {other_user.get('username') if other_user else with_uid}",
            links=[audit.target("user", with_uid, other_user.get("username") if other_user else None)],
        )

    seo_ctx = base_seo_context(
        request,
        title="Messages",
        robots="noindex,nofollow",
        breadcrumbs=[
            {"name": "Home", "url": "/feed"},
            {"name": "Messages", "url": "/messages"},
        ],
    )
    return respond(
        request,
        "messages.html",
        {
            **seo_ctx,
            "request": request,
            "user": user,
            "conversations": conversations,
            "messages": messages,
            "other_user": other_user,
            "current_conversation": current_conversation,
            "search": search,
            "other_online": other_online,
            "other_last_seen": other_last_seen,
        },
        model=MessagesOut,
    )


@router.get("/search")
async def search_users(request: Request, q: str = ""):
    user = require_user(request)
    if not q or len(q) < 1:
        return JSONResponse({"results": []})
    results = search_users_by_username(q, exclude_uid=user["uid"])
    return JSONResponse({"results": results})


@router.post("/send")
async def send_message(request: Request, data: Annotated[MessageForm, Form()]):
    user = require_user(request)
    receiver_uid = data.receiver_uid

    message = persist_message(
        user,
        receiver_uid,
        data.content,
        data.attachment_uids,
        request=request,
        origin="web",
    )
    if message is None:
        return action_result(request, "/messages")

    await _finalize_and_broadcast(user, message, request)
    return action_result(
        request, f"/messages?with_uid={receiver_uid}", data={"uid": message["uid"]}
    )


async def broadcast_message(
    sender: dict, message: dict, client_id: Optional[str] = None
) -> None:
    frame = message_frame(message, sender.get("username", ""), client_id)
    message_hub.mark_delivered(message["uid"])
    targets = [message["sender_uid"], message["receiver_uid"]]
    await message_hub.send_to_users(targets, frame)


async def _finalize_and_broadcast(
    sender: dict, message: dict, request: object, client_id: Optional[str] = None
) -> None:
    scope = getattr(request, "scope", None)
    pending = scope.get(PENDING_SCOPE_KEY) if scope is not None else None
    if pending:
        await asyncio.gather(*pending, return_exceptions=True)
        pending.clear()
        row = get_table("messages").find_one(uid=message["uid"])
        if row:
            message["content"] = row["content"]
    await broadcast_message(sender, message, client_id)


def _resolve_ws_user(websocket: WebSocket):
    user = _user_from_session(websocket)
    if user:
        return user
    key = websocket.headers.get("x-api-key", "").strip()
    if not key:
        scheme, _, credentials = websocket.headers.get("authorization", "").partition(
            " "
        )
        if scheme.lower() == "bearer":
            key = credentials.strip()
    if key:
        return _user_from_api_key(key)
    return None


async def _announce_presence(user_uid: str, online: bool) -> None:
    other_uids = {c["other_user"]["uid"] for c in get_conversations(user_uid) if c["other_user"]}
    if not other_uids:
        return
    frame = {
        "type": "presence",
        "user_uid": user_uid,
        "online": online,
        "last_seen": None if online else message_hub.last_seen(user_uid),
    }
    await message_hub.send_to_users(list(other_uids), frame)


@router.websocket("/ws")
async def messages_ws(websocket: WebSocket):
    await websocket.accept()
    user = _resolve_ws_user(websocket)
    if not user:
        await websocket.close(code=1008)
        return

    user_uid = user["uid"]
    was_offline = message_hub.register(user_uid, websocket)
    message_relay.start()
    try:
        await websocket.send_json({"type": "ready", "user_uid": user_uid})
        if was_offline:
            await _announce_presence(user_uid, True)
        while True:
            data = await websocket.receive_json()
            kind = data.get("type")
            if kind == "send":
                receiver_uid = str(data.get("receiver_uid", "")).strip()
                content = str(data.get("content", ""))
                client_id = data.get("client_id")
                raw_attachments = data.get("attachment_uids") or []
                attachment_uids = [str(a) for a in raw_attachments][:MAX_WS_ATTACHMENTS]
                if not receiver_uid:
                    continue
                message = persist_message(
                    user,
                    receiver_uid,
                    content,
                    attachment_uids,
                    request=websocket,
                    origin="websocket",
                )
                if message is None:
                    await websocket.send_json(
                        {"type": "error", "client_id": client_id, "text": "Message not sent."}
                    )
                    continue
                await _finalize_and_broadcast(user, message, websocket, client_id)
            elif kind == "typing":
                receiver_uid = str(data.get("receiver_uid", "")).strip()
                if receiver_uid:
                    await message_hub.send_to_user(
                        receiver_uid, {"type": "typing", "from_uid": user_uid}
                    )
            elif kind == "read":
                with_uid = str(data.get("with_uid", "")).strip()
                if with_uid:
                    mark_conversation_read(user_uid, with_uid)
                    other_user = get_users_by_uids([with_uid]).get(with_uid)
                    other_username = other_user.get("username") if other_user else None
                    audit.record(
                        websocket,
                        "message.read_on_view",
                        user=user,
                        target_type="user",
                        target_uid=with_uid,
                        target_label=other_username,
                        summary=f"{user['username']} read messages from {other_username or with_uid}",
                        links=[audit.target("user", with_uid, other_username)],
                    )
                    await message_hub.send_to_user(
                        with_uid, {"type": "read", "by_uid": user_uid}
                    )
            elif kind == "presence":
                with_uid = str(data.get("with_uid", "")).strip()
                if with_uid:
                    await websocket.send_json(
                        {
                            "type": "presence",
                            "user_uid": with_uid,
                            "online": message_hub.is_online(with_uid),
                            "last_seen": message_hub.last_seen(with_uid),
                        }
                    )
    except WebSocketDisconnect:
        pass
    except Exception:  # noqa: BLE001
        logger.exception("messages websocket loop failed for %s", user_uid)
    finally:
        went_offline = message_hub.unregister(user_uid, websocket)
        if went_offline:
            try:
                await _announce_presence(user_uid, False)
            except Exception:  # noqa: BLE001
                logger.debug("presence offline announce failed for %s", user_uid)
