using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Excititor.Worker.Options;

namespace StellaOps.Excititor.Worker.Scheduling;

internal sealed class VexWorkerHostedService : BackgroundService
{
    private readonly IOptions<VexWorkerOptions> _options;
    private readonly IVexProviderRunner _runner;
    private readonly ILogger<VexWorkerHostedService> _logger;
    private readonly TimeProvider _timeProvider;

    public VexWorkerHostedService(
        IOptions<VexWorkerOptions> options,
        IVexProviderRunner runner,
        ILogger<VexWorkerHostedService> logger,
        TimeProvider timeProvider)
    {
        _options = options ?? throw new ArgumentNullException(nameof(options));
        _runner = runner ?? throw new ArgumentNullException(nameof(runner));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var schedules = _options.Value.ResolveSchedules();
        if (schedules.Count == 0)
        {
            _logger.LogWarning("Excititor worker has no configured provider schedules; the service will remain idle.");
            await Task.CompletedTask;
            return;
        }

        _logger.LogInformation("Excititor worker starting with {ProviderCount} provider schedule(s).", schedules.Count);

        var tasks = new List<Task>(schedules.Count);
        foreach (var schedule in schedules)
        {
            tasks.Add(RunScheduleAsync(schedule, stoppingToken));
        }

        await Task.WhenAll(tasks);
    }

    private async Task RunScheduleAsync(VexWorkerSchedule schedule, CancellationToken cancellationToken)
    {
        try
        {
            if (schedule.InitialDelay > TimeSpan.Zero)
            {
                _logger.LogInformation(
                    "Provider {ProviderId} initial delay of {InitialDelay} before first execution.",
                    schedule.ProviderId,
                    schedule.InitialDelay);

                await Task.Delay(schedule.InitialDelay, cancellationToken).ConfigureAwait(false);
            }

            using var timer = new PeriodicTimer(schedule.Interval);
            do
            {
                var startedAt = _timeProvider.GetUtcNow();
                _logger.LogInformation(
                    "Provider {ProviderId} run started at {StartedAt}. Interval={Interval}.",
                    schedule.ProviderId,
                    startedAt,
                    schedule.Interval);

                try
                {
                    await _runner.RunAsync(schedule.ProviderId, cancellationToken).ConfigureAwait(false);

                    var completedAt = _timeProvider.GetUtcNow();
                    var elapsed = completedAt - startedAt;

                    _logger.LogInformation(
                        "Provider {ProviderId} run completed at {CompletedAt} (duration {Duration}).",
                        schedule.ProviderId,
                        completedAt,
                        elapsed);
                }
                catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
                {
                    _logger.LogInformation("Provider {ProviderId} run cancelled.", schedule.ProviderId);
                    break;
                }
                catch (Exception ex)
                {
                    _logger.LogError(
                        ex,
                        "Provider {ProviderId} run failed: {Message}",
                        schedule.ProviderId,
                        ex.Message);
                }
            }
            while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false));
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            _logger.LogInformation("Provider {ProviderId} schedule cancelled.", schedule.ProviderId);
        }
    }
}
