using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Scripting;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Scripting;

namespace ZB.MOM.WW.ScadaBridge.Host.Services;

/// <summary>
/// In-process, per-node <see cref="IScriptArtifactChangeBus"/>: a lock-free
/// copy-on-write subscriber list. <see cref="Publish"/> snapshots the current
/// handlers and invokes each, swallowing and logging any handler fault so a
/// misbehaving consumer can never fault a producer's post-commit publish.
/// <para>
/// This is deliberately node-local — cross-node / failover propagation is the
/// consumer's responsibility (content-hash / revision self-heal). See
/// <c>docs/plans/2026-07-08-script-artifact-invalidation-contract.md</c>.
/// </para>
/// </summary>
public sealed class InProcessScriptArtifactChangeBus : IScriptArtifactChangeBus
{
    private readonly Lock _writeLock = new();
    private readonly ILogger<InProcessScriptArtifactChangeBus> _logger;

    // Copy-on-write: readers (Publish) take the reference without locking; writers
    // (Subscribe/unsubscribe) swap in a new immutable array under _writeLock.
    private volatile Action<ScriptArtifactsChanged>[] _handlers =
        Array.Empty<Action<ScriptArtifactsChanged>>();

    /// <summary>Initializes the bus with a logger for swallowed handler faults.</summary>
    /// <param name="logger">Logger used to record subscriber handler exceptions.</param>
    public InProcessScriptArtifactChangeBus(ILogger<InProcessScriptArtifactChangeBus> logger) =>
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));

    /// <inheritdoc />
    public void Publish(ScriptArtifactsChanged notification)
    {
        ArgumentNullException.ThrowIfNull(notification);

        // Snapshot without locking — the array is never mutated in place.
        var handlers = _handlers;
        foreach (var handler in handlers)
        {
            try
            {
                handler(notification);
            }
            catch (Exception ex)
            {
                // A bad consumer must NEVER fault the producer's commit path.
                _logger.LogError(
                    ex,
                    "Script-artifact-change subscriber threw handling {Kind} ({Count} names) from {Source}; ignored.",
                    notification.ArtifactKind,
                    notification.Names.Count,
                    notification.Source);
            }
        }
    }

    /// <inheritdoc />
    public IDisposable Subscribe(Action<ScriptArtifactsChanged> handler)
    {
        ArgumentNullException.ThrowIfNull(handler);

        lock (_writeLock)
        {
            var updated = new Action<ScriptArtifactsChanged>[_handlers.Length + 1];
            Array.Copy(_handlers, updated, _handlers.Length);
            updated[^1] = handler;
            _handlers = updated;
        }

        return new Subscription(this, handler);
    }

    private void Unsubscribe(Action<ScriptArtifactsChanged> handler)
    {
        lock (_writeLock)
        {
            var index = Array.IndexOf(_handlers, handler);
            if (index < 0)
            {
                return;
            }

            var updated = new Action<ScriptArtifactsChanged>[_handlers.Length - 1];
            Array.Copy(_handlers, 0, updated, 0, index);
            Array.Copy(_handlers, index + 1, updated, index, _handlers.Length - index - 1);
            _handlers = updated;
        }
    }

    private sealed class Subscription : IDisposable
    {
        private readonly InProcessScriptArtifactChangeBus _bus;
        private Action<ScriptArtifactsChanged>? _handler;

        /// <summary>Initializes a new instance of the <see cref="Subscription"/> class.</summary>
        /// <param name="bus">The bus this subscription is registered with.</param>
        /// <param name="handler">The handler to remove from the bus on <see cref="Dispose"/>.</param>
        public Subscription(InProcessScriptArtifactChangeBus bus, Action<ScriptArtifactsChanged> handler)
        {
            _bus = bus;
            _handler = handler;
        }

        /// <summary>Unsubscribes the handler from the bus. Idempotent — only the first call has an effect.</summary>
        public void Dispose()
        {
            // Idempotent: only the first Dispose removes the handler.
            var handler = Interlocked.Exchange(ref _handler, null);
            if (handler is not null)
            {
                _bus.Unsubscribe(handler);
            }
        }
    }
}
