using LiteCqrs;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Media;
using TeleWave.Application.Media.MovieImport;
using TeleWave.Application.Media.Register;
using TeleWave.Domain.Media;

namespace TeleWave.Infrastructure.Media;

/// <summary>
/// Периодически сканирует inbox/: файлы с допустимым расширением и стабильным размером (не растут
/// между тиками — значит докопировались) регистрируются как ассеты и уходят в очередь обработки.
/// Регистрация переносит файл в originals/, поэтому повторно он не подхватывается.
/// </summary>
public sealed class InboxScannerBackgroundService(
    IServiceScopeFactory scopeFactory,
    IMediaProcessingQueue queue,
    MediaPathResolver paths,
    IOptions<MediaOptions> mediaOptions,
    ILogger<InboxScannerBackgroundService> logger
) : BackgroundService
{
    private readonly MediaOptions _media = mediaOptions.Value;
    private readonly Dictionary<string, long> _lastSizes = new(StringComparer.Ordinal);

    // Файлы, регистрация которых стабильно отклоняется командой (например, неподдерживаемый контейнер):
    // помечаем, чтобы не пытаться регистрировать их заново каждые два тика и не засорять лог. Забываются,
    // когда файл исчезает из inbox (тогда одноимённый новый файл снова получит попытку).
    private readonly HashSet<string> _failed = new(StringComparer.Ordinal);

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        paths.EnsureDirectories();
        using var timer = new PeriodicTimer(
            TimeSpan.FromSeconds(Math.Max(1, _media.InboxScanSeconds))
        );

        while (await timer.WaitForNextTickAsync(stoppingToken))
        {
            try
            {
                await ScanAsync(stoppingToken);
            }
            catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
            {
                break;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Ошибка сканирования inbox");
            }
        }
    }

    private async Task ScanAsync(CancellationToken cancellationToken)
    {
        if (!Directory.Exists(paths.InboxDir))
            return;

        var files = Directory
            .EnumerateFiles(paths.InboxDir)
            .Where(f => MediaFormats.IsAllowed(f))
            .ToList();

        var present = new HashSet<string>(StringComparer.Ordinal);

        foreach (var path in files)
        {
            var name = Path.GetFileName(path);
            present.Add(name);

            if (_failed.Contains(name))
                continue; // уже пытались и получили отказ — ждём, пока файл уберут/заменят

            long size;
            try
            {
                size = new FileInfo(path).Length;
            }
            catch (IOException)
            {
                continue; // файл ещё пишется/заблокирован — попробуем на следующем тике
            }

            if (size <= 0)
            {
                _lastSizes[name] = size;
                continue;
            }

            if (!_lastSizes.TryGetValue(name, out var previous) || previous != size)
            {
                _lastSizes[name] = size; // размер меняется — ждём стабилизации
                continue;
            }

            var registered = await RegisterAsync(name, cancellationToken);
            _lastSizes.Remove(name);
            if (!registered)
                _failed.Add(name); // команда отклонила файл — больше не пытаемся, пока он в inbox
        }

        // Забываем исчезнувшие файлы, чтобы словари не росли (и одноимённый новый файл снова попробовался).
        foreach (var stale in _lastSizes.Keys.Where(k => !present.Contains(k)).ToList())
            _lastSizes.Remove(stale);
        foreach (var stale in _failed.Where(k => !present.Contains(k)).ToList())
            _failed.Remove(stale);
    }

    private async Task<bool> RegisterAsync(string fileName, CancellationToken cancellationToken)
    {
        await using var scope = scopeFactory.CreateAsyncScope();
        var sender = scope.ServiceProvider.GetRequiredService<ISender>();

        var result = await sender.Send(
            new RegisterMediaAssetCommand(fileName, MediaSource.Inbox, fileName),
            cancellationToken
        );

        if (result.IsSuccess)
        {
            queue.Enqueue(result.Value);
            logger.LogInformation(
                "Из inbox зарегистрирован ассет {AssetId} ({File})",
                result.Value,
                fileName
            );
            await AttachMovieAsync(sender, result.Value, fileName, cancellationToken);
            return true;
        }

        logger.LogWarning(
            "Не удалось зарегистрировать {File} из inbox: {Error}",
            fileName,
            result.Error.Code
        );
        return false;
    }

    /// <summary>
    /// Пробует завести фильм по имени файла. Неуверенное совпадение — не ошибка: ассет останется
    /// без шоу и попадёт в разбор. Сбой источника метаданных тоже не должен ронять сканер — файл
    /// уже зарегистрирован, и это главное.
    /// </summary>
    private async Task AttachMovieAsync(
        ISender sender,
        Guid assetId,
        string fileName,
        CancellationToken cancellationToken
    )
    {
        if (!_media.InboxAutoMovies)
            return;

        try
        {
            var attached = await sender.Send(
                new AutoAttachMovieCommand(assetId, fileName),
                cancellationToken
            );

            if (attached is { IsSuccess: true, Value: true })
                logger.LogInformation("По файлу {File} заведён фильм из inbox", fileName);
        }
        catch (Exception ex) when (ex is not OperationCanceledException)
        {
            logger.LogWarning(ex, "Не удалось опознать фильм по имени {File}", fileName);
        }
    }
}
