using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Core.Resilience;

/// <summary>
///     Wraps the three mutating surfaces of <see cref="IAlarmSource"/>
///     (<see cref="IAlarmSource.SubscribeAlarmsAsync"/>, <see cref="IAlarmSource.UnsubscribeAlarmsAsync"/>,
///     <see cref="IAlarmSource.AcknowledgeAsync"/>) through <see cref="CapabilityInvoker"/> so the
///     Phase 6.1 resilience pipeline runs — retry semantics match
///     <see cref="DriverCapability.AlarmSubscribe"/> (retries by default) and
///     <see cref="DriverCapability.AlarmAcknowledge"/> (does NOT retry).
/// </summary>
/// <remarks>
///     <para>Multi-host dispatch: when the driver implements <see cref="IPerCallHostResolver"/>,
///     each source-node-id is resolved individually + grouped by host so a dead PLC inside a
///     multi-device driver doesn't poison the sibling hosts' breakers. Drivers with a single
///     host fall back to <see cref="IDriver.DriverInstanceId"/> as the single-host key.</para>
///
///     <para>Why this lives here + not on <see cref="CapabilityInvoker"/>: alarm surfaces have a
///     handle-returning shape (SubscribeAlarmsAsync returns <see cref="IAlarmSubscriptionHandle"/>)
///     + a per-call fan-out (AcknowledgeAsync gets a batch of
///     <see cref="AlarmAcknowledgeRequest"/>s that may span multiple hosts). Keeping the fan-out
///     logic here keeps the invoker's execute-overloads narrow.</para>
/// </remarks>
public sealed class AlarmSurfaceInvoker
{
    private readonly CapabilityInvoker _invoker;
    private readonly IAlarmSource _alarmSource;
    private readonly IPerCallHostResolver? _hostResolver;
    private readonly string _defaultHost;

    /// <summary>Initializes a new instance of the AlarmSurfaceInvoker class.</summary>
    /// <param name="invoker">The capability invoker for resilience pipeline.</param>
    /// <param name="alarmSource">The alarm source to invoke.</param>
    /// <param name="defaultHost">The default host name for single-host scenarios.</param>
    /// <param name="hostResolver">Optional per-call host resolver for multi-host dispatch.</param>
    public AlarmSurfaceInvoker(
        CapabilityInvoker invoker,
        IAlarmSource alarmSource,
        string defaultHost,
        IPerCallHostResolver? hostResolver = null)
    {
        ArgumentNullException.ThrowIfNull(invoker);
        ArgumentNullException.ThrowIfNull(alarmSource);
        ArgumentException.ThrowIfNullOrWhiteSpace(defaultHost);

        _invoker = invoker;
        _alarmSource = alarmSource;
        _defaultHost = defaultHost;
        _hostResolver = hostResolver;
    }

    /// <summary>
    ///     Subscribe to alarm events for a set of source node ids, fanning out by resolved host
    ///     so per-host breakers apply. Returns one handle per host — callers that
    ///     don't care about per-host separation may concatenate them. Each returned handle wraps
    ///     the driver's opaque handle together with its resolved host so <see cref="UnsubscribeAsync"/>
    ///     routes through the same host's pipeline that the subscription was created on.
    /// </summary>
    /// <param name="sourceNodeIds">The source node IDs to subscribe to.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>One subscription handle per resolved host.</returns>
    public async Task<IReadOnlyList<IAlarmSubscriptionHandle>> SubscribeAsync(
        IReadOnlyList<string> sourceNodeIds,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(sourceNodeIds);
        if (sourceNodeIds.Count == 0) return [];

        var byHost = GroupByHost(sourceNodeIds);
        var handles = new List<IAlarmSubscriptionHandle>(byHost.Count);
        foreach (var (host, ids) in byHost)
        {
            var inner = await _invoker.ExecuteAsync(
                DriverCapability.AlarmSubscribe,
                host,
                async ct => await _alarmSource.SubscribeAlarmsAsync(ids, ct).ConfigureAwait(false),
                cancellationToken).ConfigureAwait(false);
            handles.Add(new HostBoundHandle(inner, host));
        }
        return handles;
    }

    /// <summary>
    ///     Cancel an alarm subscription. Routes through the same host's resilience pipeline
    ///     that the subscription was created on (carried in the <see cref="HostBoundHandle"/>
    ///     wrapper returned by <see cref="SubscribeAsync"/>). Falls back to the default host for
    ///     handles not created by this invoker so the method remains safe to call on any
    ///     <see cref="IAlarmSubscriptionHandle"/> implementation.
    /// </summary>
    /// <param name="handle">The subscription handle to unsubscribe.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public ValueTask UnsubscribeAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(handle);
        var (innerHandle, host) = handle is HostBoundHandle bound
            ? (bound.Inner, bound.Host)
            : (handle, _defaultHost);

        return _invoker.ExecuteAsync(
            DriverCapability.AlarmSubscribe,
            host,
            async ct => await _alarmSource.UnsubscribeAlarmsAsync(innerHandle, ct).ConfigureAwait(false),
            cancellationToken);
    }

    /// <summary>
    ///     Acknowledge alarms. Fans out by resolved host; each host's batch runs through the
    ///     AlarmAcknowledge pipeline (no-retry — an alarm-ack is not idempotent
    ///     at the plant-floor acknowledgement level even if the OPC UA spec permits re-issue).
    /// </summary>
    /// <param name="acknowledgements">The alarm acknowledgement requests.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public async Task AcknowledgeAsync(
        IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(acknowledgements);
        if (acknowledgements.Count == 0) return;

        var byHost = _hostResolver is null
            ? new Dictionary<string, List<AlarmAcknowledgeRequest>> { [_defaultHost] = acknowledgements.ToList() }
            : acknowledgements
                .GroupBy(a => _hostResolver.ResolveHost(a.SourceNodeId))
                .ToDictionary(g => g.Key, g => g.ToList());

        foreach (var (host, batch) in byHost)
        {
            var batchSnapshot = batch; // capture for the lambda
            await _invoker.ExecuteAsync(
                DriverCapability.AlarmAcknowledge,
                host,
                async ct => await _alarmSource.AcknowledgeAsync(batchSnapshot, ct).ConfigureAwait(false),
                cancellationToken).ConfigureAwait(false);
        }
    }

    private Dictionary<string, List<string>> GroupByHost(IReadOnlyList<string> sourceNodeIds)
    {
        if (_hostResolver is null)
            return new Dictionary<string, List<string>> { [_defaultHost] = sourceNodeIds.ToList() };

        var result = new Dictionary<string, List<string>>(StringComparer.Ordinal);
        foreach (var id in sourceNodeIds)
        {
            var host = _hostResolver.ResolveHost(id);
            if (!result.TryGetValue(host, out var list))
                result[host] = list = new List<string>();
            list.Add(id);
        }
        return result;
    }

    /// <summary>
    ///     Wraps an <see cref="IAlarmSubscriptionHandle"/> returned by the driver with the
    ///     resolved host name used when the subscription was created. <see cref="UnsubscribeAsync"/>
    ///     unwraps this to route the unsubscribe through the same host's resilience pipeline.
    /// </summary>
    private sealed class HostBoundHandle(IAlarmSubscriptionHandle inner, string host) : IAlarmSubscriptionHandle
    {
        /// <summary>Gets the inner subscription handle.</summary>
        public IAlarmSubscriptionHandle Inner { get; } = inner;
        /// <summary>Gets the resolved host name.</summary>
        public string Host { get; } = host;
        /// <inheritdoc />
        public string DiagnosticId => Inner.DiagnosticId;
    }
}
