#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Baya.Application.Contracts.Payments;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Baya.Infrastructure.Persistence.Services.Scheduling;

/// <summary>
/// The single in-process job scheduler (refinement-phase-7): it drives every registered <see cref="IRecurringJob"/>
/// on its own cadence, replacing the two hand-written <c>PeriodicTimer</c> hosted services and giving the four
/// previously admin-manual sweeps (credential-expiry scan, EVV no-show sweep, weekly payout-batch generation,
/// and — Phase 8 — the Moadian reconciliation poll) a schedule. It intentionally uses <b>no new infrastructure</b>:
/// SQL Server stays the only external dependency, so a single-instance MVP needs neither Hangfire/Quartz nor Redis.
///
/// <para><b>Multi-instance readiness.</b> Each tick runs under <see cref="IDistributedLock"/>
/// (<c>scheduler:{Name}</c>). Today that lock is in-process (a no-op across instances); the moment a second API
/// instance runs it becomes the scale-out gate — swapping the lock seam to Redis serializes ticks across nodes.
/// Because every job is idempotent and the DB uniques/state-machines are authoritative, even a double-run is safe.</para>
///
/// <para><b>Not started under the "Testing" environment</b> so integration tests (WebApplicationFactory over
/// in-memory SQLite) stay deterministic — no background sweep mutates rows mid-assertion. Each job's underlying
/// command/service is unit-tested directly instead.</para>
/// </summary>
internal sealed class RecurringJobSchedulerHostedService(
    IServiceScopeFactory scopeFactory,
    IEnumerable<IRecurringJob> jobs,
    IDistributedLock distributedLock,
    IHostEnvironment environment,
    ILogger<RecurringJobSchedulerHostedService> logger) : BackgroundService
{
    // Floor so a mis-set cadence key (0 or negative) can never turn a loop into a hot spin.
    private static readonly TimeSpan MinInterval = TimeSpan.FromSeconds(10);
    // Used when a job's interval resolution throws (e.g. the config store is briefly unreachable).
    private static readonly TimeSpan FallbackInterval = TimeSpan.FromMinutes(5);

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        if (environment.IsEnvironment("Testing"))
            return;

        var jobList = jobs.ToArray();
        logger.LogInformation("Recurring job scheduler starting with {Count} job(s): {Jobs}",
            jobList.Length, string.Join(", ", jobList.Select(j => j.Name)));

        // One independent loop per job so their cadences don't couple. A crash in one loop never stops the others.
        await Task.WhenAll(jobList.Select(job => RunJobLoopAsync(job, stoppingToken)));
    }

    private async Task RunJobLoopAsync(IRecurringJob job, CancellationToken stoppingToken)
    {
        // Run once at startup, then on the job's own (re-read) cadence.
        while (!stoppingToken.IsCancellationRequested)
        {
            var interval = await TickAndResolveIntervalAsync(job, stoppingToken);

            try
            {
                await Task.Delay(interval, stoppingToken);
            }
            catch (OperationCanceledException)
            {
                return; // Host is shutting down.
            }
        }
    }

    private async Task<TimeSpan> TickAndResolveIntervalAsync(IRecurringJob job, CancellationToken stoppingToken)
    {
        await using var scope = scopeFactory.CreateAsyncScope();
        var services = scope.ServiceProvider;

        await RunTickAsync(job, services, stoppingToken);
        return await ResolveIntervalAsync(job, services, stoppingToken);
    }

    private async Task RunTickAsync(IRecurringJob job, IServiceProvider services, CancellationToken stoppingToken)
    {
        try
        {
            await using var handle = await distributedLock.AcquireAsync($"scheduler:{job.Name}", stoppingToken);
            await job.RunAsync(services, stoppingToken);
        }
        catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
        {
            // Host is shutting down — expected, don't log as an error.
        }
        catch (Exception ex)
        {
            // A failing tick must never kill the loop — the next tick retries on schedule.
            logger.LogError(ex, "Recurring job {Job} failed", job.Name);
        }
    }

    private async Task<TimeSpan> ResolveIntervalAsync(IRecurringJob job, IServiceProvider services, CancellationToken stoppingToken)
    {
        try
        {
            var interval = await job.GetIntervalAsync(services, stoppingToken);
            return interval < MinInterval ? MinInterval : interval;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Recurring job {Job} interval resolution failed; using fallback {Fallback}", job.Name, FallbackInterval);
            return FallbackInterval;
        }
    }
}
