using System.Threading.Channels;
using Microsoft.Extensions.Logging;
using TeamsISO.Engine.Interop;

namespace TeamsISO.Engine.Discovery;

/// <summary>
/// Polls <see cref="INdiInterop.GetCurrentSources"/> at a fixed cadence, diffs the
/// resulting set against the previous poll, and emits <see cref="DiscoveryEvent"/>s
/// on a channel for downstream consumers.
/// </summary>
public sealed class NdiDiscoveryService
{
    private readonly INdiInterop _interop;
    private readonly ChannelWriter<DiscoveryEvent> _writer;
    private readonly ILogger<NdiDiscoveryService> _logger;
    private NdiFindHandle _finder;
    private readonly HashSet<string> _previous = new();
    private string? _discoveryGroups;
    private int _refreshRequested;

    public NdiDiscoveryService(
        INdiInterop interop,
        ChannelWriter<DiscoveryEvent> writer,
        ILogger<NdiDiscoveryService> logger,
        string? discoveryGroups = null)
    {
        _interop = interop;
        _writer = writer;
        _logger = logger;
        _discoveryGroups = discoveryGroups;
        _finder = interop.CreateFinder(discoveryGroups);
    }

    /// <summary>
    /// Request that the next poll tick rebuild the underlying NDI finder. Useful right
    /// after the operator changes discovery groups or applies a new transcoder topology
    /// — without this, the finder is bound to the groups it was created with and never
    /// sees new sources from the just-configured group. Honored on the next
    /// <see cref="RunAsync"/> tick: the old finder is disposed, a fresh one is created,
    /// and the seen-set is cleared so all currently-visible sources re-fire as
    /// <see cref="DiscoveryEvent.Added"/>. Cheap (idempotent) — extra Refresh calls
    /// while a refresh is already pending are coalesced.
    /// </summary>
    public void RequestRefresh() => Interlocked.Exchange(ref _refreshRequested, 1);

    /// <summary>
    /// Runs a single poll cycle. Public for unit testing; production uses <see cref="RunAsync"/>.
    /// </summary>
    public void PollOnce()
    {
        var current = _interop.GetCurrentSources(_finder);
        var currentSet = new HashSet<string>(current);

        foreach (var name in currentSet.Except(_previous))
        {
            var parsed = NdiSourceParser.Parse(name);
            if (parsed is null)
            {
                _logger.LogTrace("Ignoring unrecognized source: {Name}", name);
                continue;
            }
            _writer.TryWrite(new DiscoveryEvent.Added(parsed));
        }

        foreach (var name in _previous.Except(currentSet))
        {
            var parsed = NdiSourceParser.Parse(name);
            if (parsed is null) continue;
            _writer.TryWrite(new DiscoveryEvent.Removed(parsed));
        }

        _previous.Clear();
        foreach (var name in currentSet) _previous.Add(name);
    }

    /// <summary>Long-running poll loop. Cancel the token to stop.</summary>
    public async Task RunAsync(TimeSpan pollInterval, CancellationToken cancellationToken)
    {
        using var timer = new PeriodicTimer(pollInterval);
        try
        {
            while (await timer.WaitForNextTickAsync(cancellationToken))
            {
                if (Interlocked.Exchange(ref _refreshRequested, 0) == 1)
                {
                    try
                    {
                        _logger.LogInformation("Rebuilding NDI finder on operator request.");
                        _finder.Dispose();
                        _finder = _interop.CreateFinder(_discoveryGroups);
                        _previous.Clear();
                    }
                    catch (Exception ex)
                    {
                        _logger.LogWarning(ex, "Finder refresh failed; continuing with existing finder.");
                    }
                }
                try { PollOnce(); }
                catch (Exception ex) { _logger.LogWarning(ex, "Discovery poll failed; will retry on next tick."); }
            }
        }
        catch (OperationCanceledException) { /* expected */ }
        finally
        {
            _finder.Dispose();
        }
    }

    /// <summary>
    /// Updates the cached discovery-groups string used by future finder rebuilds.
    /// Call <see cref="RequestRefresh"/> after this to actually pick up the change.
    /// </summary>
    public void UpdateDiscoveryGroups(string? discoveryGroups) =>
        _discoveryGroups = discoveryGroups;
}
