using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.System;
using Jiaowu.Api.Infrastructure.BackgroundJobs;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;

namespace Jiaowu.Api.Infrastructure.Grades;

/// <summary>
/// Repairs missing or stale materialized grade statistics on a database-
/// configured fixed interval. Grade writes do not enqueue refresh jobs; this
/// worker batches changes made during bulk imports.
/// </summary>
public sealed class CourseGradeStatisticsRefreshWorker(
    IServiceScopeFactory scopeFactory,
    TimeProvider timeProvider,
    ILogger<CourseGradeStatisticsRefreshWorker> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        logger.LogInformation("Database-configured course grade statistics scheduler started.");
        using var timer = new PeriodicTimer(TimeSpan.FromSeconds(10), timeProvider);
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await using var scope = scopeFactory.CreateAsyncScope();
                var scheduler = scope.ServiceProvider
                    .GetRequiredService<CourseGradeStatisticsRefreshScheduler>();
                var queued = await scheduler.EnqueueDueAsync(
                    timeProvider.GetUtcNow().UtcDateTime,
                    stoppingToken);
                if (queued > 0)
                    logger.LogInformation(
                        "Scheduled course grade statistics scan queued {Count} refresh jobs.",
                        queued);
            }
            catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
            {
                break;
            }
            catch (Exception exception)
            {
                logger.LogError(exception, "Scheduled course grade statistics scan failed.");
            }

            if (!await timer.WaitForNextTickAsync(stoppingToken)) break;
        }
    }
}

