using Dalamud.Plugin.Services;
using LightlessSync.API.Dto.User;
using LightlessSync.Services.ActorTracking;
using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;

namespace LightlessSync.Services.LightFinder;

public class LightFinderScannerService : DisposableMediatorSubscriberBase
{
    private readonly ILogger<LightFinderScannerService> _logger;
    private readonly ActorObjectService _actorTracker;
    private readonly IFramework _framework;

    private readonly LightFinderService _broadcastService;
    private readonly LightFinderPlateHandler _lightFinderPlateHandler;
    private readonly LightFinderNativePlateHandler _lightFinderNativePlateHandler;

    private readonly ConcurrentDictionary<string, BroadcastEntry> _broadcastCache = new(StringComparer.Ordinal);
    private readonly Queue<string> _lookupQueue = new();
    private readonly HashSet<string> _lookupQueuedCids = [];
    private readonly HashSet<string> _syncshellCids = [];
    private volatile bool _pendingLocalBroadcast;
    private TimeSpan? _pendingLocalTtl;

    private static readonly TimeSpan _maxAllowedTtl = TimeSpan.FromMinutes(4);
    private static readonly TimeSpan _retryDelay = TimeSpan.FromMinutes(1);

    private readonly CancellationTokenSource _cleanupCts = new();
    private readonly Task? _cleanupTask;

    private readonly int _checkEveryFrames = 20;
    private int _frameCounter = 0;
    private const int _maxLookupsPerFrame = 30;
    private const int _maxQueueSize = 100;

    private volatile bool _batchRunning = false;

    public IReadOnlyDictionary<string, BroadcastEntry> BroadcastCache => _broadcastCache;
    public readonly record struct BroadcastEntry(bool IsBroadcasting, DateTime ExpiryTime, string? GID);

    public LightFinderScannerService(ILogger<LightFinderScannerService> logger,
        IFramework framework,
        LightFinderService broadcastService,
        LightlessMediator mediator,
        LightFinderPlateHandler lightFinderPlateHandler,
        LightFinderNativePlateHandler lightFinderNativePlateHandler,
        ActorObjectService actorTracker) : base(logger, mediator)
    {
        _logger = logger;
        _actorTracker = actorTracker;
        _broadcastService = broadcastService;
        _lightFinderPlateHandler = lightFinderPlateHandler;
        _lightFinderNativePlateHandler = lightFinderNativePlateHandler;

        _logger = logger;
        _framework = framework;
        _framework.Update += OnFrameworkUpdate;

        Mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged);
        _cleanupTask = Task.Run(ExpiredBroadcastCleanupLoop, _cleanupCts.Token);

