using Microsoft.EntityFrameworkCore;
using Nexus.Api.Data;
using Nexus.Api.Models;
using System.Text.Json;

namespace Nexus.Api.Services;

public sealed class NotificationService(NexusDbContext db, ILiveUpdateService liveUpdateService) : INotificationService
{
    public async Task<Notification> CreateAsync(string type, string title, string? message, string forUser, Guid? taskId = null, CancellationToken ct = default)
    {
        var notification = new Notification
        {
            Type = type,
            Title = title,
            Message = message,
            ForUser = forUser.ToLowerInvariant(),
            TaskId = taskId
        };
        db.Notifications.Add(notification);
        db.OutboxEvents.Add(CreateNotificationEvent("notification.created", notification));
        await db.SaveChangesAsync(ct);
        await PublishSnapshotAsync(notification.ForUser, ct);
        return notification;
    }

    public async Task<IReadOnlyList<Notification>> GetForUserAsync(string forUser, int limit = 50, bool unreadOnly = false, CancellationToken ct = default)
    {
        var query = db.Notifications
            .Where(n => n.ForUser == forUser.ToLowerInvariant());

        if (unreadOnly)
            query = query.Where(n => !n.IsRead);

        return await query
            .OrderByDescending(n => n.CreatedAt)
            .Take(limit)
            .ToListAsync(ct);
    }

    public async Task<NotificationReadResult> MarkAsReadAsync(Guid id, CancellationToken ct = default)
    {
        var notification = await db.Notifications.FindAsync([id], ct);
        if (notification is null) return new NotificationReadResult(null, false);

        var changed = !notification.IsRead;
        if (changed)
        {
            notification.IsRead = true;
            db.OutboxEvents.Add(CreateNotificationEvent("notification.updated", notification));
            await db.SaveChangesAsync(ct);
        }
        await PublishSnapshotAsync(notification.ForUser, ct);
        return new NotificationReadResult(notification, changed);
    }

    public async Task<int> MarkAllAsReadAsync(string forUser, CancellationToken ct = default)
    {
        var normalizedUser = forUser.ToLowerInvariant();
        int count;
        if (db.Database.IsRelational())
        {
            await using var transaction = await db.Database.BeginTransactionAsync(ct);
            count = await db.Notifications
                .Where(n => n.ForUser == normalizedUser && !n.IsRead)
                .ExecuteUpdateAsync(s => s.SetProperty(n => n.IsRead, true), ct);
            if (count > 0)
            {
                db.OutboxEvents.Add(CreateNotificationCollectionEvent(
                    "notification.read_all",
                    count));
                await db.SaveChangesAsync(ct);
            }
            await transaction.CommitAsync(ct);
        }
        else
        {
            var unread = await db.Notifications
                .Where(n => n.ForUser == normalizedUser && !n.IsRead)
                .ToListAsync(ct);
            foreach (var notification in unread)
                notification.IsRead = true;
            count = unread.Count;
            if (count > 0)
            {
                db.OutboxEvents.Add(CreateNotificationCollectionEvent(
                    "notification.read_all",
                    count));
                await db.SaveChangesAsync(ct);
            }
        }
        await PublishSnapshotAsync(normalizedUser, ct);
        return count;
    }

    public async Task<int> GetUnreadCountAsync(string forUser, CancellationToken ct = default)
    {
        return await db.Notifications
            .CountAsync(n => n.ForUser == forUser.ToLowerInvariant() && !n.IsRead, ct);
    }

    public async Task<NotificationSnapshotDto> GetSnapshotAsync(string forUser, int limit = 50, bool unreadOnly = false, CancellationToken ct = default)
    {
        var normalizedUser = forUser.ToLowerInvariant();
        var notifications = await GetForUserAsync(normalizedUser, limit, unreadOnly, ct);
        var unreadCount = await GetUnreadCountAsync(normalizedUser, ct);
        return new NotificationSnapshotDto(
            notifications.Select(MapToDto).ToList(),
            unreadCount,
            normalizedUser);
    }

    private async Task PublishSnapshotAsync(string forUser, CancellationToken ct)
    {
        var snapshot = await GetSnapshotAsync(forUser, ct: ct);
        liveUpdateService.Publish("notifications.snapshot", snapshot, "notifications");
    }

    private static NotificationDto MapToDto(Notification n) => new(
        n.Id, n.Type, n.Title, n.Message,
        n.ForUser, n.TaskId, n.IsRead, n.CreatedAt);

    private static OutboxEvent CreateNotificationEvent(
        string type,
        Notification notification)
        => new()
        {
            Type = type,
            AggregateType = "notification",
            AggregateId = notification.Id.ToString(),
            AggregateRevision = notification.IsRead ? 1 : 0,
            PayloadJson = JsonSerializer.Serialize(new
            {
                notificationType = notification.Type,
                notification.TaskId,
                notification.IsRead
            })
        };

    private static OutboxEvent CreateNotificationCollectionEvent(
        string type,
        int affectedCount)
        => new()
        {
            Type = type,
            AggregateType = "notification",
            AggregateId = "*",
            AggregateRevision = 0,
            PayloadJson = JsonSerializer.Serialize(new { affectedCount })
        };
}
