using System.Diagnostics;
using System.Text.Json;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;

namespace Jiaowu.Api.Infrastructure.Scheduling;

public sealed class AutomaticScheduleJobProcessor(
    AppDbContext db,
    AutomaticScheduleGenerator generator,
    IServiceScopeFactory scopeFactory,
    ILogger<AutomaticScheduleJobProcessor> logger)
{
    public async Task ProcessAsync(Guid jobId, CancellationToken stoppingToken)
    {
        try
        {
            var job = await db.AutomaticScheduleJobs
                .FirstOrDefaultAsync(x => x.Id == jobId, stoppingToken);
            if (job is null ||
                job.Status is AutomaticScheduleJobStatus.Succeeded
                    or AutomaticScheduleJobStatus.Failed)
            {
                return;
            }

            var plan = await db.SchedulePlans.FirstOrDefaultAsync(
                x => x.Id == job.SchedulePlanId,
                stoppingToken);
            if (plan is null || plan.Status != SchedulePlanStatus.Draft)
                throw new InvalidOperationException("排课草稿不存在或已不允许修改。");

            job.Status = AutomaticScheduleJobStatus.Running;
            job.StartedAt = DateTime.UtcNow;
            job.CompletedAt = null;
            job.ErrorMessage = null;
            job.TotalTasks = await db.TeachingTasks.CountAsync(
                x =>
                    x.AcademicTermId == plan.AcademicTermId &&
                    x.Status == TeachingTaskStatus.Published,
                stoppingToken);
            job.ProcessedTasks = 0;
            job.CreatedEntries = 0;
            job.CompletedTasks = 0;
            job.MessagesJson = null;
            await db.SaveChangesAsync(stoppingToken);

            var progressClock = Stopwatch.StartNew();
            var lastPersistedTaskCount = 0;

            async Task ReportProgress(
                AutomaticScheduleProgress progress,
                CancellationToken cancellationToken)
            {
                var isFinal = progress.ProcessedTasks >= progress.TotalTasks;
                var hasBatch = progress.ProcessedTasks - lastPersistedTaskCount >= 5;
                if (!isFinal && !hasBatch && progressClock.ElapsedMilliseconds < 500)
                    return;

                await PersistProgressAsync(jobId, progress, cancellationToken);
                lastPersistedTaskCount = progress.ProcessedTasks;
                progressClock.Restart();
            }

            var result = await generator.GenerateAsync(
                plan,
                ReportProgress,
                false,
                stoppingToken);
            job.Status = AutomaticScheduleJobStatus.Succeeded;
            job.ActiveSchedulePlanId = null;
            job.TotalTasks = result.TotalTasks;
            job.ProcessedTasks = result.ProcessedTasks;
            job.CreatedEntries = result.CreatedEntries;
            job.CompletedTasks = result.CompletedTasks;
            job.MessagesJson = JsonSerializer.Serialize(result.Messages);
            job.CompletedAt = DateTime.UtcNow;
            await db.SaveChangesAsync(stoppingToken);

            logger.LogInformation(
                "Automatic schedule job {JobId} completed with {CreatedEntries} entries.",
                job.Id,
                result.CreatedEntries);
        }
        catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
        {
            logger.LogInformation(
                "Automatic schedule job {JobId} was interrupted by application shutdown.",
                jobId);
            throw;
        }
        catch (Exception exception)
        {
            logger.LogError(
                exception,
                "Automatic schedule job {JobId} failed.",
                jobId);
            await MarkFailedAsync(jobId, exception);
        }
    }

    private async Task PersistProgressAsync(
        Guid jobId,
        AutomaticScheduleProgress progress,
        CancellationToken cancellationToken)
    {
        try
        {
            await using var scope = scopeFactory.CreateAsyncScope();
            var progressDb = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            var updatedAt = DateTime.UtcNow;
            await progressDb.AutomaticScheduleJobs
                .Where(x =>
                    x.Id == jobId &&
                    x.Status == AutomaticScheduleJobStatus.Running)
                .ExecuteUpdateAsync(
                    setters => setters
                        .SetProperty(x => x.TotalTasks, progress.TotalTasks)
                        .SetProperty(x => x.ProcessedTasks, progress.ProcessedTasks)
                        .SetProperty(x => x.CreatedEntries, progress.CreatedEntries)
                        .SetProperty(x => x.CompletedTasks, progress.CompletedTasks)
                        .SetProperty(x => x.UpdatedAt, updatedAt),
                    cancellationToken);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch (Exception exception)
        {
            logger.LogWarning(
                exception,
                "Could not persist progress for automatic schedule job {JobId}.",
                jobId);
        }
    }

    private async Task MarkFailedAsync(Guid jobId, Exception exception)
    {
        db.ChangeTracker.Clear();
        var job = await db.AutomaticScheduleJobs.FirstOrDefaultAsync(
            x => x.Id == jobId,
            CancellationToken.None);
        if (job is null)
            return;

        var message = exception.GetBaseException().Message;
        job.Status = AutomaticScheduleJobStatus.Failed;
        job.ActiveSchedulePlanId = null;
        job.ErrorMessage = message.Length <= 2000 ? message : message[..2000];
        job.CompletedAt = DateTime.UtcNow;
        await db.SaveChangesAsync(CancellationToken.None);
    }
}
