using Hangfire;
using Hangfire.Common;
using Hangfire.States;
using KArtSell.BuildingBlocks.Time;
using KArtSell.Modules.ModelOperations.Application;
using KArtSell.Modules.ModelOperations.Domain;
using Microsoft.Extensions.Logging;

namespace KArtSell.Modules.ModelOperations.Scheduling;

public sealed class ModelOperationsDispatcherJob(
    IModelScheduleRepository schedules,
    IBackgroundJobClient jobs,
    IClock clock,
    ILogger<ModelOperationsDispatcherJob> logger)
{
    private static readonly Action<ILogger, string, string, Exception?> LogDispatchFailed =
        LoggerMessage.Define<string, string>(
            LogLevel.Error,
            new EventId(1, nameof(LogDispatchFailed)),
            "Failed to dispatch model operation {OperationCode} for {ScopeKey}.");

    [Queue("q-control")]
    [DisableConcurrentExecution(timeoutInSeconds: 840)]
    [AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
    public async Task ExecuteAsync()
    {
        var now = clock.UtcNow;
        var leaseOwner = $"dispatcher:{Environment.MachineName}:{Guid.NewGuid():N}";
        var due = await schedules.AcquireDueAsync(now, leaseOwner, TimeSpan.FromMinutes(14), 50, CancellationToken.None);

        foreach (var item in due)
        {
            try
            {
                var plan = ScheduleOccurrencePlanner.Plan(item.ScheduledFor, item.Cadence, item.CatchUpPolicy, item.MaxCatchUp, now);
                if (plan.OccurrencesToDispatch.Count == 0)
                {
                    await schedules.AdvanceWithoutDispatchAsync(item.ScheduleId, leaseOwner, item.DispatchRevision, now, plan.NextDueAt, CancellationToken.None);
                    continue;
                }

                var backgroundJobId = string.Empty;
                foreach (var occurrence in plan.OccurrencesToDispatch)
                {
                    var idempotencyKey = $"{item.OperationCode}:{item.ScopeKey}:{item.ScheduleVersion}:{occurrence.UtcDateTime:yyyyMMddHHmmss}Z";
                    var job = Job.FromExpression<ScheduledModelOperationJob>(handler => handler.ExecuteAsync(
                        item.ScheduleId,
                        item.OperationCode,
                        item.ScopeKey,
                        item.AutomationMode,
                        idempotencyKey,
                        occurrence,
                        item.CatchUpPolicy,
                        item.MaxCatchUp));
                    backgroundJobId = jobs.Create(job, new EnqueuedState(item.Queue));
                }

                await schedules.MarkDispatchedAsync(
                    item.ScheduleId,
                    leaseOwner,
                    item.DispatchRevision,
                    backgroundJobId,
                    now,
                    plan.NextDueAt,
                    CancellationToken.None);
            }
            catch (Exception exception)
            {
                LogDispatchFailed(logger, item.OperationCode, item.ScopeKey, exception);
                await schedules.ReleaseAsync(
                    item.ScheduleId,
                    leaseOwner,
                    item.DispatchRevision,
                    "DISPATCH_FAILED",
                    now,
                    CancellationToken.None);
            }
        }
    }
}
