"""Notification persistence service."""

import uuid
from datetime import datetime, timezone
from typing import Any

from sqlalchemy import func, select, update
from sqlalchemy.engine import CursorResult
from sqlalchemy.ext.asyncio import AsyncSession

from src.models import Notification


class NotificationService:
    """Singleton notification persistence service.

    All methods filter by user_id to enforce strict ownership isolation.
    """

    async def create_notification(
        self,
        session: AsyncSession,
        user_id: uuid.UUID,
        *,
        category: str,
        severity: str,
        title: str,
        message: str | None = None,
        source_type: str | None = None,
        source_id: uuid.UUID | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> Notification:
        """Insert a new notification row.

        Args:
            session: Database session.
            user_id: Owner of the notification.
            category: Notification category (e.g., instance, system, health).
            severity: Severity level (e.g., info, warning, error, success).
            title: Short notification title.
            message: Optional longer message body.
            source_type: Optional source entity type.
            source_id: Optional source entity UUID.
            metadata: Optional JSON metadata dictionary.

        Returns:
            The newly created Notification instance.
        """
        notification = Notification(
            user_id=user_id,
            category=category,
            severity=severity,
            title=title,
            message=message,
            source_type=source_type,
            source_id=source_id,
            notification_metadata=metadata or {},
        )
        session.add(notification)
        await session.commit()
        await session.refresh(notification)
        return notification

    async def list_notifications(
        self,
        session: AsyncSession,
        user_id: uuid.UUID,
        *,
        limit: int = 20,
        offset: int = 0,
        unread_only: bool = False,
        mute_categories: list[str] | None = None,
    ) -> tuple[list[Notification], int]:
        """Return paginated notifications for a user.

        Excludes dismissed notifications and applies optional filtering.

        Args:
            session: Database session.
            user_id: Owner of the notifications.
            limit: Maximum number of items to return.
            offset: Number of items to skip.
            unread_only: If True, only return unread notifications.
            mute_categories: Categories to exclude from results.

        Returns:
            A tuple of (items, total_count).
        """
        where_clauses = [
            Notification.user_id == user_id,
            Notification.dismissed_at.is_(None),
        ]

        if unread_only:
            where_clauses.append(Notification.read_at.is_(None))

        if mute_categories:
            where_clauses.append(Notification.category.not_in(mute_categories))

        total_stmt = (
            select(func.count()).select_from(Notification).where(*where_clauses)
        )
        total_result = await session.execute(total_stmt)
        total = total_result.scalar_one()

        items_stmt = (
            select(Notification)
            .where(*where_clauses)
            .order_by(Notification.created_at.desc())
            .limit(limit)
            .offset(offset)
        )
        items_result = await session.execute(items_stmt)
        items = list(items_result.scalars().all())

        return items, total

    async def get_unread_count(
        self,
        session: AsyncSession,
        user_id: uuid.UUID,
    ) -> int:
        """Count unread, non-dismissed notifications for a user.

        Args:
            session: Database session.
            user_id: Owner of the notifications.

        Returns:
            Number of unread notifications.
        """
        stmt = (
            select(func.count())
            .select_from(Notification)
            .where(
                Notification.user_id == user_id,
                Notification.read_at.is_(None),
                Notification.dismissed_at.is_(None),
            )
        )
        result = await session.execute(stmt)
        return result.scalar_one()

    async def mark_read(
        self,
        session: AsyncSession,
        notification_id: uuid.UUID,
        user_id: uuid.UUID,
    ) -> Notification:
        """Mark a single notification as read.

        Args:
            session: Database session.
            notification_id: UUID of the notification to mark.
            user_id: Owner of the notification.

        Returns:
            The updated Notification instance.

        Raises:
            ValueError: If the notification does not exist or is not owned by the user.
        """
        notification = await self._get_owned_notification(
            session, notification_id, user_id
        )
        notification.read_at = datetime.now(timezone.utc)
        await session.commit()
        await session.refresh(notification)
        return notification

    async def mark_all_read(
        self,
        session: AsyncSession,
        user_id: uuid.UUID,
    ) -> int:
        """Mark all unread notifications as read for a user.

        Args:
            session: Database session.
            user_id: Owner of the notifications.

        Returns:
            Number of rows updated.
        """
        stmt = (
            update(Notification)
            .where(
                Notification.user_id == user_id,
                Notification.read_at.is_(None),
                Notification.dismissed_at.is_(None),
            )
            .values(read_at=datetime.now(timezone.utc))
        )
        result: CursorResult[Any] = await session.execute(stmt)  # type: ignore[assignment]
        await session.commit()
        return result.rowcount or 0

    async def dismiss_all(
        self,
        session: AsyncSession,
        user_id: uuid.UUID,
    ) -> int:
        """Soft-delete all non-dismissed notifications for a user.

        Args:
            session: Database session.
            user_id: Owner of the notifications.

        Returns:
            Number of rows updated.
        """
        stmt = (
            update(Notification)
            .where(
                Notification.user_id == user_id,
                Notification.dismissed_at.is_(None),
            )
            .values(dismissed_at=datetime.now(timezone.utc))
        )
        result: CursorResult[Any] = await session.execute(stmt)  # type: ignore[assignment]
        await session.commit()
        return result.rowcount or 0

    async def dismiss(
        self,
        session: AsyncSession,
        notification_id: uuid.UUID,
        user_id: uuid.UUID,
    ) -> None:
        """Soft-delete a notification by setting dismissed_at.

        Args:
            session: Database session.
            notification_id: UUID of the notification to dismiss.
            user_id: Owner of the notification.

        Raises:
            ValueError: If the notification does not exist or is not owned by the user.
        """
        notification = await self._get_owned_notification(
            session, notification_id, user_id
        )
        notification.dismissed_at = datetime.now(timezone.utc)
        await session.commit()

    async def _get_owned_notification(
        self,
        session: AsyncSession,
        notification_id: uuid.UUID,
        user_id: uuid.UUID,
    ) -> Notification:
        """Fetch a notification and verify ownership.

        Args:
            session: Database session.
            notification_id: UUID of the notification.
            user_id: Expected owner.

        Returns:
            The Notification instance.

        Raises:
            ValueError: If the notification does not exist or is not owned.
        """
        notification = await session.get(Notification, notification_id)
        if notification is None or notification.user_id != user_id:
            raise ValueError("Notification not found")
        return notification


# Module-level singleton instance
notification_service = NotificationService()
