using System.Globalization;
using System.Text;
using System.Xml;
using LiteCqrs;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Streaming.GetIptvGuide;
using TeleWave.Infrastructure.Streaming;

namespace TeleWave.Api.Endpoints;

/// <summary>
/// Выгрузка для внешних IPTV-плееров: M3U со списком каналов и XMLTV с программой передач.
///
/// Плеер не умеет ни Bearer, ни httpOnly-cookie — он умеет только открыть URL. Поэтому пропуск
/// зашит в сами ссылки файла долгим stream-токеном: **файл и есть доступ к эфиру**, и делиться им
/// нельзя. Срок — <c>Storage:IptvTokenDays</c>; блокировка зрителя обрывает доступ на ближайшей
/// перезагрузке плейлиста канала, а смена <c>Jwt:SigningKey</c> обнуляет все выданные разом.
/// </summary>
public static class IptvEndpoints
{
    /// <summary>Сколько суток программы класть в XMLTV: горизонт ленты и так неделя.</summary>
    private static readonly TimeSpan GuideWindow = TimeSpan.FromDays(7);

    public static IEndpointRouteBuilder MapIptvEndpoints(this IEndpointRouteBuilder app)
    {
        var iptv = app.MapGroup("/api/iptv").WithTags("IPTV");

        // Файл забирает браузер под обычной авторизацией — токен внутрь кладёт уже сервер.
        iptv.MapGet("/playlist.m3u", Playlist).RequireAuthorization();
        // XMLTV тянет сам плеер, у него из пропусков только токен из ссылки.
        iptv.MapGet("/epg.xml", Guide);

        return app;
    }

    private static async Task<IResult> Playlist(
        ICurrentUser currentUser,
        StreamTokenService tokens,
        HttpRequest request,
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        if (currentUser.UserId is not { } userId)
            return Results.Unauthorized();

        // Окно нулевое: плейлисту нужен только список каналов, программу плеер возьмёт из XMLTV.
        var now = DateTimeOffset.UtcNow;
        var channels = await sender.Send(new GetIptvGuideQuery(now, now), cancellationToken);

        var (token, _) = tokens.IssueForIptv(userId);
        var origin = Origin(request);
        var body = RenderPlaylist(channels, origin, token);

        return Results.File(
            Encoding.UTF8.GetBytes(body),
            "audio/x-mpegurl",
            fileDownloadName: "telewave.m3u"
        );
    }

    private static async Task<IResult> Guide(
        HttpRequest request,
        StreamTokenService tokens,
        IIdentityService identity,
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        if (tokens.Validate(request.Query["token"]) is not { } userId)
            return Results.Unauthorized();
        // Та же сверка, что и у плейлиста канала: заблокированный зритель не должен продолжать
        // тянуть гид по выданному раньше файлу.
        var profile = await identity.GetProfileAsync(userId, cancellationToken);
        if (profile is null || profile.IsBlocked)
            return Results.Unauthorized();

        var now = DateTimeOffset.UtcNow;
        var channels = await sender.Send(
            new GetIptvGuideQuery(now, now.Add(GuideWindow)),
            cancellationToken
        );

        return Results.File(
            Encoding.UTF8.GetBytes(RenderGuide(channels, Origin(request))),
            "application/xml",
            fileDownloadName: "telewave-epg.xml"
        );
    }

    /// <summary>
    /// Абсолютный адрес сервиса. Плееру нужны именно абсолютные ссылки — относительные ему не от
    /// чего считать. Схема и хост берутся из запроса: TLS внешний, и их подставляет
    /// <c>ForwardedHeaders</c>.
    /// </summary>
    private static string Origin(HttpRequest request) => $"{request.Scheme}://{request.Host}";

