"""Notification service layer.

All notification business logic lives here; routes (added in a later phase) stay
thin and call into these functions. A single generic :func:`create_notification`
API backs every module, so callers never touch the model or session directly.

Business rules enforced here:

* a notification belongs to exactly one user;
* notifications are immutable after creation except for ``read_at``;
* access is participant-only (a user sees only their own notifications).
"""

from __future__ import annotations

from flask import current_app

from app.extensions import db
from app.models import Notification, NotificationType, RelatedEntityType, User
from app.models.base import utcnow

# Mirrors the approved schema column lengths (VARCHAR(150) / VARCHAR(500)).
MAX_TITLE_LENGTH = 150
MAX_MESSAGE_LENGTH = 500


class NotificationError(Exception):
    """Base error for notification operations that violate a business rule."""


class InvalidNotificationError(NotificationError):
    """Raised when notification content fails validation before storage."""


class NotOwnerError(NotificationError):
    """Raised when a user acts on a notification that is not theirs."""


def create_notification(
    user: User,
    notification_type: NotificationType,
    title: str,
    message: str,
    *,
    related_entity_type: RelatedEntityType | None = None,
    related_entity_id: int | None = None,
    expires_at=None,
) -> Notification:
    """Create and persist a notification for ``user``.

    This is the single, generic entry point every module uses. Content is
    trimmed and validated before storage; an empty/oversized title or message
    raises :class:`InvalidNotificationError`. The optional
    ``related_entity_type`` / ``related_entity_id`` link the notification to the
    record it concerns (used later to build a link).
    """
    clean_title = (title or "").strip()
    clean_message = (message or "").strip()
    if not clean_title or not clean_message:
        raise InvalidNotificationError("Notification title and message are required.")
    if len(clean_title) > MAX_TITLE_LENGTH:
        raise InvalidNotificationError(
            f"Notification title cannot exceed {MAX_TITLE_LENGTH} characters."
        )
    if len(clean_message) > MAX_MESSAGE_LENGTH:
        raise InvalidNotificationError(
            f"Notification message cannot exceed {MAX_MESSAGE_LENGTH} characters."
        )

    notification = Notification(
        user_id=user.id,
        notification_type=notification_type,
        title=clean_title,
        message=clean_message,
        related_entity_type=related_entity_type,
        related_entity_id=related_entity_id,
        expires_at=expires_at,
    )
    db.session.add(notification)
    db.session.commit()
    return notification


def get_user_notifications(user: User, page: int = 1, per_page: int | None = None):
    """Return a pagination of ``user``'s notifications, newest first."""
    if per_page is None:
        per_page = current_app.config.get("DEFAULT_PAGE_SIZE", 20)
    query = (
        db.select(Notification)
        .where(Notification.user_id == user.id)
        .order_by(Notification.created_at.desc(), Notification.id.desc())
    )
    return db.paginate(query, page=page, per_page=per_page, error_out=False)


def get_notification_or_404(public_id: str, user: User) -> Notification:
    """Return a notification by public id only if it belongs to ``user``.

    Aborts with 404 otherwise (does not reveal that the notification exists).
    """
    return db.first_or_404(
        db.select(Notification).where(
            Notification.public_id == public_id,
            Notification.user_id == user.id,
        )
    )


def mark_notification_read(notification: Notification, user: User) -> None:
    """Mark a single notification read for its owner. Idempotent.

    Setting ``read_at`` is the only permitted mutation; calling this again after
    the notification is already read is a no-op.
    """
    if notification.user_id != user.id:
        raise NotOwnerError("You can only update your own notifications.")
    if notification.read_at is None:
        notification.read_at = utcnow()
        db.session.commit()


def mark_all_notifications_read(user: User) -> int:
    """Mark all of ``user``'s unread notifications read. Returns the count updated.

    Runs as a single bulk ``UPDATE`` so it stays efficient regardless of volume.
    """
    result = db.session.execute(
        db.update(Notification)
        .where(
            Notification.user_id == user.id,
            Notification.read_at.is_(None),
        )
        .values(read_at=utcnow())
    )
    db.session.commit()
    return result.rowcount


def get_unread_notification_count(user: User) -> int:
    """Return the number of unread notifications for ``user``.

    A lightweight ``COUNT`` query, suited to the future navbar badge.
    """
    return db.session.scalar(
        db.select(db.func.count())
        .select_from(Notification)
        .where(
            Notification.user_id == user.id,
            Notification.read_at.is_(None),
        )
    )
