using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.User; using LightlessSync.LightlessConfiguration; using LightlessSync.Services.Mediator; using LightlessSync.Utils; using LightlessSync.WebAPI; using LightlessSync.WebAPI.SignalR; using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace LightlessSync.Services; public class BroadcastService : IHostedService, IMediatorSubscriber { private readonly ILogger _logger; private readonly ApiController _apiController; private readonly LightlessMediator _mediator; private readonly HubFactory _hubFactory; private readonly LightlessConfigService _config; private readonly DalamudUtilService _dalamudUtil; public LightlessMediator Mediator => _mediator; public bool IsLightFinderAvailable { get; private set; } = true; public bool IsBroadcasting => _config.Current.BroadcastEnabled; private bool _syncedOnStartup = false; private bool _waitingForTtlFetch = false; private TimeSpan? _remainingTtl = null; private DateTime _lastTtlCheck = DateTime.MinValue; private DateTime _lastForcedDisableTime = DateTime.MinValue; private static readonly TimeSpan DisableCooldown = TimeSpan.FromSeconds(5); public TimeSpan? RemainingTtl => _remainingTtl; public TimeSpan? RemainingCooldown { get { var elapsed = DateTime.UtcNow - _lastForcedDisableTime; if (elapsed >= DisableCooldown) return null; return DisableCooldown - elapsed; } } public BroadcastService(ILogger logger, LightlessMediator mediator, HubFactory hubFactory, LightlessConfigService config, DalamudUtilService dalamudUtil, ApiController apiController) { _logger = logger; _mediator = mediator; _hubFactory = hubFactory; _config = config; _dalamudUtil = dalamudUtil; _apiController = apiController; } private async Task RequireConnectionAsync(string context, Func action) { if (!_apiController.IsConnected) { _logger.LogDebug($"{context} skipped, not connected"); return; } await action().ConfigureAwait(false); } public async Task StartAsync(CancellationToken cancellationToken) { _mediator.Subscribe(this, OnEnableBroadcast); _mediator.Subscribe(this, OnBroadcastStatusChanged); _mediator.Subscribe(this, OnTick); _apiController.OnConnected += () => _ = CheckLightfinderSupportAsync(cancellationToken); _ = CheckLightfinderSupportAsync(cancellationToken); } public Task StopAsync(CancellationToken cancellationToken) { _mediator.UnsubscribeAll(this); _apiController.OnConnected -= () => _ = CheckLightfinderSupportAsync(cancellationToken); return Task.CompletedTask; } // need to rework this, this is cooked private async Task CheckLightfinderSupportAsync(CancellationToken cancellationToken) { try { while (!_apiController.IsConnected && !cancellationToken.IsCancellationRequested) await Task.Delay(250, cancellationToken).ConfigureAwait(false); if (cancellationToken.IsCancellationRequested) return; var hub = _hubFactory.GetOrCreate(CancellationToken.None); var dummy = "0".PadLeft(64, '0'); await hub.InvokeAsync("IsUserBroadcasting", dummy, cancellationToken); await hub.InvokeAsync("SetBroadcastStatus", dummy, true, null, cancellationToken); await hub.InvokeAsync("GetBroadcastTtl", dummy, cancellationToken); await hub.InvokeAsync>("AreUsersBroadcasting", new[] { dummy }, cancellationToken); IsLightFinderAvailable = true; _logger.LogInformation("Lightfinder is available."); } catch (HubException ex) when (ex.Message.Contains("Method does not exist")) { _logger.LogWarning("Lightfinder unavailable: required method missing."); IsLightFinderAvailable = false; _config.Current.BroadcastEnabled = false; _config.Current.BroadcastTtl = DateTime.MinValue; _config.Save(); _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); } catch (OperationCanceledException) { _logger.LogInformation("Lightfinder check was canceled."); } catch (Exception ex) { _logger.LogWarning(ex, "Lightfinder check failed."); IsLightFinderAvailable = false; _config.Current.BroadcastEnabled = false; _config.Current.BroadcastTtl = DateTime.MinValue; _config.Save(); _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); } } private void OnEnableBroadcast(EnableBroadcastMessage msg) { _ = RequireConnectionAsync(nameof(OnEnableBroadcast), async () => { try { GroupBroadcastRequestDto? groupDto = null; if (_config.Current.SyncshellFinderEnabled && _config.Current.SelectedFinderSyncshell != null) { groupDto = new GroupBroadcastRequestDto { HashedCID = msg.HashedCid, GID = _config.Current.SelectedFinderSyncshell, Enabled = msg.Enabled, }; } await _apiController.SetBroadcastStatus(msg.HashedCid, msg.Enabled, groupDto).ConfigureAwait(false); _logger.LogInformation("Broadcast {Status} for {Cid}", msg.Enabled ? "enabled" : "disabled", msg.HashedCid); if (!msg.Enabled) { _config.Current.BroadcastEnabled = false; _config.Current.BroadcastTtl = DateTime.MinValue; _config.Save(); _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); return; } _waitingForTtlFetch = true; var ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false); if (ttl is { } remaining && remaining > TimeSpan.Zero) { _config.Current.BroadcastTtl = DateTime.UtcNow + remaining; _config.Current.BroadcastEnabled = true; _config.Save(); _logger.LogInformation("Fetched TTL from server: {TTL}", remaining); _mediator.Publish(new BroadcastStatusChangedMessage(true, remaining)); } else { _logger.LogWarning("No valid TTL returned after enabling broadcast. Disabling."); _config.Current.BroadcastEnabled = false; _config.Current.BroadcastTtl = DateTime.MinValue; _config.Save(); _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); } _waitingForTtlFetch = false; } catch (Exception ex) { _logger.LogError(ex, "Failed to toggle broadcast for {Cid}", msg.HashedCid); _waitingForTtlFetch = false; } }); } private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg) { _config.Current.BroadcastEnabled = msg.Enabled; _config.Save(); } public async Task CheckIfBroadcastingAsync(string targetCid) { bool result = false; await RequireConnectionAsync(nameof(CheckIfBroadcastingAsync), async () => { try { _logger.LogInformation("[BroadcastCheck] Checking CID: {cid}", targetCid); var info = await _apiController.IsUserBroadcasting(targetCid).ConfigureAwait(false); result = info?.TTL > TimeSpan.Zero; _logger.LogInformation("[BroadcastCheck] Result for {cid}: {result} (TTL: {ttl}, GID: {gid})", targetCid, result, info?.TTL, info?.GID); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to check broadcast status for {cid}", targetCid); } }).ConfigureAwait(false); return result; } public async Task GetBroadcastTtlAsync(string cid) { TimeSpan? ttl = null; await RequireConnectionAsync(nameof(GetBroadcastTtlAsync), async () => { try { ttl = await _apiController.GetBroadcastTtl(cid).ConfigureAwait(false); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to fetch broadcast TTL for {cid}", cid); } }).ConfigureAwait(false); return ttl; } public async Task> AreUsersBroadcastingAsync(List hashedCids) { Dictionary result = new(); await RequireConnectionAsync(nameof(AreUsersBroadcastingAsync), async () => { try { var batch = await _apiController.AreUsersBroadcasting(hashedCids).ConfigureAwait(false); if (batch?.Results != null) { foreach (var kv in batch.Results) result[kv.Key] = kv.Value; } _logger.LogInformation("Batch broadcast status check complete for {Count} CIDs", hashedCids.Count); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to batch check broadcast status"); } }).ConfigureAwait(false); return result; } public async void ToggleBroadcast() { if (!IsLightFinderAvailable) { _logger.LogWarning("ToggleBroadcast - Lightfinder is not available."); return; } await RequireConnectionAsync(nameof(ToggleBroadcast), async () => { var cooldown = RemainingCooldown; if (!_config.Current.BroadcastEnabled && cooldown is { } cd && cd > TimeSpan.Zero) { _logger.LogWarning("Cooldown active. Must wait {Remaining}s before re-enabling.", cd.TotalSeconds); return; } var hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); try { var isCurrentlyBroadcasting = await CheckIfBroadcastingAsync(hashedCid).ConfigureAwait(false); var newStatus = !isCurrentlyBroadcasting; if (!newStatus) { _lastForcedDisableTime = DateTime.UtcNow; _logger.LogInformation("Manual disable: cooldown timer started."); } _logger.LogInformation("Toggling broadcast. Server currently broadcasting: {ServerStatus}, setting to: {NewStatus}", isCurrentlyBroadcasting, newStatus); _mediator.Publish(new EnableBroadcastMessage(hashedCid, newStatus)); } catch (Exception ex) { _logger.LogError(ex, "Failed to determine current broadcast status for toggle"); } }).ConfigureAwait(false); } private async void OnTick(PriorityFrameworkUpdateMessage _) { if (!IsLightFinderAvailable) return; if (_config?.Current == null) return; if ((DateTime.UtcNow - _lastTtlCheck).TotalSeconds < 1) return; _lastTtlCheck = DateTime.UtcNow; await RequireConnectionAsync(nameof(OnTick), async () => { if (!_syncedOnStartup && _config.Current.BroadcastEnabled) { _syncedOnStartup = true; try { var hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); var ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false); if (ttl is { } remaining && remaining > TimeSpan.Zero) { _config.Current.BroadcastTtl = DateTime.UtcNow + remaining; _config.Current.BroadcastEnabled = true; _config.Save(); _logger.LogInformation("Refreshed broadcast TTL from server on first OnTick: {TTL}", remaining); } else { _logger.LogWarning("No valid TTL found on OnTick. Disabling broadcast state."); _config.Current.BroadcastEnabled = false; _config.Current.BroadcastTtl = DateTime.MinValue; _config.Save(); _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); } } catch (Exception ex) { _logger.LogError(ex, "Failed to refresh TTL in OnTick"); } } if (_config.Current.BroadcastEnabled) { if (_waitingForTtlFetch) { _logger.LogDebug("OnTick skipped: waiting for TTL fetch"); return; } var expiry = _config.Current.BroadcastTtl; var remaining = expiry - DateTime.UtcNow; _remainingTtl = remaining > TimeSpan.Zero ? remaining : null; if (_remainingTtl == null) { _logger.LogInformation("Broadcast TTL expired. Disabling broadcast locally."); _config.Current.BroadcastEnabled = false; _config.Current.BroadcastTtl = DateTime.MinValue; _config.Save(); _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); } } else { _remainingTtl = null; } }).ConfigureAwait(false); } }