using LiteCqrs;
using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Domain.Media;

namespace TeleWave.Application.Media.Stats;

public sealed class GetMediaStatsQueryHandler(
    IAppDbContext dbContext,
    IMediaProcessingLimits limits
) : IQueryHandler<GetMediaStatsQuery, MediaStatsDto>
{
    /// <summary>По скольким последним завершённым ассетам усредняем время обработки.</summary>
    private const int AverageSample = 500;

    public async Task<MediaStatsDto> Handle(
        GetMediaStatsQuery query,
        CancellationToken cancellationToken
    )
    {
        // Сгенерированные (ТВ-заставки) в статистику библиотеки не входят.
        var assets = dbContext
            .MediaAssets.AsNoTracking()
            .Where(x => x.Source != MediaSource.Generated);

        var queued = await assets.CountAsync(
            x => x.Status == MediaAssetStatus.Pending,
            cancellationToken
        );
        var processing = await assets.CountAsync(
            x => x.Status == MediaAssetStatus.Processing,
            cancellationToken
        );

        // Среднее — по недавним завершённым (ограничение выборки, чтобы не тянуть всю историю). EF не
        // усредняет TimeSpan в SQL, поэтому берём длительности последних N и усредняем в памяти.
        var recent = await assets
            .Where(x => x.Status == MediaAssetStatus.Ready && x.ProcessingDuration != null)
            .OrderByDescending(x => x.UpdatedAt)
            .Select(x => x.ProcessingDuration!.Value)
            .Take(AverageSample)
            .ToListAsync(cancellationToken);
        double? average = recent.Count > 0 ? recent.Average(d => d.TotalSeconds) : null;

        var estimate = await EstimateRemainingAsync(
            assets,
            queued,
            processing,
            average,
            cancellationToken
        );

        return new MediaStatsDto(queued, processing, average, estimate);
    }

    /// <summary>
    /// Сколько ещё крутиться очереди. Считается как суммарная оставшаяся работа, делённая на число
    /// параллельных транскодов: у ждущих в запасе целое среднее, у уже начатых — среднее минус то,
    /// что они уже отработали.
    ///
    /// Оценка, а не обещание: длинный фильм после десятка роликов сдвинет её вверх. Но и без неё
    /// «492 в очереди» не говорит администратору ничего — это может быть и час, и трое суток.
    /// </summary>
    private async Task<double?> EstimateRemainingAsync(
        IQueryable<MediaAsset> assets,
        int queued,
        int processing,
        double? average,
        CancellationToken cancellationToken
    )
    {
        if (average is not { } avg || queued + processing == 0)
            return null;

        var startedAt = await assets
            .Where(x => x.Status == MediaAssetStatus.Processing && x.ProcessingStartedAt != null)
            .Select(x => x.ProcessingStartedAt!.Value)
            .ToListAsync(cancellationToken);

        var now = DateTimeOffset.UtcNow;
        var inFlight = startedAt.Sum(started => Math.Max(0, avg - (now - started).TotalSeconds));

        // Ассет в обработке без отметки старта (пережил перезапуск) считаем целой работой: занизить
        // ожидание хуже, чем завысить.
        var unknown = (processing - startedAt.Count) * avg;

        return (queued * avg + inFlight + unknown) / limits.MaxParallelTranscodes;
    }
}
