using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Options;
using TeleWave.Application.Streaming;
using TeleWave.Infrastructure.Identity;

namespace TeleWave.Infrastructure.Streaming;

/// <summary>
/// Короткоживущий подписанный токен для cookie <c>tw_stream</c>: выдаётся авторизованному зрителю,
/// проверяется на запросах плейлиста и сегментов (работает и с нативным HLS, где заголовок не
/// поставить). Подпись — HMAC-SHA256 на том же ключе, что и JWT. TTL короткий (см.
/// <see cref="StreamingOptions.StreamTokenMinutes"/>), а <see cref="Validate"/> возвращает id зрителя —
/// эндпоинт плейлиста дополнительно сверяет, что пользователь не заблокирован.
/// </summary>
public sealed class StreamTokenService(
    IOptions<JwtOptions> jwtOptions,
    IOptions<StreamingOptions> streamingOptions
)
{
    private readonly TimeSpan _ttl = TimeSpan.FromMinutes(
        Math.Max(1, streamingOptions.Value.StreamTokenMinutes)
    );
    private readonly TimeSpan _iptvTtl = TimeSpan.FromDays(
        Math.Max(1, streamingOptions.Value.IptvTokenDays)
    );
    private readonly byte[] _key = Encoding.UTF8.GetBytes(jwtOptions.Value.SigningKey);

    public (string Token, DateTimeOffset ExpiresAt) Issue(Guid userId) => Issue(userId, _ttl);

    /// <summary>
    /// Долгий токен для IPTV-плейлиста. Отдельного вида у него нет намеренно: он попадает в те же
    /// ссылки на плейлист и сегменты, что и cookie, и проверяется тем же <see cref="Validate"/>.
    /// Отличается только сроком — внешний плеер перевыпустить пропуск не может.
    /// </summary>
    public (string Token, DateTimeOffset ExpiresAt) IssueForIptv(Guid userId) =>
        Issue(userId, _iptvTtl);

    private (string Token, DateTimeOffset ExpiresAt) Issue(Guid userId, TimeSpan ttl)
    {
        var expiresAt = DateTimeOffset.UtcNow.Add(ttl);
        var payload = $"{userId:N}.{expiresAt.ToUnixTimeSeconds()}";
        var token = $"{Base64Url(Encoding.UTF8.GetBytes(payload))}.{Base64Url(Sign(payload))}";
        return (token, expiresAt);
    }

    /// <summary>Проверяет подпись и срок; при успехе возвращает id зрителя из токена, иначе null.</summary>
    public Guid? Validate(string? token)
    {
        if (string.IsNullOrEmpty(token))
            return null;

        var parts = token.Split('.');
        if (parts.Length != 2)
            return null;

        byte[] payloadBytes;
        byte[] signature;
        try
        {
            payloadBytes = FromBase64Url(parts[0]);
            signature = FromBase64Url(parts[1]);
        }
        catch (FormatException)
        {
            return null;
        }

        var payload = Encoding.UTF8.GetString(payloadBytes);
        var expected = Sign(payload);
        if (!CryptographicOperations.FixedTimeEquals(signature, expected))
            return null;

        var dot = payload.LastIndexOf('.');
        if (dot < 0 || !long.TryParse(payload.AsSpan(dot + 1), out var expiresUnix))
            return null;

        if (DateTimeOffset.FromUnixTimeSeconds(expiresUnix) <= DateTimeOffset.UtcNow)
            return null;

        return Guid.TryParseExact(payload[..dot], "N", out var userId) ? userId : null;
    }

    private byte[] Sign(string payload) =>
        HMACSHA256.HashData(_key, Encoding.UTF8.GetBytes(payload));

    private static string Base64Url(byte[] bytes) =>
        Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_');

    private static byte[] FromBase64Url(string value)
    {
        var padded = value.Replace('-', '+').Replace('_', '/');
        padded += (padded.Length % 4) switch
        {
            2 => "==",
            3 => "=",
            _ => "",
        };
        return Convert.FromBase64String(padded);
    }
}
