#nullable enable
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Notifications;
using Baya.Application.Models.Common;
using Baya.Application.Models.Notifications;
using Microsoft.EntityFrameworkCore;
using NotificationEntity = Baya.Domain.Entities.Notifications.Notification;

namespace Baya.Infrastructure.Persistence.Services.Notifications;

/// <summary>
/// Reads and per-user commands over <c>notifications</c>. Every method is scoped to the passed
/// <c>userId</c> (the authenticated caller). Retention hard-deletes read notifications past the window
/// and never touches unread ones.
/// </summary>
internal sealed class NotificationService(ApplicationDbContext db, IDateTimeProvider dateTimeProvider) : INotificationService
{
    public async ValueTask<PagedResult<NotificationDto>> ListMineAsync(int userId, int page, int pageSize, CancellationToken cancellationToken = default)
    {
        var query = db.Set<NotificationEntity>()
            .AsNoTracking()
            .Where(n => n.UserId == userId)
            // Unread first, then newest-first via the monotonic identity (deterministic, no timestamp ties).
            .OrderBy(n => n.IsRead)
            .ThenByDescending(n => n.Id);

        var total = await query.CountAsync(cancellationToken);
        var items = await query
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(n => new NotificationDto(n.Id, n.Type, n.Title, n.Body, n.DataJson, n.IsRead, n.ReadAt, n.CreatedAt))
            .ToListAsync(cancellationToken);

        return new PagedResult<NotificationDto>(items, total, page, pageSize);
    }

    public ValueTask<int> GetUnreadCountAsync(int userId, CancellationToken cancellationToken = default) =>
        new(db.Set<NotificationEntity>().AsNoTracking().CountAsync(n => n.UserId == userId && !n.IsRead, cancellationToken));

    public async ValueTask<bool> MarkReadAsync(int userId, long notificationId, CancellationToken cancellationToken = default)
    {
        var notification = await db.Set<NotificationEntity>()
            .FirstOrDefaultAsync(n => n.Id == notificationId && n.UserId == userId, cancellationToken);

        if (notification is null)
            return false;

        if (!notification.IsRead)
        {
            notification.IsRead = true;
            notification.ReadAt = dateTimeProvider.UtcNow;
            await db.SaveChangesAsync(cancellationToken);
        }

        return true;
    }

    public async ValueTask<int> MarkAllReadAsync(int userId, CancellationToken cancellationToken = default)
    {
        var now = dateTimeProvider.UtcNow;
        return await db.Set<NotificationEntity>()
            .Where(n => n.UserId == userId && !n.IsRead)
            .ExecuteUpdateAsync(
                s => s.SetProperty(n => n.IsRead, true).SetProperty(n => n.ReadAt, now),
                cancellationToken);
    }

    public async ValueTask<int> PurgeOldReadAsync(int retentionDays, CancellationToken cancellationToken = default)
    {
        var cutoff = dateTimeProvider.UtcNow.AddDays(-retentionDays);

        // Read-only rows are the only purge candidates (unread is never deleted). The age cutoff is
        // applied in memory so the delete is a single id-keyed statement that translates on every
        // provider; the candidate set is bounded (only read notifications).
        var readRows = await db.Set<NotificationEntity>()
            .Where(n => n.IsRead)
            .Select(n => new { n.Id, n.CreatedAt })
            .ToListAsync(cancellationToken);

        var expiredIds = readRows.Where(n => n.CreatedAt < cutoff).Select(n => n.Id).ToList();
        if (expiredIds.Count == 0)
            return 0;

        return await db.Set<NotificationEntity>()
            .Where(n => expiredIds.Contains(n.Id))
            .ExecuteDeleteAsync(cancellationToken);
    }
}
