"""DMS collaboration — services.

Public API
----------
Comments:
    create_comment(...)            -> Comment
    update_comment(comment, ...)   -> Comment
    delete_comment(comment)        -> None          (soft-delete)
    resolve_comment(comment, ...)  -> Comment
    unresolve_comment(comment, resolving_user) -> Comment

Annotations:
    create_annotation(...)                -> Annotation
    update_annotation(annotation, ...)    -> Annotation
    delete_annotation(annotation)         -> None    (soft-delete)

Mentions:
    sync_mentions(comment, user_ids)      -> list[Mention]
    mark_mention_notified(mention)        -> Mention

Design notes
------------
Threading
    ``create_comment()`` enforces ONE level of nesting: a reply's parent must
    itself have no parent.  This keeps the data model flat and the API simple.
    The schema allows deeper trees, but the current service layer does not
    expose it.

Mentions
    ``sync_mentions()`` is called after every comment create / update.  It
    performs a diff against the current mention set: new @mentions create
    Mention rows (is_notified=False); removed @mentions delete the rows.
    The caller is responsible for parsing mention user IDs from the body —
    mention parsing (e.g. @username → user_id) is the API layer's job.

Soft-delete semantics
    ``delete_comment()`` marks the comment as deleted but keeps it in the DB
    for audit purposes.  Replies to a deleted comment remain visible (they
    reference the parent PK, not the parent body).

Annotation immutability
    ``update_annotation()`` allows updating body / color / position_data but
    NOT annotation_type or version — those are immutable after creation.
"""

from __future__ import annotations

from django.utils import timezone

# Sentinel for "argument not provided" (distinct from None, which is a valid page_number)
_UNSET = object()


# ---------------------------------------------------------------------------
# Domain errors
# ---------------------------------------------------------------------------

class CollaborationError(Exception):
    """Base collaboration domain error."""


class PermissionError(CollaborationError):
    """Raised when a user does not have permission for an action."""


class CommentError(CollaborationError):
    """Comment-specific domain error."""


class AnnotationError(CollaborationError):
    """Annotation-specific domain error."""


# ---------------------------------------------------------------------------
# Comment services
# ---------------------------------------------------------------------------

def create_comment(
    *,
    tenant_id: int,
    organization_node_id: int,
    document,
    author,
    body: str,
    version=None,
    parent=None,
) -> "Comment":
    """Create a new comment or reply on a document.

    Threading rule: a parent comment must itself be a root comment
    (``parent.parent is None``).  Raises ``CommentError`` if violated.
    """
    from simorgh.apps.dms.collaboration.models import Comment

    if not body or not body.strip():
        raise CommentError("Comment body cannot be empty.")

    if parent is not None and parent.parent_id is not None:
        raise CommentError(
            "Reply nesting is limited to one level. "
            "Replies to replies are not supported."
        )

    return Comment.objects.create(
        tenant_id=tenant_id,
        organization_node_id=organization_node_id,
        document=document,
        author=author,
        body=body.strip(),
        version=version,
        parent=parent,
    )


def update_comment(
    comment: "Comment",
    *,
    body: str,
) -> "Comment":
    """Update the body of an existing comment.

    Only body can be edited; author / document / version / parent are
    immutable after creation.  Raises ``CommentError`` if body is empty.
    """
    if not body or not body.strip():
        raise CommentError("Comment body cannot be empty.")
    comment.body = body.strip()
    comment.save(update_fields=["body", "updated_at"])
    return comment


def delete_comment(comment: "Comment") -> None:
    """Soft-delete a comment.

    Replies are NOT automatically deleted — they remain visible as orphaned
    replies.  This matches the UX convention of showing "[deleted]" comments
    with their replies intact.
    """
    comment.is_deleted = True
    comment.save(update_fields=["is_deleted", "updated_at"])


def resolve_comment(comment: "Comment", *, resolving_user) -> "Comment":
    """Mark a comment thread as resolved.

    Only root comments can be resolved (not individual replies).
    Raises ``CommentError`` if the comment is already resolved or is a reply.
    """
    if comment.parent_id is not None:
        raise CommentError("Only top-level comments can be resolved.")
    if comment.is_resolved:
        raise CommentError("Comment is already resolved.")

    comment.is_resolved = True
    comment.resolved_by = resolving_user
    comment.resolved_at = timezone.now()
    comment.save(update_fields=["is_resolved", "resolved_by", "resolved_at", "updated_at"])
    return comment


