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

/// <summary>
/// Pure helper for the inbound-API-key re-arch on the API-method form. The approval
/// relationship moved from <c>ApiMethod.ApprovedApiKeyIds</c> (a CSV on the method) onto
/// per-key method-scopes managed through <c>IInboundApiKeyAdmin</c>. When an operator edits
/// the "Approved API Keys" list for one method, we must reconcile that method's NAME into (or
/// out of) each affected key's scope set — without touching keys whose membership did not change.
/// </summary>
/// <remarks>
/// Factored out of the Blazor component so the add/remove/empty-guard logic is unit-testable
/// independent of bUnit rendering.
/// </remarks>
public static class ApiMethodKeyScopeReconciler
{
    /// <summary>
    /// The recomputed scope set for one key that must be persisted via
    /// <c>IInboundApiKeyAdmin.SetMethodsAsync</c>.
    /// </summary>
    /// <param name="KeyId">The key whose scopes changed.</param>
    /// <param name="NewMethods">The full new method-scope set for that key (always non-empty —
    /// the empty case is reported as a conflict instead).</param>
    public sealed record KeyScopeUpdate(string KeyId, IReadOnlyList<string> NewMethods);

    /// <summary>
    /// Outcome of reconciling one method's approved-key selection against the keys' current scopes.
    /// </summary>
    /// <param name="Updates">Per-affected-key new scope sets to push. Empty when nothing changed.</param>
    /// <param name="EmptyScopeKeyNames">Display names of keys for which revoking this method would
    /// leave zero scopes. When this is non-empty the caller MUST NOT apply <see cref="Updates"/> —
    /// it should surface a validation error and abort, because the server rejects empty scope sets.</param>
    public sealed record ReconcileResult(
        IReadOnlyList<KeyScopeUpdate> Updates,
        IReadOnlyList<string> EmptyScopeKeyNames);

    /// <summary>
    /// Computes the scope edits needed so that exactly the keys in <paramref name="selectedKeyIds"/>
    /// are scoped to <paramref name="methodName"/>.
    /// </summary>
    /// <param name="methodName">The method whose approval set is being edited. Must already exist
    /// in the store (save the ApiMethod entity FIRST so its name resolves).</param>
    /// <param name="selectedKeyIds">Keys the operator wants approved for this method (after edit).</param>
    /// <param name="initialKeyIds">Keys that were approved for this method when the form loaded
    /// (empty on create).</param>
    /// <param name="currentMethodsByKey">Each affected key's CURRENT full scope set, keyed by KeyId.
    /// Read fresh from the seam right before reconciling so concurrent edits do not get clobbered.</param>
    /// <param name="keyNamesById">Display names by KeyId, for human-readable empty-scope messages.</param>
    /// <returns>A <see cref="ReconcileResult"/> with the scope updates to apply and any empty-scope warnings.</returns>
    public static ReconcileResult Reconcile(
        string methodName,
        IReadOnlySet<string> selectedKeyIds,
        IReadOnlySet<string> initialKeyIds,
        IReadOnlyDictionary<string, IReadOnlyList<string>> currentMethodsByKey,
        IReadOnlyDictionary<string, string> keyNamesById)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(methodName);
        ArgumentNullException.ThrowIfNull(selectedKeyIds);
        ArgumentNullException.ThrowIfNull(initialKeyIds);
        ArgumentNullException.ThrowIfNull(currentMethodsByKey);
        ArgumentNullException.ThrowIfNull(keyNamesById);

        var added = selectedKeyIds.Except(initialKeyIds, StringComparer.Ordinal);
        var removed = initialKeyIds.Except(selectedKeyIds, StringComparer.Ordinal);

        var updates = new List<KeyScopeUpdate>();
        var emptyScopeKeyNames = new List<string>();

        // Approving: add this method's name to the key's current scope set (idempotent).
        foreach (var keyId in added)
        {
            var current = currentMethodsByKey.TryGetValue(keyId, out var m)
                ? m
                : (IReadOnlyList<string>)Array.Empty<string>();
            var next = new HashSet<string>(current, StringComparer.Ordinal) { methodName };
            updates.Add(new KeyScopeUpdate(keyId, next.OrderBy(s => s, StringComparer.Ordinal).ToList()));
        }

        // Revoking: remove this method's name. If that empties the key, it is a conflict —
        // the server rejects empty scope sets, so we report it and the caller must abort.
        foreach (var keyId in removed)
        {
            var current = currentMethodsByKey.TryGetValue(keyId, out var m)
                ? m
                : (IReadOnlyList<string>)Array.Empty<string>();
            var next = new HashSet<string>(current, StringComparer.Ordinal);
            next.Remove(methodName);

            if (next.Count == 0)
            {
                emptyScopeKeyNames.Add(
                    keyNamesById.TryGetValue(keyId, out var name) ? name : keyId);
                continue;
            }

            updates.Add(new KeyScopeUpdate(keyId, next.OrderBy(s => s, StringComparer.Ordinal).ToList()));
        }

        return new ReconcileResult(updates, emptyScopeKeyNames);
    }
}
