using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using ytLive.Helpers;
using ytLive.Models;
namespace ytLive.Services;
///
/// TASK 3·20: reads the channel's recent Super Chat / Super Sticker events so a session report can be
/// shown without storing anything.
/// YouTube is the authority. This service only asks; it keeps no ledger, and the events it
/// returns are the ones YouTube still considers valid (removed or refunded events do not come back).
/// Scope: the endpoint is authorised by the app's existing auth/youtube scope, so no
/// re-consent is needed — unlike the Analytics API, which does.
///
public interface ISuperChatEventSource
{
/// The most recent events for a channel (YouTube caps the window at roughly 30 days).
Task> GetRecentAsync(
string channelId, int maxResults = 200, CancellationToken cancellationToken = default);
}
public sealed class SuperChatEventService : ISuperChatEventSource
{
private const string ApiBase = "https://www.googleapis.com/youtube/v3";
private readonly YouTubeAuthService _auth;
private readonly HttpClient _http;
public SuperChatEventService(YouTubeAuthService auth, HttpClient? http = null)
{
_auth = auth;
_http = http ?? new HttpClient();
}
public async Task> GetRecentAsync(
string channelId, int maxResults = 200, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(channelId)) return Array.Empty();
if (_auth.CurrentChannel?.AccessToken is not { } token) return Array.Empty();
_http.DefaultRequestHeaders.Authorization = new("Bearer", token);
var url = $"{ApiBase}/superChatEvents?part=snippet&maxResults={Math.Clamp(maxResults, 1, 200)}" +
$"&channelId={Uri.EscapeDataString(channelId)}";
try
{
var response = await _http.GetAsync(url, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
AppLog.Write($"superChatEvents.list failed: {(int)response.StatusCode}");
return Array.Empty();
}
var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
return Parse(json);
}
catch (Exception ex)
{
AppLog.Write($"superChatEvents.list failed: {ex.Message}");
return Array.Empty();
}
}
/// Pure parse of a superChatEvents.list response. Both event types carry their money
/// in a different object, so a sticker must not be silently dropped.
internal static IReadOnlyList Parse(string json)
{
var events = new List();
using var document = JsonDocument.Parse(json);
if (!document.RootElement.TryGetProperty("items", out var items)) return events;
foreach (var item in items.EnumerateArray())
{
if (!item.TryGetProperty("snippet", out var snippet)) continue;
var id = item.TryGetProperty("id", out var idProp) ? idProp.GetString() ?? string.Empty : string.Empty;
var publishedAt = snippet.TryGetProperty("publishedAt", out var published)
&& DateTime.TryParse(published.GetString(), out var parsed)
? parsed.ToUniversalTime()
: DateTime.UtcNow;
var kind = ChatEventKind.SuperChat;
JsonElement details;
if (snippet.TryGetProperty("superChatDetails", out var chat))
details = chat;
else if (snippet.TryGetProperty("superStickerDetails", out var sticker))
{
details = sticker;
kind = ChatEventKind.SuperSticker;
}
else
{
continue;
}
var micros = details.TryGetProperty("amountMicros", out var amount)
&& amount.ValueKind == JsonValueKind.Number
&& amount.TryGetInt64(out var value)
? value
: 0;
var currency = details.TryGetProperty("currency", out var cur) && cur.ValueKind == JsonValueKind.String
? cur.GetString() ?? string.Empty
: string.Empty;
var display = details.TryGetProperty("amountDisplayString", out var shown)
&& shown.ValueKind == JsonValueKind.String
? shown.GetString()
: null;
events.Add(new SuperChatEvent(id, kind, micros, currency, display, publishedAt));
}
return events;
}
}