public sealed class CourseGradeStatisticsRefreshScheduler(
    AppDbContext db,
    ILogger<CourseGradeStatisticsRefreshScheduler> logger)
{
    public async Task<int> EnqueueDueAsync(
        DateTime utcNow,
        CancellationToken cancellationToken) =>
        await ExecuteWithLeaseAsync(async () =>
        {
            var setting = await db.CourseGradeStatisticsRefreshSettings
                .SingleOrDefaultAsync(
                    x => x.Key == CourseGradeStatisticsRefreshSetting.DefaultKey,
                    cancellationToken);
            if (setting is null)
            {
                setting = new CourseGradeStatisticsRefreshSetting();
                db.CourseGradeStatisticsRefreshSettings.Add(setting);
            }

            var interval = TimeSpan.FromSeconds(
                Math.Clamp(setting.IntervalSeconds, 10, 86400));
            if (!setting.IsEnabled ||
                setting.LastRunAt.HasValue && utcNow < setting.LastRunAt.Value + interval)
            {
                if (db.Entry(setting).State == EntityState.Added)
                    await db.SaveChangesAsync(cancellationToken);
                return 0;
            }

            setting.LastRunAt = utcNow;
            var queued = await EnqueueStaleCoreAsync(setting.BatchSize, cancellationToken);
            await db.SaveChangesAsync(cancellationToken);
            return queued;
        }, cancellationToken);

    public async Task<int> EnqueueStaleAsync(
        int batchSize,
        CancellationToken cancellationToken) =>
        await ExecuteWithLeaseAsync(async () =>
        {
            var queued = await EnqueueStaleCoreAsync(batchSize, cancellationToken);
            if (queued > 0) await db.SaveChangesAsync(cancellationToken);
            return queued;
        }, cancellationToken);

    private async Task<int> ExecuteWithLeaseAsync(
        Func<Task<int>> action,
        CancellationToken cancellationToken)
    {
        var usesMySqlLease = db.Database.ProviderName?.Contains(
            "MySql",
            StringComparison.OrdinalIgnoreCase) == true;
        if (usesMySqlLease && !await TryAcquireMySqlLeaseAsync(cancellationToken))
        {
            await db.Database.CloseConnectionAsync();
            logger.LogDebug("Another instance owns the grade statistics refresh lease.");
            return 0;
        }

        try
        {
            return await action();
        }
        finally
        {
            if (usesMySqlLease)
                await ReleaseMySqlLeaseAsync();
        }
    }

    private async Task<int> EnqueueStaleCoreAsync(
        int batchSize,
        CancellationToken cancellationToken)
    {
        batchSize = Math.Clamp(batchSize, 1, 5000);
        var activeTargets = await (
                from job in db.CourseGradeStatisticsRefreshJobs.AsNoTracking()
                join sheet in db.GradeSheets.AsNoTracking()
                    on job.GradeSheetId equals sheet.Id
                where job.Status == CourseGradeStatisticsRefreshJobStatus.Queued ||
                      job.Status == CourseGradeStatisticsRefreshJobStatus.Running
                select new CourseTermTarget(
                    sheet.TeachingTask!.CourseId,
                    sheet.TeachingTask.AcademicTermId))
            .Distinct()
            .ToListAsync(cancellationToken);
        var active = activeTargets.ToHashSet();

        var rows = await db.GradeSheets.AsNoTracking()
            .Where(sheet =>
                sheet.Status == GradeSheetStatus.Published &&
                sheet.Records.Any(record => record.TotalScore != null))
            .Select(sheet => new RefreshCandidate(
                sheet.Id,
                sheet.TeachingTask!.CourseId,
                sheet.TeachingTask.AcademicTermId,
                sheet.UpdatedAt,
                sheet.Records
                    .Where(record => record.TotalScore != null)
                    .Max(record => record.UpdatedAt),
                db.TeachingTaskGradeStatistics
                    .Where(statistic => statistic.GradeSheetId == sheet.Id)
                    .Select(statistic => (DateTime?)statistic.CalculatedAt)
                    .FirstOrDefault()))
            .ToListAsync(cancellationToken);

        var stale = rows
            .Where(row =>
                row.CalculatedAt is null ||
                row.SheetUpdatedAt > row.CalculatedAt ||
                row.RecordsUpdatedAt > row.CalculatedAt)
            .GroupBy(row => new CourseTermTarget(row.CourseId, row.AcademicTermId))
            .Where(group => !active.Contains(group.Key))
            .Select(group => group
                .OrderByDescending(row => row.RecordsUpdatedAt)
                .ThenByDescending(row => row.SheetUpdatedAt)
                .First())
            .Take(batchSize)
            .ToArray();

        foreach (var candidate in stale)
        {
            var job = new CourseGradeStatisticsRefreshJob
            {
                GradeSheetId = candidate.GradeSheetId
            };
            db.CourseGradeStatisticsRefreshJobs.Add(job);
            db.BackgroundJobOutboxMessages.Add(BackgroundJobOutboxMessage.Create(
                BackgroundJobKind.CourseGradeStatisticsRefresh,
                job.Id));
        }

        if (stale.Length == 0) return 0;
        logger.LogDebug(
            "Queued {Count} stale course grade statistics targets.",
            stale.Length);
        return stale.Length;
    }

    private async Task<bool> TryAcquireMySqlLeaseAsync(
        CancellationToken cancellationToken)
    {
        await db.Database.OpenConnectionAsync(cancellationToken);
        await using var command = db.Database.GetDbConnection().CreateCommand();
        command.CommandText = "SELECT GET_LOCK('jiaowu:grade-statistics-refresh', 0);";
        var result = await command.ExecuteScalarAsync(cancellationToken);
        return Convert.ToInt32(result) == 1;
    }

    private async Task ReleaseMySqlLeaseAsync()
    {
        try
        {
            await using var command = db.Database.GetDbConnection().CreateCommand();
            command.CommandText = "SELECT RELEASE_LOCK('jiaowu:grade-statistics-refresh');";
            await command.ExecuteScalarAsync(CancellationToken.None);
        }
        catch (Exception exception)
        {
            logger.LogWarning(exception, "Failed to release grade statistics refresh lease.");
        }
        finally
        {
            await db.Database.CloseConnectionAsync();
        }
    }

    private sealed record RefreshCandidate(
        Guid GradeSheetId,
        Guid CourseId,
        Guid AcademicTermId,
        DateTime SheetUpdatedAt,
        DateTime RecordsUpdatedAt,
        DateTime? CalculatedAt);

    private sealed record CourseTermTarget(Guid CourseId, Guid AcademicTermId);
}