def unresolve_comment(comment: "Comment", *, resolving_user) -> "Comment":
    """Re-open a previously resolved comment thread.

    ``resolving_user`` is accepted for symmetry / audit purposes but not
    persisted (resolved_by is left as the original resolver).
    """
    if not comment.is_resolved:
        raise CommentError("Comment is not resolved.")

    comment.is_resolved = False
    comment.save(update_fields=["is_resolved", "updated_at"])
    return comment


# ---------------------------------------------------------------------------
# Annotation services
# ---------------------------------------------------------------------------

def create_annotation(
    *,
    tenant_id: int,
    organization_node_id: int,
    document,
    version,
    author,
    annotation_type: str,
    body: str = "",
    page_number: int | None = None,
    position_data: dict | None = None,
    color: str = "#FFFF00",
    linked_comment=None,
) -> "Annotation":
    """Create a new annotation on a document version."""
    from simorgh.apps.dms.collaboration.models import Annotation, AnnotationType

    if annotation_type not in AnnotationType.values:
        raise AnnotationError(
            f"Invalid annotation type: {annotation_type!r}. "
            f"Allowed: {', '.join(AnnotationType.values)}."
        )

    # Verify the version belongs to the document
    if version.document_id != document.pk:
        raise AnnotationError("Version does not belong to the given document.")

    return Annotation.objects.create(
        tenant_id=tenant_id,
        organization_node_id=organization_node_id,
        document=document,
        version=version,
        author=author,
        annotation_type=annotation_type,
        body=body or "",
        page_number=page_number,
        position_data=position_data or {},
        color=color,
        linked_comment=linked_comment,
    )


def update_annotation(
    annotation: "Annotation",
    *,
    body: str | None = None,
    color: str | None = None,
    position_data: dict | None = None,
    page_number=_UNSET,
) -> "Annotation":
    """Update mutable fields of an annotation.

    ``annotation_type`` and ``version`` are immutable after creation.
    Pass ``page_number=None`` to clear the page number.
    """
    update_fields = ["updated_at"]

    if body is not None:
        annotation.body = body
        update_fields.append("body")
    if color is not None:
        annotation.color = color
        update_fields.append("color")
    if position_data is not None:
        annotation.position_data = position_data
        update_fields.append("position_data")
    if page_number is not _UNSET:
        annotation.page_number = page_number
        update_fields.append("page_number")

    annotation.save(update_fields=update_fields)
    return annotation


def delete_annotation(annotation: "Annotation") -> None:
    """Soft-delete an annotation."""
    annotation.is_deleted = True
    annotation.save(update_fields=["is_deleted", "updated_at"])


# ---------------------------------------------------------------------------
# Mention services
# ---------------------------------------------------------------------------

def sync_mentions(comment: "Comment", user_ids: list[int]) -> list["Mention"]:
    """Synchronise @mentions for a comment after create/update.

    Adds Mention rows for newly mentioned users; removes Mention rows for
    users no longer mentioned.  Returns the current list of Mention objects.

    The caller is responsible for resolving @username → user_id mapping.
    """
    from django.contrib.auth import get_user_model
    from simorgh.apps.dms.collaboration.models import Mention

    User = get_user_model()
    user_ids_set = set(user_ids or [])

    existing = {m.mentioned_user_id: m for m in comment.mentions.all()}
    existing_ids = set(existing.keys())

    to_add = user_ids_set - existing_ids
    to_remove = existing_ids - user_ids_set

    # Remove de-mentioned users
    if to_remove:
        Mention.objects.filter(comment=comment, mentioned_user_id__in=to_remove).delete()

    # Add new mentions
    new_mentions = []
    for uid in to_add:
        try:
            user = User.objects.get(pk=uid)
        except User.DoesNotExist:
            continue
        m = Mention.objects.create(
            comment=comment,
            mentioned_user=user,
            tenant_id=comment.tenant_id,
            organization_node_id=comment.organization_node_id,
        )
        new_mentions.append(m)

    # Return full current list
    return list(comment.mentions.select_related("mentioned_user").all())


def mark_mention_notified(mention: "Mention") -> "Mention":
    """Mark a mention as notified (called by the notification pipeline)."""
    mention.is_notified = True
    mention.save(update_fields=["is_notified"])
    return mention
