using System.Globalization;
using System.IO.Compression;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using LiteCqrs;
using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Broadcast;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Common.Models;
using TeleWave.Domain.Programming;

namespace TeleWave.Application.Programming.Planning.DebugExport;

/// <summary>
/// Пакует снимок канала в архив и, если настроен каталог, оставляет копию на сервере.
///
/// Zip, а не один большой JSON: файлов десяток, каждый читают по отдельности, а лента и группы
/// сжимаются в разы — дамп канала на неделю это мегабайты текста.
/// </summary>
public sealed class ExportChannelDebugCommandHandler(
    IAppDbContext dbContext,
    ChannelDebugCollector collector,
    IDebugExportStore store
) : ICommandHandler<ExportChannelDebugCommand, Result<DebugExportDto>>
{
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        WriteIndented = true,
        // Циклы в дампе возможны (навигации EF), и падать на них нельзя: дамп собирают тогда,
        // когда уже что-то не так.
        ReferenceHandler = ReferenceHandler.IgnoreCycles,
        Converters = { new JsonStringEnumConverter() },
        Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
    };

    public async Task<Result<DebugExportDto>> Handle(
        ExportChannelDebugCommand command,
        CancellationToken cancellationToken
    )
    {
        var channel = await dbContext
            .Channels.AsNoTracking()
            .FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
        if (channel is null)
            return Result.Failure<DebugExportDto>(ChannelErrors.NotFound);

        var template = channel.TemplateId is not { } templateId
            ? null
            : await dbContext
                .ScheduleTemplates.AsNoTracking()
                .Include(t => t.Layers)
                    .ThenInclude(l => l.Slots)
                .AsSplitQuery()
                .FirstOrDefaultAsync(t => t.Id == templateId, cancellationToken);
        if (template is null)
            return Result.Failure<DebugExportDto>(ChannelErrors.TemplateNotFound);

        var now = DateTimeOffset.UtcNow;
        var files = await collector.CollectAsync(channel, template, now, cancellationToken);

        var fileName = FileName(channel.Slug, now);
        var content = Pack(files);
        var savedTo = await store.SaveAsync(fileName, content, cancellationToken);

        return Result.Success(new DebugExportDto(fileName, content, savedTo));
    }

    /// <summary>Имя архива: канал и время сборки — дампы одного канала копятся десятками.</summary>
    private static string FileName(string slug, DateTimeOffset now) =>
        $"telewave-debug-{slug}-{now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture)}.zip";

    private static byte[] Pack(IReadOnlyList<DebugFile> files)
    {
        using var buffer = new MemoryStream();
        using (var archive = new ZipArchive(buffer, ZipArchiveMode.Create, leaveOpen: true))
        {
            foreach (var file in files)
            {
                var entry = archive.CreateEntry(file.Name, CompressionLevel.Optimal);
                using var stream = entry.Open();
                using var writer = new StreamWriter(stream, Encoding.UTF8);
                writer.Write(
                    file.Payload is string text
                        ? text
                        : JsonSerializer.Serialize(file.Payload, JsonOptions)
                );
            }
        }

        return buffer.ToArray();
    }
}
