using System.Text;
using LiteCqrs;
using Microsoft.Extensions.Options;
using TeleWave.Api.Common;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Common.Models;
using TeleWave.Application.Media;
using TeleWave.Application.Media.Delete;
using TeleWave.Application.Media.ListMedia;
using TeleWave.Application.Media.ManualInbox;
using TeleWave.Application.Media.MovieImport;
using TeleWave.Application.Media.Register;
using TeleWave.Application.Media.Stats;
using TeleWave.Domain.Media;
using TeleWave.Infrastructure.Identity;
using TeleWave.Infrastructure.Media;

namespace TeleWave.Api.Endpoints;

public static class MediaEndpoints
{
    public static IEndpointRouteBuilder MapMediaEndpoints(this IEndpointRouteBuilder app)
    {
        var admin = app.MapGroup("/api/admin/media")
            .WithTags("Admin.Media")
            .RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));

        admin.MapPost("", Upload).Produces<UploadMediaResponse>(StatusCodes.Status201Created);
        admin.MapGet("", List).Produces<PagedList<MediaAssetDto>>();
        admin.MapGet("/stats", Stats).Produces<MediaStatsDto>();
        admin.MapDelete("/{id:guid}", Delete).Produces(StatusCodes.Status204NoContent);

        // Ручной inbox: сканером не разбирается — файлы выбирает админ и сразу указывает шоу.
        admin.MapGet("/manual", ListManual).Produces<ManualInboxListDto>();
        admin.MapPost("/manual/import", ImportManual).Produces<ImportManualInboxResultDto>();

        // Разбор фильмов: имена приходят с клиента (каталог manual/ либо выбранные в браузере
        // файлы), сервер разбирает их и ищет в источнике. Импорт заводит шоу на файл.
        admin.MapPost("/movies/match", MatchMovies).Produces<IReadOnlyList<MovieMatchDto>>();
        admin.MapPost("/movies/match-one", MatchMovie).Produces<MovieMatchDto>();
        admin.MapPost("/movies/import", ImportMovies).Produces<ImportMoviesResultDto>();

        // Просмотр обработанного ассета в админке (ролики, проверка серии). Публичная раздача идёт
        // по stream-куке, здесь роут под JWT — плейлист и сегменты грузит hls.js с Bearer.
        admin.MapGet("/{id:guid}/preview/index.m3u8", PreviewPlaylist);
        admin.MapGet("/{id:guid}/preview/{file}", PreviewSegment);

        return app;
    }

    /// <summary>
    /// Потоковая загрузка: тело запроса — сырые байты файла, имя передаётся в query «fileName».
    /// Файл стримится на диск без буферизации в память, затем регистрируется и уходит в обработку.
    /// </summary>
    private static async Task<IResult> Upload(
        string fileName,
        HttpRequest request,
        IMediaStorage storage,
        IMediaProcessingQueue queue,
        ISender sender,
        UploadLimits limits,
        CancellationToken cancellationToken
    )
    {
        if (string.IsNullOrWhiteSpace(fileName))
            return Results.Problem(
                title: MediaErrors.EmptyFileName.Code,
                detail: MediaErrors.EmptyFileName.Message,
                statusCode: StatusCodes.Status400BadRequest
            );

        if (!MediaFormats.IsAllowed(fileName))
            return Results.Problem(
                title: MediaErrors.UnsupportedFormat.Code,
                detail: MediaErrors.UnsupportedFormat.Message,
                statusCode: StatusCodes.Status400BadRequest
            );

        var contentLength = request.ContentLength ?? 0;
        if (contentLength > limits.MaxUploadBytes)
            return Results.Problem(
                title: MediaErrors.FileTooLarge.Code,
                detail: MediaErrors.FileTooLarge.Message,
                statusCode: StatusCodes.Status400BadRequest
            );

        var free = storage.GetAvailableFreeSpaceBytes();
        if (free - contentLength < limits.MinFreeSpaceBytes)
            return Results.Problem(
                title: MediaErrors.InsufficientStorage.Code,
                detail: MediaErrors.InsufficientStorage.Message,
                statusCode: StatusCodes.Status409Conflict
            );

        var extension = Path.GetExtension(fileName);
        var token = await storage.SaveUploadAsync(request.Body, extension, cancellationToken);

        // Файл уже на диске в uploads/, но в БД его ещё нет и ссылок на токен нигде не остаётся:
        // любой исход, кроме успешной регистрации, обязан за собой убрать. Удаляем и при исключении
        // (оборванный запрос, сбой БД) — иначе гигабайты остаются в uploads/ до уборщика.
        // Токен отмены для уборки не пробрасываем: при обрыве запроса он уже сработал.
        Result<Guid> result;
        try
        {
            result = await sender.Send(
                new RegisterMediaAssetCommand(token, MediaSource.Upload, fileName),
                cancellationToken
            );
        }
        catch
        {
            await storage.DeleteUploadAsync(token, CancellationToken.None);
            throw;
        }

        if (!result.IsSuccess)
        {
            await storage.DeleteUploadAsync(token, CancellationToken.None);
            return result.ToHttpResult();
        }

        queue.Enqueue(result.Value);
        return Results.Created(
            $"/api/admin/media/{result.Value}",
            new UploadMediaResponse(result.Value)
        );
    }

    private static async Task<IResult> List(
        [AsParameters] ListMediaFilter filter,
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        var result = await sender.Send(
            new ListMediaAssetsQuery(
                filter.Page is > 0 ? filter.Page.Value : 1,
                filter.PageSize is > 0 ? filter.PageSize.Value : 20,
                filter.Status ?? [],
                filter.Search,
                filter.Sort,
                filter.Desc ?? false
            ),
            cancellationToken
        );
        return Results.Ok(result);
    }

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

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

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

    private static async Task<IResult> MatchMovies(
        MatchMoviesBody body,
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        var result = await sender.Send(
            new MatchMoviesQuery(body.Names, body.Provider),
            cancellationToken
        );
        return Results.Ok(result);
    }

    private static async Task<IResult> MatchMovie(
        MatchMovieBody body,
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        var result = await sender.Send(
            new MatchMovieQuery(body.Name, body.Title, body.Year, body.Provider),
            cancellationToken
        );
        return Results.Ok(result);
    }

    private static async Task<IResult> ImportMovies(
        ImportMoviesBody body,
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        var result = await sender.Send(
            new ImportMoviesCommand(body.Items, body.Provider),
            cancellationToken
        );
        return result.ToHttpResult();
    }

    private static async Task<IResult> ImportManual(
        ImportManualInboxBody body,
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        var result = await sender.Send(
            new ImportManualInboxCommand(body.Items, body.ShowId),
            cancellationToken
        );
        return result.ToHttpResult();
    }

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

        var baseUrl = $"/api/admin/media/{id}/preview/";
        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");
    }

    private static IResult PreviewSegment(Guid id, string file, MediaPathResolver paths)
    {
        if (!SegmentFiles.IsSegmentName(file))
            return Results.NotFound();

        if (SegmentFiles.TryResolveExisting(paths, id, file) is not { } path)
            return Results.NotFound();

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

/// <summary>
/// Фильтр и страница списка медиа. Все поля nullable намеренно: обязательный параметр в query
/// заставлял минимальный API отвечать 400 на запросы без него — например, из выборок «все готовые
/// ассеты», которым сортировка не нужна. Умолчания подставляет хендлер, а не объявление: при
/// <c>AsParameters</c> обязательность определяется nullable-типом, а не значением по умолчанию.
/// </summary>
public sealed record ListMediaFilter(
    int? Page,
    int? PageSize,
    MediaAssetStatus[]? Status,
    string? Search,
    string? Sort,
    bool? Desc
);

public sealed record UploadMediaResponse(Guid Id);

public sealed record ImportManualInboxBody(IReadOnlyList<ManualImportItem> Items, Guid ShowId);

public sealed record MatchMoviesBody(IReadOnlyList<string> Names, string Provider);

public sealed record MatchMovieBody(string Name, string Title, int? Year, string Provider);

public sealed record ImportMoviesBody(IReadOnlyList<MovieImportItem> Items, string Provider);