    private static string RenderPlaylist(
        IReadOnlyList<IptvChannelDto> channels,
        string origin,
        string token
    )
    {
        var sb = new StringBuilder();
        sb.Append(
            CultureInfo.InvariantCulture,
            $"#EXTM3U x-tvg-url=\"{origin}/api/iptv/epg.xml?token={token}\"\n"
        );

        foreach (var channel in channels)
        {
            var logo = channel.LogoImageId is { } imageId
                ? $" tvg-logo=\"{origin}/api/images/{imageId}\""
                : string.Empty;
            var number = channel.Number is { } value
                ? $" tvg-chno=\"{value.ToString(CultureInfo.InvariantCulture)}\""
                : string.Empty;
            var name = Attribute(channel.Name);

            sb.Append(
                CultureInfo.InvariantCulture,
                $"#EXTINF:-1 tvg-id=\"{Attribute(channel.Slug)}\" tvg-name=\"{name}\"{number}{logo} group-title=\"TeleWave\",{name}\n"
            );
            sb.Append(
                CultureInfo.InvariantCulture,
                $"{origin}/api/channels/{channel.Slug}/live.m3u8?token={token}\n"
            );
        }

        return sb.ToString();
    }

    /// <summary>
    /// Значение атрибута M3U. Экранирования в формате нет вовсе, поэтому кавычки и переводы строк
    /// не заменяются, а вырезаются: иначе название канала рвало бы строку файла.
    /// </summary>
    private static string Attribute(string value) =>
        value.Replace("\"", string.Empty).ReplaceLineEndings(" ").Trim();

    private static string RenderGuide(IReadOnlyList<IptvChannelDto> channels, string origin)
    {
        var output = new StringBuilder();
        // Через XmlWriter, а не склейкой строк: в названия и описания приходит текст из библиотеки,
        // и экранировать его руками — верный способ однажды отдать битый XML.
        using var writer = XmlWriter.Create(
            output,
            new XmlWriterSettings { Indent = true, Encoding = Encoding.UTF8 }
        );

        writer.WriteStartDocument();
        writer.WriteStartElement("tv");
        writer.WriteAttributeString("generator-info-name", "TeleWave");

        // Сначала все каналы, потом все передачи: XMLTV требует именно такого порядка секций.
        foreach (var channel in channels)
            WriteChannel(writer, channel, origin);

        foreach (var channel in channels)
        {
            foreach (var programme in channel.Programmes)
                WriteProgramme(writer, channel.Slug, programme, origin);
        }

        writer.WriteEndElement();
        writer.WriteEndDocument();
        writer.Flush();

        return output.ToString();
    }

    private static void WriteChannel(XmlWriter writer, IptvChannelDto channel, string origin)
    {
        writer.WriteStartElement("channel");
        writer.WriteAttributeString("id", channel.Slug);
        writer.WriteElementString("display-name", channel.Name);
        // Номер вторым display-name: так плееры с нумерацией подхватывают его без своих настроек.
        if (channel.Number is { } number)
            writer.WriteElementString(
                "display-name",
                number.ToString(CultureInfo.InvariantCulture)
            );
        WriteIcon(writer, channel.LogoImageId, origin);
        writer.WriteEndElement();
    }

    private static void WriteProgramme(
        XmlWriter writer,
        string slug,
        IptvProgrammeDto programme,
        string origin
    )
    {
        writer.WriteStartElement("programme");
        writer.WriteAttributeString("start", XmltvTime(programme.StartsAtUtc));
        writer.WriteAttributeString("stop", XmltvTime(programme.EndsAtUtc));
        writer.WriteAttributeString("channel", slug);

        writer.WriteElementString("title", programme.Title);
        if (!string.IsNullOrWhiteSpace(programme.SubTitle))
            writer.WriteElementString("sub-title", programme.SubTitle);
        if (!string.IsNullOrWhiteSpace(programme.Description))
            writer.WriteElementString("desc", programme.Description);
        WriteIcon(writer, programme.ImageId, origin);

        writer.WriteEndElement();
    }

    private static void WriteIcon(XmlWriter writer, Guid? imageId, string origin)
    {
        if (imageId is not { } id)
            return;

        writer.WriteStartElement("icon");
        writer.WriteAttributeString("src", $"{origin}/api/images/{id}");
        writer.WriteEndElement();
    }

    /// <summary>Время XMLTV: <c>YYYYMMDDHHMMSS +0000</c>. Отдаём в UTC — плеер сдвинет сам.</summary>
    private static string XmltvTime(DateTimeOffset moment) =>
        moment.ToUniversalTime().ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture)
        + " +0000";
}
