using System.Globalization;
using System.Text;
using LiteCqrs;
using Microsoft.Extensions.Hosting;
using TeleWave.Api.Common;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Streaming;
using TeleWave.Application.Streaming.GetLivePlaylist;
using TeleWave.Application.Streaming.GetPublicEpg;
using TeleWave.Application.Streaming.ListPublicChannels;
using TeleWave.Infrastructure.Media;
using TeleWave.Infrastructure.Streaming;

namespace TeleWave.Api.Endpoints;

public static class StreamingEndpoints
{
    private const string StreamCookieName = "tw_stream";

    public static IEndpointRouteBuilder MapStreamingEndpoints(this IEndpointRouteBuilder app)
    {
        // Публичный API канала (Bearer): список, EPG, выдача stream-cookie.
        var channels = app.MapGroup("/api/channels").WithTags("Channels").RequireAuthorization();
        channels.MapGet("", ListChannels).Produces<IReadOnlyList<PublicChannelDto>>();
        // Что включено на стороне зрителя: сейчас только переключение по номерам (см. 6.8).
        channels.MapGet("/features", ViewerFeatures).Produces<ViewerFeaturesDto>();
        channels.MapPost("/{slug}/watch", Watch).Produces(StatusCodes.Status204NoContent);
        channels.MapGet("/{slug}/epg", Epg);

        // Раздача эфира (cookie tw_stream): плейлист и сегменты — их грузит <video>/hls.js.
        app.MapGet("/api/channels/{slug}/live.m3u8", LivePlaylist).WithTags("Streaming");
        app.MapGet("/api/stream/{assetId:guid}/{file}", Segment).WithTags("Streaming");

        return app;
    }

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

    private static async Task<IResult> ViewerFeatures(
        ISiteSettings siteSettings,
        CancellationToken cancellationToken
    ) =>
        Results.Ok(
            new ViewerFeaturesDto(
                await siteSettings.AreChannelNumbersEnabledAsync(cancellationToken)
            )
        );

    /// <summary>
    /// Выдаёт cookie доступа к эфиру. Канал в маршруте есть для симметрии с остальными
    /// эндпоинтами, но токен не привязан к каналу — он подтверждает зрителя, а не подписку на
    /// конкретную ленту, поэтому в сигнатуре slug не нужен.
    /// </summary>
    private static IResult Watch(
        ICurrentUser currentUser,
        StreamTokenService tokens,
        HttpRequest request,
        HttpResponse response,
        IHostEnvironment env
    )
    {
        if (currentUser.UserId is not { } userId)
            return Results.Unauthorized();

        var (token, expiresAt) = tokens.Issue(userId);
        response.Cookies.Append(
            StreamCookieName,
            token,
            new CookieOptions
            {
                HttpOnly = true,
                // Вне Development — всегда Secure (прод за внешним TLS-прокси; request.IsHttps ненадёжен
                // при неполной настройке ForwardedHeaders). См. UseSecureCookie в AuthEndpoints.
                Secure = !env.IsDevelopment() || request.IsHttps,
                SameSite = SameSiteMode.Strict,
                Path = "/api",
                Expires = expiresAt,
            }
        );
        return Results.NoContent();
    }

    private static async Task<IResult> Epg(
        string slug,
        DateTimeOffset? from,
        DateTimeOffset? to,
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        var fromUtc = from ?? DateTimeOffset.UtcNow;
        var toUtc = to ?? fromUtc.AddHours(12);
        var result = await sender.Send(
            new GetPublicEpgQuery(slug, fromUtc, toUtc),
            cancellationToken
        );
        return result.ToHttpResult();
    }

    private static async Task<IResult> LivePlaylist(
        string slug,
        HttpRequest request,
        HttpResponse response,
        StreamTokenService tokens,
        IIdentityService identity,
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        // Плейлист hls.js перезагружает регулярно — здесь дёшево (1 запрос на перезагрузку) сверить,
        // что зритель из токена ещё существует и не заблокирован. Так блокировка отражается почти сразу,
        // не дожидаясь истечения короткого TTL cookie; сегменты этой проверки не делают (слишком часто).
        // Для IPTV это единственная точка, где блокировка вообще может сработать: у внешнего плеера
        // пропуск долгий и перевыпуску не подлежит.
        if (Viewer(request, tokens) is not { } userId)
            return Results.Unauthorized();
        var profile = await identity.GetProfileAsync(userId, cancellationToken);
        if (profile is null || profile.IsBlocked)
            return Results.Unauthorized();

        var result = await sender.Send(
            new GetLivePlaylistQuery(slug, DateTimeOffset.UtcNow),
            cancellationToken
        );
        if (!result.IsSuccess)
            return result.ToHttpResult();
        if (result.Value.Segments.Count == 0)
            return Results.StatusCode(StatusCodes.Status503ServiceUnavailable);

        // Токен из ссылки тянется дальше в адреса сегментов: плеер разрешает их относительно
        // плейлиста и query при этом теряет, а cookie у него нет — без этого сегменты дадут 401.
        var token = request.Query["token"].ToString();
        var suffix = string.IsNullOrEmpty(token)
            ? string.Empty
            : $"?token={Uri.EscapeDataString(token)}";

        response.Headers.CacheControl = "no-cache";
        return Results.Text(Render(result.Value, suffix), "application/vnd.apple.mpegurl");
    }

    private static IResult Segment(
        Guid assetId,
        string file,
        HttpRequest request,
        HttpResponse response,
        StreamTokenService tokens,
        MediaPathResolver paths
    )
    {
        if (Viewer(request, tokens) is null)
            return Results.Unauthorized();
        if (!SegmentFiles.IsSegmentName(file))
            return Results.NotFound();

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

        response.Headers.CacheControl = "public, max-age=31536000, immutable";
        return Results.File(path, "video/mp2t", enableRangeProcessing: true);
    }

    /// <summary>
    /// Кто смотрит: cookie — для браузера, токен в ссылке — для внешнего IPTV-плеера, который
    /// cookie не носит. Токен один и тот же по формату, различается только сроком выдачи.
    /// </summary>
    private static Guid? Viewer(HttpRequest request, StreamTokenService tokens) =>
        tokens.Validate(request.Cookies[StreamCookieName])
        ?? tokens.Validate(request.Query["token"]);

    private static string Render(LivePlaylistDto playlist, string segmentSuffix)
    {
        var extinf = playlist.TargetDuration.ToString("F6", CultureInfo.InvariantCulture);
        var sb = new StringBuilder();
        sb.Append("#EXTM3U\n");
        sb.Append("#EXT-X-VERSION:3\n");
        sb.Append(
            CultureInfo.InvariantCulture,
            $"#EXT-X-TARGETDURATION:{playlist.TargetDuration}\n"
        );
        sb.Append(
            CultureInfo.InvariantCulture,
            $"#EXT-X-MEDIA-SEQUENCE:{playlist.MediaSequence}\n"
        );

        foreach (var segment in playlist.Segments)
        {
            if (segment.Discontinuity)
                sb.Append("#EXT-X-DISCONTINUITY\n");
            sb.Append(CultureInfo.InvariantCulture, $"#EXTINF:{extinf},\n");
            sb.Append(
                CultureInfo.InvariantCulture,
                $"/api/stream/{segment.AssetId:N}/seg{segment.LocalIndex:D5}.ts{segmentSuffix}\n"
            );
        }

        return sb.ToString();
    }
}

/// <summary>Опции зрительской части, включённые глобально.</summary>
public sealed record ViewerFeaturesDto(bool ChannelNumbersEnabled);
