using System.Text.Json;
using Health.Application.Notifications;
using Health.Infrastructure.Data.Records;
using Npgsql;

namespace Health.Infrastructure.Notifications;

public sealed class EfUserNotificationProducer(AppDbContext db) : IUserNotificationProducer
{
    private readonly AppDbContext _db = db;

    public async Task<bool> EnqueueAsync(
        Guid userId,
        Guid sourceId,
        NotificationMessage message,
        CancellationToken ct)
    {
        if (await _db.NotificationOutbox.AsNoTracking().AnyAsync(x => x.SourceTaskId == sourceId, ct))
            return false;

        var now = DateTime.UtcNow;
        await _db.NotificationOutbox.AddAsync(new NotificationOutboxRecord
        {
            Id = Guid.NewGuid(),
            UserId = userId,
            SourceTaskId = sourceId,
            Type = message.Type,
            Payload = JsonSerializer.Serialize(message),
            Status = "Pending",
            AvailableAt = now,
            CreatedAt = now,
            UpdatedAt = now,
        }, ct);

        try
        {
            await _db.SaveChangesAsync(ct);
            return true;
        }
        catch (DbUpdateException ex) when (
            ex.InnerException is PostgresException { SqlState: PostgresErrorCodes.UniqueViolation })
        {
            _db.ChangeTracker.Clear();
            return false;
        }
    }
}

public sealed class EfNotificationOutboxProcessor(AppDbContext db) : INotificationOutboxProcessor
{
    private const int MaxAttempts = 5;
    private readonly AppDbContext _db = db;

    public async Task RecoverStaleAsync(CancellationToken ct)
    {
        var now = DateTime.UtcNow;
        await _db.NotificationOutbox
            .Where(x => x.Status == "Processing" &&
                x.UpdatedAt < now.AddMinutes(-5) && x.Attempts < MaxAttempts)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(x => x.Status, "Pending")
                .SetProperty(x => x.AvailableAt, now)
                .SetProperty(x => x.UpdatedAt, now), ct);

        await _db.NotificationOutbox
            .Where(x => x.Status == "Processing" &&
                x.UpdatedAt < now.AddMinutes(-5) && x.Attempts >= MaxAttempts)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(x => x.Status, "Failed")
                .SetProperty(x => x.LastError, "通知任务执行超时且已达到最大重试次数")
                .SetProperty(x => x.UpdatedAt, now), ct);
    }

    public async Task<bool> ProcessNextAsync(CancellationToken ct)
    {
        var now = DateTime.UtcNow;
        var candidate = await _db.NotificationOutbox.AsNoTracking()
            .Where(x => x.Status == "Pending" &&
                x.AvailableAt <= now && x.Attempts < MaxAttempts)
            .OrderBy(x => x.CreatedAt)
            .FirstOrDefaultAsync(ct);
        if (candidate == null) return false;

        var claimed = await _db.NotificationOutbox
            .Where(x => x.Id == candidate.Id && x.Status == "Pending")
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(x => x.Status, "Processing")
                .SetProperty(x => x.Attempts, x => x.Attempts + 1)
                .SetProperty(x => x.UpdatedAt, now), ct);
        if (claimed == 0) return true;

        try
        {
            var message = JsonSerializer.Deserialize<NotificationMessage>(candidate.Payload)
                ?? throw new InvalidOperationException("通知消息内容为空");

            if (!await _db.UserNotifications.AsNoTracking()
                    .AnyAsync(x => x.SourceId == candidate.SourceTaskId, ct))
            {
                await _db.UserNotifications.AddAsync(new UserNotification
                {
                    Id = Guid.NewGuid(),
                    UserId = candidate.UserId,
                    SourceId = candidate.SourceTaskId,
                    Type = message.Type,
                    Title = message.Title,
                    Message = message.Message,
                    Severity = message.Severity,
                    ActionType = message.ActionType,
                    ActionTargetId = message.ActionTargetId,
                    CreatedAt = candidate.CreatedAt,
                }, ct);
                // Persist the inbox item first. If completion fails, stale-task
                // recovery can safely retry because SourceId is idempotent.
                await _db.SaveChangesAsync(ct);
            }

            await _db.NotificationOutbox.Where(x => x.Id == candidate.Id)
                .ExecuteUpdateAsync(setters => setters
                    .SetProperty(x => x.Status, "Completed")
                    .SetProperty(x => x.LastError, (string?)null)
                    .SetProperty(x => x.UpdatedAt, DateTime.UtcNow), ct);
        }
        catch (Exception ex)
        {
            var attempts = candidate.Attempts + 1;
            await _db.NotificationOutbox.Where(x => x.Id == candidate.Id)
                .ExecuteUpdateAsync(setters => setters
                    .SetProperty(x => x.Status, attempts >= MaxAttempts ? "Failed" : "Pending")
                    .SetProperty(x => x.AvailableAt,
                        DateTime.UtcNow.AddSeconds(Math.Pow(2, attempts) * 5))
                    .SetProperty(x => x.LastError,
                        ex.Message.Length > 2000 ? ex.Message[..2000] : ex.Message)
                    .SetProperty(x => x.UpdatedAt, DateTime.UtcNow), ct);
        }

        return true;
    }
}
