using Health.Application.Medications;
using Health.Infrastructure.Data.Records;
using Npgsql;

namespace Health.Infrastructure.Medications;

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

    public async Task<bool> EnqueueAsync(MedicationReminderTask task, CancellationToken ct)
    {
        var exists = await _db.MedicationReminderTasks.AsNoTracking().AnyAsync(x =>
            x.MedicationId == task.MedicationId &&
            x.ScheduledDate == task.ScheduledDate &&
            x.ScheduledTime == task.ScheduledTime, ct);
        if (exists) return false;

        var now = DateTime.UtcNow;
        await _db.MedicationReminderTasks.AddAsync(new MedicationReminderTaskRecord
        {
            Id = task.TaskId,
            UserId = task.UserId,
            MedicationId = task.MedicationId,
            Name = task.Name,
            Dosage = task.Dosage,
            ScheduledDate = task.ScheduledDate,
            ScheduledTime = task.ScheduledTime,
            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 async Task RecoverStaleAsync(CancellationToken ct)
    {
        var now = DateTime.UtcNow;
        var staleCutoff = now.AddMinutes(-5);

        // 超时但仍有重试余量 → 重新排队
        await _db.MedicationReminderTasks
            .Where(x => x.Status == "Processing" && x.UpdatedAt < staleCutoff && x.Attempts < MaxAttempts)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(x => x.Status, "Pending")
                .SetProperty(x => x.AvailableAt, now)
                .SetProperty(x => x.UpdatedAt, now), ct);

        // 超时且已达重试上限（多为最后一次执行中进程崩溃）→ 标记失败，避免永久卡在 Processing
        await _db.MedicationReminderTasks
            .Where(x => x.Status == "Processing" && x.UpdatedAt < staleCutoff && x.Attempts >= MaxAttempts)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(x => x.Status, "Failed")
                .SetProperty(x => x.LastError, "任务执行超时且已达最大重试次数")
                .SetProperty(x => x.UpdatedAt, now), ct);
    }

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

        var claimed = await _db.MedicationReminderTasks
            .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 null;

        return new MedicationReminderTask(
            candidate.Id,
            candidate.UserId,
            candidate.MedicationId,
            candidate.Name,
            candidate.Dosage,
            candidate.ScheduledDate,
            candidate.ScheduledTime);
    }

    public Task CompleteAsync(Guid taskId, CancellationToken ct) =>
        _db.MedicationReminderTasks.Where(x => x.Id == taskId)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(x => x.Status, "Completed")
                .SetProperty(x => x.UpdatedAt, DateTime.UtcNow), ct);

    public async Task RetryAsync(Guid taskId, string error, CancellationToken ct)
    {
        var task = await _db.MedicationReminderTasks.FirstOrDefaultAsync(x => x.Id == taskId, ct);
        if (task == null) return;

        task.LastError = error.Length > 2000 ? error[..2000] : error;
        task.UpdatedAt = DateTime.UtcNow;
        if (task.Attempts >= MaxAttempts)
        {
            task.Status = "Failed";
        }
        else
        {
            task.Status = "Pending";
            task.AvailableAt = DateTime.UtcNow.AddSeconds(Math.Pow(2, task.Attempts) * 5);
        }
        await _db.SaveChangesAsync(ct);
    }
}
