using System.Text;
using LiteCqrs;
using TeleWave.Api.Common;
using TeleWave.Application.Broadcast.Bumpers;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Domain.Broadcast;
using TeleWave.Infrastructure.Identity;
using TeleWave.Infrastructure.Media;

namespace TeleWave.Api.Endpoints;

/// <summary>
/// Блоки ТВ-заставок: оформление, звук, подблоки с текстом и рендер превью. Блоки общие для всех
/// каналов, поэтому и раздел свой, не канальный — канал только ссылается на них врезками стыков.
/// </summary>
public static class BumperEndpoints
{
    public static IEndpointRouteBuilder MapBumperEndpoints(this IEndpointRouteBuilder app)
    {
        var admin = app.MapGroup("/api/admin/bumpers")
            .WithTags("Admin.Bumpers")
            .RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));

        admin.MapGet("", List).Produces<IReadOnlyList<BumperTemplateDto>>();
        admin.MapPost("", Create).Produces<CreatedIdResponse>(StatusCodes.Status201Created);
        admin.MapPut("/{templateId:guid}", Update).Produces(StatusCodes.Status204NoContent);
        admin.MapDelete("/{templateId:guid}", Delete).Produces(StatusCodes.Status204NoContent);

        admin
            .MapPut("/{templateId:guid}/audio", UploadAudio)
            .Produces(StatusCodes.Status204NoContent);
        admin
            .MapDelete("/{templateId:guid}/audio", ClearAudio)
            .Produces(StatusCodes.Status204NoContent);
        admin
            .MapPut("/{templateId:guid}/background", SetBackground)
            .Produces(StatusCodes.Status204NoContent);
        admin
            .MapDelete("/{templateId:guid}/background", ClearBackground)
            .Produces(StatusCodes.Status204NoContent);

        admin
            .MapPost("/{templateId:guid}/variants", AddVariant)
            .Produces<CreatedIdResponse>(StatusCodes.Status201Created);
        admin
            .MapPut("/{templateId:guid}/variants/{variantId:guid}", UpdateVariant)
            .Produces(StatusCodes.Status204NoContent);
        admin
            .MapDelete("/{templateId:guid}/variants/{variantId:guid}", RemoveVariant)
            .Produces(StatusCodes.Status204NoContent);

        admin
            .MapPost("/{templateId:guid}/preview", RenderPreview)
            .Produces(StatusCodes.Status204NoContent);
        admin.MapGet("/{templateId:guid}/preview/{variantId:guid}/index.m3u8", PreviewPlaylist);
        admin.MapGet("/{templateId:guid}/preview/{variantId:guid}/{file}", PreviewSegment);

        return app;
    }

    private static async Task<IResult> List(ISender sender, CancellationToken cancellationToken)
    {
        var result = await sender.Send(new ListBumperTemplatesQuery(), cancellationToken);
        return Results.Ok(result);
    }

    private static async Task<IResult> Create(
        BumperNameBody body,
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        var result = await sender.Send(
            new CreateBumperTemplateCommand(body.Name),
            cancellationToken
        );
        return result.IsSuccess
            ? Results.Created(
                $"/api/admin/bumpers/{result.Value}",
                new CreatedIdResponse(result.Value)
            )
            : result.ToHttpResult();
    }

    private static async Task<IResult> Update(
        Guid templateId,
        UpdateBumperTemplateBody body,
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        var result = await sender.Send(
            new UpdateBumperTemplateCommand(
                templateId,
                new BumperStyle(
                    body.Name,
                    body.Font,
                    body.BackgroundColor,
                    body.BackgroundColor2,
                    body.AccentColor,
                    body.TextColor
                )
            ),
            cancellationToken
        );
        return result.ToHttpResult();
    }

    private static async Task<IResult> Delete(
        Guid templateId,
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        var result = await sender.Send(
            new DeleteBumperTemplateCommand(templateId),
            cancellationToken
        );
        return result.ToHttpResult();
    }

    private static async Task<IResult> UploadAudio(
        Guid templateId,
        string fileName,
        HttpRequest request,
        IBumperTemplateStorage storage,
        IAudioProbe probe,
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        if (ResolveExtension(fileName, request) is not { } ext)
            return BumperErrors.InvalidFile.ToProblem();

        await storage.SaveAudioAsync(templateId, ext, request.Body, cancellationToken);

        // Длина заставки идёт по длине звука — замеряем ffprobe (при неудаче 0 → дефолтная длина).
        var path = storage.AudioPath(templateId, ext);
        var duration = path is null
            ? null
            : await probe.TryGetDurationAsync(path, cancellationToken);

        var result = await sender.Send(
            new SetBumperTemplateAudioCommand(templateId, ext, duration?.TotalSeconds ?? 0),
            cancellationToken
        );
        if (!result.IsSuccess)
            await storage.DeleteAudioAsync(templateId, cancellationToken);
        return result.ToHttpResult();
    }

    private static async Task<IResult> ClearAudio(
        Guid templateId,
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        var result = await sender.Send(
            new ClearBumperTemplateAudioCommand(templateId),
            cancellationToken
        );
        return result.ToHttpResult();
    }

    private static async Task<IResult> SetBackground(
        Guid templateId,
        SetBumperBackgroundBody body,
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        var result = await sender.Send(
            new SetBumperTemplateBackgroundCommand(templateId, body.ImageId),
            cancellationToken
        );
        return result.ToHttpResult();
    }

    private static async Task<IResult> ClearBackground(
        Guid templateId,
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        var result = await sender.Send(
            new ClearBumperTemplateBackgroundCommand(templateId),
            cancellationToken
        );
        return result.ToHttpResult();
    }

    private static async Task<IResult> AddVariant(
        Guid templateId,
        BumperNameBody body,
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        var result = await sender.Send(
            new AddBumperVariantCommand(templateId, body.Name),
            cancellationToken
        );
        return result.IsSuccess
            ? Results.Created(
                $"/api/admin/bumpers/{templateId}",
                new CreatedIdResponse(result.Value)
            )
            : result.ToHttpResult();
    }

    private static async Task<IResult> UpdateVariant(
        Guid templateId,
        Guid variantId,
        BumperVariantInput input,
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        var result = await sender.Send(
            new UpdateBumperVariantCommand(templateId, variantId, input),
            cancellationToken
        );
        return result.ToHttpResult();
    }

    private static async Task<IResult> RemoveVariant(
        Guid templateId,
        Guid variantId,
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        var result = await sender.Send(
            new RemoveBumperVariantCommand(templateId, variantId),
            cancellationToken
        );
        return result.ToHttpResult();
    }

    /// <summary>Синхронно рендерит пример заставки блока (несколько секунд ffmpeg).</summary>
    private static async Task<IResult> RenderPreview(
        Guid templateId,
        Guid? channelId,
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        var result = await sender.Send(
            new RenderBumperPreviewCommand(templateId, channelId),
            cancellationToken
        );
        return result.IsSuccess ? Results.NoContent() : result.ToHttpResult();
    }

    /// <summary>Плейлист превью подблока: переписываем ffmpeg-index.m3u8, направляя сегменты на admin-роут.</summary>
    private static IResult PreviewPlaylist(Guid templateId, Guid variantId, MediaPathResolver paths)
    {
        var previewId = BumperPreview.AssetId(variantId);
        if (SegmentFiles.TryResolveExisting(paths, previewId, "index.m3u8") is not { } indexPath)
            return Results.NotFound();

        var baseUrl = $"/api/admin/bumpers/{templateId}/preview/{variantId}/";
        var sb = new StringBuilder();
        foreach (var line in File.ReadLines(indexPath))
        {
            var trimmed = line.Trim();
            if (trimmed.Length == 0)
                continue;
            // Комментарии/директивы — как есть; строки-сегменты (абсолютный путь от ffmpeg) → admin-URL.
            sb.Append(trimmed.StartsWith('#') ? trimmed : baseUrl + Path.GetFileName(trimmed))
                .Append('\n');
        }

        return Results.Text(sb.ToString(), "application/vnd.apple.mpegurl");
    }

    /// <summary>
    /// Сегмент превью. Блок в маршруте есть, но хендлеру не нужен: каталог превью адресуется
    /// подблоком (см. <see cref="BumperPreview.AssetId"/>) — незаявленные параметры не связываются.
    /// </summary>
    private static IResult PreviewSegment(Guid variantId, string file, MediaPathResolver paths)
    {
        if (!SegmentFiles.IsSegmentName(file))
            return Results.NotFound();

        var previewId = BumperPreview.AssetId(variantId);
        if (SegmentFiles.TryResolveExisting(paths, previewId, file) is not { } path)
            return Results.NotFound();

        return Results.File(path, "video/mp2t", enableRangeProcessing: true);
    }

    /// <summary>Проверяет расширение файла (по allowlist) и размер (Content-Length). Возвращает
    /// нормализованное расширение (с точкой, нижний регистр) или null при отказе.</summary>
    private static string? ResolveExtension(string fileName, HttpRequest request)
    {
        if (request.ContentLength is > BumperFiles.MaxBytes or 0 or null)
            return null;
        var ext = Path.GetExtension(fileName).ToLowerInvariant();
        return BumperFiles.AudioExtensions.Contains(ext) ? ext : null;
    }
}

public sealed record BumperNameBody(string Name);

public sealed record UpdateBumperTemplateBody(
    string Name,
    BumperFont Font,
    string BackgroundColor,
    string BackgroundColor2,
    string AccentColor,
    string TextColor
);

public sealed record SetBumperBackgroundBody(Guid ImageId);

/// <summary>Ограничения на загружаемый звук блока заставки (фон-картинка — через общий реестр).</summary>
internal static class BumperFiles
{
    public const long MaxBytes = 200L * 1024 * 1024; // 200 МБ

    public static readonly IReadOnlySet<string> AudioExtensions = new HashSet<string>(
        StringComparer.OrdinalIgnoreCase
    )
    {
        ".mp3",
        ".m4a",
        ".aac",
        ".ogg",
        ".opus",
        ".wav",
    };
}
