using Cronos;
using System.Diagnostics;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace StellaOps.Feedser.Core.Jobs;

/// <summary>
/// Background service that evaluates cron expressions for registered jobs and triggers them.
/// </summary>
public sealed class JobSchedulerHostedService : BackgroundService
{
    private readonly IJobCoordinator _coordinator;
    private readonly JobSchedulerOptions _options;
    private readonly ILogger<JobSchedulerHostedService> _logger;
    private readonly TimeProvider _timeProvider;
    private readonly JobDiagnostics _diagnostics;
    private readonly Dictionary<string, CronExpression> _cronExpressions = new(StringComparer.Ordinal);
    private readonly Dictionary<string, DateTimeOffset> _nextOccurrences = new(StringComparer.Ordinal);

    public JobSchedulerHostedService(
        IJobCoordinator coordinator,
        IOptions<JobSchedulerOptions> optionsAccessor,
        ILogger<JobSchedulerHostedService> logger,
        TimeProvider timeProvider,
        JobDiagnostics diagnostics)
    {
        _coordinator = coordinator ?? throw new ArgumentNullException(nameof(coordinator));
        _options = (optionsAccessor ?? throw new ArgumentNullException(nameof(optionsAccessor))).Value;
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
        _diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));

        foreach (var definition in _options.Definitions.Values)
        {
            if (string.IsNullOrWhiteSpace(definition.CronExpression))
            {
                continue;
            }

            try
            {
                var cron = CronExpression.Parse(definition.CronExpression!, CronFormat.Standard);
                _cronExpressions[definition.Kind] = cron;
            }
            catch (CronFormatException ex)
            {
                _logger.LogError(ex, "Invalid cron expression '{Cron}' for job {Kind}", definition.CronExpression, definition.Kind);
            }
        }
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        if (_cronExpressions.Count == 0)
        {
            _logger.LogInformation("No cron-based jobs registered; scheduler idle.");
            await Task.Delay(Timeout.Infinite, stoppingToken).ConfigureAwait(false);
            return;
        }

        while (!stoppingToken.IsCancellationRequested)
        {
            var now = _timeProvider.GetUtcNow();
            var nextWake = now.AddMinutes(5); // default sleep when nothing scheduled

            foreach (var (kind, cron) in _cronExpressions)
            {
                if (!_options.Definitions.TryGetValue(kind, out var definition) || !definition.Enabled)
                {
                    continue;
                }

                var next = GetNextOccurrence(kind, cron, now);
                if (next <= now.AddMilliseconds(500))
                {
                    _ = TriggerJobAsync(kind, next, stoppingToken);
                    _nextOccurrences[kind] = GetNextOccurrence(kind, cron, now.AddSeconds(1));
                    next = _nextOccurrences[kind];
                }

                if (next < nextWake)
                {
                    nextWake = next;
                }
            }

            var delay = nextWake - now;
            if (delay < TimeSpan.FromSeconds(1))
            {
                delay = TimeSpan.FromSeconds(1);
            }

            try
            {
                await Task.Delay(delay, stoppingToken).ConfigureAwait(false);
            }
            catch (TaskCanceledException)
            {
                break;
            }
        }
    }

    private DateTimeOffset GetNextOccurrence(string kind, CronExpression cron, DateTimeOffset reference)
    {
        if (_nextOccurrences.TryGetValue(kind, out var cached) && cached > reference)
        {
            return cached;
        }

        var next = cron.GetNextOccurrence(reference.UtcDateTime, TimeZoneInfo.Utc);
        if (next is null)
        {
            // No future occurrence; schedule far in future to avoid tight loop.
            next = reference.UtcDateTime.AddYears(100);
        }

        var nextUtc = DateTime.SpecifyKind(next.Value, DateTimeKind.Utc);
        var offset = new DateTimeOffset(nextUtc);
        _nextOccurrences[kind] = offset;
        return offset;
    }

    private async Task TriggerJobAsync(string kind, DateTimeOffset scheduledFor, CancellationToken stoppingToken)
    {
        var invokedAt = _timeProvider.GetUtcNow();
        _diagnostics.RecordSchedulerSkew(kind, scheduledFor, invokedAt);

        using var activity = _diagnostics.StartSchedulerActivity(kind, scheduledFor, invokedAt);
        try
        {
            var result = await _coordinator.TriggerAsync(kind, parameters: null, trigger: "scheduler", stoppingToken).ConfigureAwait(false);
            activity?.SetTag("job.trigger.outcome", result.Outcome.ToString());
            if (result.Run is not null)
            {
                activity?.SetTag("job.run_id", result.Run.RunId);
            }
            if (!string.IsNullOrWhiteSpace(result.ErrorMessage))
            {
                activity?.SetTag("job.trigger.error", result.ErrorMessage);
            }

            if (result.Outcome == JobTriggerOutcome.Accepted)
            {
                activity?.SetStatus(ActivityStatusCode.Ok);
            }
            else
            {
                activity?.SetStatus(ActivityStatusCode.Ok, result.Outcome.ToString());
            }

            if (result.Outcome != JobTriggerOutcome.Accepted)
            {
                _logger.LogDebug("Scheduler trigger for {Kind} resulted in {Outcome}", kind, result.Outcome);
            }
        }
        catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            _logger.LogError(ex, "Cron trigger for job {Kind} failed", kind);
        }
    }
}