        _actorTracker = actorTracker;
    }

    private void OnFrameworkUpdate(IFramework framework) => Update();

    public void Update()
    {
        _frameCounter++;
        var lookupsThisFrame = 0;

        if (!_broadcastService.IsBroadcasting)
            return;

        TryPrimeLocalBroadcastCache();

        var now = DateTime.UtcNow;

        foreach (var address in _actorTracker.PlayerAddresses)
        {
            if (address == nint.Zero)
                continue;

            var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer(address);
            var isStale = !_broadcastCache.TryGetValue(cid, out var entry) || entry.ExpiryTime <= now;

            if (isStale && _lookupQueuedCids.Add(cid) && _lookupQueue.Count < _maxQueueSize)
                _lookupQueue.Enqueue(cid);
        }

        if (_frameCounter % _checkEveryFrames == 0 && _lookupQueue.Count > 0)
        {
            var cidsToLookup = new List<string>();
            while (_lookupQueue.Count > 0 && lookupsThisFrame < _maxLookupsPerFrame)
            {
                var cid = _lookupQueue.Dequeue();
                _lookupQueuedCids.Remove(cid);
                cidsToLookup.Add(cid);
                lookupsThisFrame++;
            }

            if (cidsToLookup.Count > 0 && !_batchRunning)
            {
                _batchRunning = true;
                _ = BatchUpdateBroadcastCacheAsync(cidsToLookup).ContinueWith(_ => _batchRunning = false);
            }
        }
    }

    private async Task BatchUpdateBroadcastCacheAsync(List<string> cids)
    {
        var results = await _broadcastService.AreUsersBroadcastingAsync(cids).ConfigureAwait(false);
        var now = DateTime.UtcNow;

        foreach (var (cid, info) in results)
        {
            if (string.IsNullOrWhiteSpace(cid) || info == null)
                continue;

            var ttl = info.IsBroadcasting && info.TTL.HasValue
                ? TimeSpan.FromTicks(Math.Min(info.TTL.Value.Ticks, _maxAllowedTtl.Ticks))
                : _retryDelay;

            var expiry = now + ttl;

            _broadcastCache.AddOrUpdate(cid,
                new BroadcastEntry(info.IsBroadcasting, expiry, info.GID),
                (_, old) => new BroadcastEntry(info.IsBroadcasting, expiry, info.GID));
        }

        var activeCids = _broadcastCache
            .Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now)
            .Select(e => e.Key)
            .ToList();

        _lightFinderPlateHandler.UpdateBroadcastingCids(activeCids);
        _lightFinderNativePlateHandler.UpdateBroadcastingCids(activeCids);
        UpdateSyncshellBroadcasts();
    }

    private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg)
    {
        if (!msg.Enabled)
        {
            _broadcastCache.Clear();
            _lookupQueue.Clear();
            _lookupQueuedCids.Clear();
            _syncshellCids.Clear();
            _pendingLocalBroadcast = false;
            _pendingLocalTtl = null;

            _lightFinderPlateHandler.UpdateBroadcastingCids([]);
            _lightFinderNativePlateHandler.UpdateBroadcastingCids([]);
            return;
        }

        _pendingLocalBroadcast = true;
        _pendingLocalTtl = msg.Ttl;
        TryPrimeLocalBroadcastCache();
    }

    private void TryPrimeLocalBroadcastCache()
    {
        if (!_pendingLocalBroadcast)
            return;

        if (!TryGetLocalHashedCid(out var localCid))
            return;

        var ttl = _pendingLocalTtl ?? _maxAllowedTtl;
        var expiry = DateTime.UtcNow + ttl;

        _broadcastCache.AddOrUpdate(localCid,
            new BroadcastEntry(true, expiry, null),
            (_, old) => new BroadcastEntry(true, expiry, old.GID));

        _pendingLocalBroadcast = false;
        _pendingLocalTtl = null;

        var now = DateTime.UtcNow;
        var activeCids = _broadcastCache
            .Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now)
            .Select(e => e.Key)
            .ToList();

        _lightFinderPlateHandler.UpdateBroadcastingCids(activeCids);
        _lightFinderNativePlateHandler.UpdateBroadcastingCids(activeCids);
    }

    private void UpdateSyncshellBroadcasts()
    {
        var now = DateTime.UtcNow;
        var nearbyCids = GetNearbyHashedCids(out _);
        var newSet = nearbyCids.Count == 0
            ? new HashSet<string>(StringComparer.Ordinal)
            : _broadcastCache
                .Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
                .Where(e => nearbyCids.Contains(e.Key))
                .Select(e => e.Key)
                .ToHashSet(StringComparer.Ordinal);

        if (!_syncshellCids.SetEquals(newSet))
        {
            _syncshellCids.Clear();
            foreach (var cid in newSet)
                _syncshellCids.Add(cid);

            Mediator.Publish(new SyncshellBroadcastsUpdatedMessage());
        }
    }

    public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts(bool excludeLocal = false)
    {
        var now = DateTime.UtcNow;
        var nearbyCids = GetNearbyHashedCids(out var localCid);
        if (nearbyCids.Count == 0)
            return [];

        return [.. _broadcastCache
            .Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
            .Where(e => nearbyCids.Contains(e.Key))
            .Where(e => !excludeLocal || !string.Equals(e.Key, localCid, StringComparison.Ordinal))
            .Select(e => new BroadcastStatusInfoDto
            {
                HashedCID = e.Key,
                IsBroadcasting = true,
                TTL = e.Value.ExpiryTime - now,
                GID = e.Value.GID
            })];
    }

    public bool TryGetLocalHashedCid(out string hashedCid)
    {
        hashedCid = string.Empty;
        var descriptors = _actorTracker.PlayerDescriptors;
        if (descriptors.Count == 0)
            return false;

        foreach (var descriptor in descriptors)
        {
            if (!descriptor.IsLocalPlayer || string.IsNullOrWhiteSpace(descriptor.HashedContentId))
                continue;

            hashedCid = descriptor.HashedContentId;
            return true;
        }

        return false;
    }

    private HashSet<string> GetNearbyHashedCids(out string? localCid)
    {
        localCid = null;
        var descriptors = _actorTracker.PlayerDescriptors;
        if (descriptors.Count == 0)
            return new HashSet<string>(StringComparer.Ordinal);

        var set = new HashSet<string>(StringComparer.Ordinal);
        foreach (var descriptor in descriptors)
        {
            if (string.IsNullOrWhiteSpace(descriptor.HashedContentId))
                continue;

            if (descriptor.IsLocalPlayer)
                localCid = descriptor.HashedContentId;

            set.Add(descriptor.HashedContentId);
        }

        return set;
    }

    private async Task ExpiredBroadcastCleanupLoop()
    {
        var token = _cleanupCts.Token;

        try
        {
            while (!token.IsCancellationRequested)
            {
                await Task.Delay(TimeSpan.FromSeconds(10), token).ConfigureAwait(false);

                var now = DateTime.UtcNow;
                foreach (var (cid, entry) in _broadcastCache.ToArray())
                {
                    if (entry.ExpiryTime <= now)
                        _broadcastCache.TryRemove(cid, out _);
                }
            }
        }
        catch (OperationCanceledException)
        {
            // No action needed when cancelled
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Broadcast cleanup loop crashed");
        }

        UpdateSyncshellBroadcasts();
    }

    public int CountActiveBroadcasts(string? excludeHashedCid = null)
    {
        var now = DateTime.UtcNow;
        var comparer = StringComparer.Ordinal;
        return _broadcastCache.Count(entry =>
            entry.Value.IsBroadcasting &&
            entry.Value.ExpiryTime > now &&
            (excludeHashedCid is null || !comparer.Equals(entry.Key, excludeHashedCid)));
    }

    public List<KeyValuePair<string, BroadcastEntry>> GetActiveBroadcasts(string? excludeHashedCid = null)
    {
        var now = DateTime.UtcNow;
        var comparer = StringComparer.Ordinal;
        return [.. _broadcastCache.Where(entry =>
            entry.Value.IsBroadcasting &&
            entry.Value.ExpiryTime > now &&
            (excludeHashedCid is null || !comparer.Equals(entry.Key, excludeHashedCid)))];
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
        _framework.Update -= OnFrameworkUpdate;
        if (_cleanupTask != null)
        {
            _cleanupTask?.Wait(100, _cleanupCts.Token);
        }

        _cleanupCts.Cancel();
        _cleanupCts.Dispose();

        _cleanupTask?.Wait(100);
        _cleanupCts.Dispose();
    }
}
