using System.Collections.Concurrent;
using NATS.Server.Imports;
using NATS.Server.Subscriptions;

namespace NATS.Server.Auth;

public sealed class Account : IDisposable
{
    public const string GlobalAccountName = "$G";
    public const string SystemAccountName = "$SYS";
    public const string ClientInfoHdr = "Nats-Request-Info";

    public string Name { get; }
    public SubList SubList { get; } = new();
    public Permissions? DefaultPermissions { get; set; }
    public int MaxConnections { get; set; } // 0 = unlimited
    public int MaxSubscriptions { get; set; } // 0 = unlimited
    public ExportMap Exports { get; } = new();
    public ImportMap Imports { get; } = new();
    public int MaxJetStreamStreams { get; set; } // 0 = unlimited
    public string? JetStreamTier { get; set; }

    /// <summary>
    /// Indicates whether this account is the designated system account.
    /// The system account owns $SYS.> subjects for internal server-to-server communication.
    /// Reference: Go server/accounts.go — isSystemAccount().
    /// </summary>
    public bool IsSystemAccount { get; set; }

    /// <summary>Per-account JetStream resource limits (storage, consumers, ack pending).</summary>
    public AccountLimits JetStreamLimits { get; set; } = AccountLimits.Unlimited;

    // JWT fields
    public string? Nkey { get; set; }
    public string? Issuer { get; set; }
    public Dictionary<string, object>? SigningKeys { get; set; }
    private readonly ConcurrentDictionary<string, long> _revokedUsers = new(StringComparer.Ordinal);

    /// <summary>Special key used to revoke all users regardless of their NKey.</summary>
    /// <remarks>Go reference: jwt.All constant used in accounts.go isRevoked (~line 2934).</remarks>
    private const string GlobalRevocationKey = "*";

    public void RevokeUser(string userNkey, long issuedAt) => _revokedUsers[userNkey] = issuedAt;

    /// <summary>
    /// Revokes all users with a JWT issued at or before <paramref name="issuedBefore"/>.
    /// Uses the special key "*" (global revocation) to indicate all users are revoked
    /// up to the given timestamp.
    /// Go reference: accounts.go — Revocations[jwt.All] assignment (~line 3887).
    /// </summary>
    public void RevokeAllUsers(long issuedBefore) => _revokedUsers[GlobalRevocationKey] = issuedBefore;

    public bool IsUserRevoked(string userNkey, long issuedAt)
    {
        if (_revokedUsers.TryGetValue(userNkey, out var revokedAt))
            return issuedAt <= revokedAt;
        // Check "*" wildcard for all-user revocation
        if (_revokedUsers.TryGetValue(GlobalRevocationKey, out revokedAt))
            return issuedAt <= revokedAt;
        return false;
    }

    /// <summary>Returns the number of revoked user NKey entries (including any global revocation entry).</summary>
    public int RevokedUserCount => _revokedUsers.Count;

    /// <summary>Returns true when a global ("*") revocation entry is present.</summary>
    /// <remarks>Go reference: accounts.go — Revocations[jwt.All] check (~line 3887).</remarks>
    public bool IsGlobalRevocation() => _revokedUsers.ContainsKey(GlobalRevocationKey);

    /// <summary>Returns a snapshot list of all revoked NKeys currently in the revocation dictionary.</summary>
    public IReadOnlyList<string> GetRevokedUsers() => [.. _revokedUsers.Keys];

    /// <summary>
    /// Removes the revocation entry for <paramref name="userNkey"/>.
    /// Returns <see langword="true"/> if the entry was found and removed.
    /// </summary>
    public bool UnrevokeUser(string userNkey) => _revokedUsers.TryRemove(userNkey, out _);

    /// <summary>Removes all revocation entries, including any global ("*") revocation.</summary>
    public void ClearAllRevocations() => _revokedUsers.Clear();

    /// <summary>Returns a summary snapshot of the current revocation state for this account.</summary>
    public RevocationInfo GetRevocationInfo() =>
        new(RevokedUserCount, IsGlobalRevocation(), GetRevokedUsers());

    private readonly ConcurrentDictionary<ulong, byte> _clients = new();
    private int _subscriptionCount;
    private int _jetStreamStreamCount;
    private int _consumerCount;
    private long _storageUsed;

    public Account(string name)
    {
        Name = name;
    }

    public int ClientCount => _clients.Count;
    public int SubscriptionCount => Volatile.Read(ref _subscriptionCount);
    public int JetStreamStreamCount => Volatile.Read(ref _jetStreamStreamCount);
    public int ConsumerCount => Volatile.Read(ref _consumerCount);
    public long StorageUsed => Interlocked.Read(ref _storageUsed);

    /// <summary>Returns false if max connections exceeded.</summary>
    public bool AddClient(ulong clientId)
    {
        if (MaxConnections > 0 && _clients.Count >= MaxConnections)
            return false;
        _clients[clientId] = 0;
        return true;
    }

    public void RemoveClient(ulong clientId) => _clients.TryRemove(clientId, out _);

    public bool IncrementSubscriptions()
    {
        if (MaxSubscriptions > 0 && Volatile.Read(ref _subscriptionCount) >= MaxSubscriptions)
            return false;
        Interlocked.Increment(ref _subscriptionCount);
        return true;
    }

    public void DecrementSubscriptions()
    {
        Interlocked.Decrement(ref _subscriptionCount);
    }

    /// <summary>
    /// Reserves a stream slot, checking both <see cref="MaxJetStreamStreams"/> (legacy)
    /// and <see cref="JetStreamLimits"/>.<see cref="AccountLimits.MaxStreams"/>.
    /// </summary>
    public bool TryReserveStream()
    {
        var effectiveMax = JetStreamLimits.MaxStreams > 0
            ? JetStreamLimits.MaxStreams
            : MaxJetStreamStreams;

        if (effectiveMax > 0 && Volatile.Read(ref _jetStreamStreamCount) >= effectiveMax)
            return false;

        Interlocked.Increment(ref _jetStreamStreamCount);
        return true;
    }

    public void ReleaseStream()
    {
        if (Volatile.Read(ref _jetStreamStreamCount) == 0)
            return;

        Interlocked.Decrement(ref _jetStreamStreamCount);
    }

    /// <summary>Reserves a consumer slot. Returns false if <see cref="AccountLimits.MaxConsumers"/> is exceeded.</summary>
    public bool TryReserveConsumer()
    {
        var max = JetStreamLimits.MaxConsumers;
        if (max > 0 && Volatile.Read(ref _consumerCount) >= max)
            return false;

        Interlocked.Increment(ref _consumerCount);
        return true;
    }

    public void ReleaseConsumer()
    {
        if (Volatile.Read(ref _consumerCount) == 0)
            return;

        Interlocked.Decrement(ref _consumerCount);
    }

    /// <summary>
    /// Adjusts the tracked storage usage by <paramref name="deltaBytes"/>.
    /// Returns false if the positive delta would exceed <see cref="AccountLimits.MaxStorage"/>.
    /// A negative delta always succeeds.
    /// </summary>
    public bool TrackStorageDelta(long deltaBytes)
    {
        var maxStorage = JetStreamLimits.MaxStorage;

        if (deltaBytes > 0 && maxStorage > 0)
        {
            var current = Interlocked.Read(ref _storageUsed);
            if (current + deltaBytes > maxStorage)
                return false;
        }

        Interlocked.Add(ref _storageUsed, deltaBytes);
        return true;
    }

    // Generation ID for permission cache invalidation.
    // Callers hold a per-client copy; when it diverges from GenerationId they must flush their caches.
    // Reference: Go server/accounts.go — account generation tracking for permission invalidation.
    private long _generationId;

    public long GenerationId => Interlocked.Read(ref _generationId);

    /// <summary>Increments the generation counter, signalling that permission caches are stale.</summary>
    public void IncrementGeneration() => Interlocked.Increment(ref _generationId);

    // Slow consumer tracking
    // Go reference: server/client.go — handleSlowConsumer, markConnAsSlow, server/accounts.go slowConsumerCount
    private long _slowConsumerCount;

    public long SlowConsumerCount => Interlocked.Read(ref _slowConsumerCount);

    public void IncrementSlowConsumers() => Interlocked.Increment(ref _slowConsumerCount);

    public void ResetSlowConsumerCount() => Interlocked.Exchange(ref _slowConsumerCount, 0L);

    // Per-account message/byte stats
    private long _inMsgs;
    private long _outMsgs;
    private long _inBytes;
    private long _outBytes;

    public long InMsgs => Interlocked.Read(ref _inMsgs);
    public long OutMsgs => Interlocked.Read(ref _outMsgs);
    public long InBytes => Interlocked.Read(ref _inBytes);
    public long OutBytes => Interlocked.Read(ref _outBytes);

    public void IncrementInbound(long msgs, long bytes)
    {
        Interlocked.Add(ref _inMsgs, msgs);
        Interlocked.Add(ref _inBytes, bytes);
    }

    public void IncrementOutbound(long msgs, long bytes)
    {
        Interlocked.Add(ref _outMsgs, msgs);
        Interlocked.Add(ref _outBytes, bytes);
    }

    // Internal (ACCOUNT) client for import/export message routing
    private InternalClient? _internalClient;

    public InternalClient GetOrCreateInternalClient(ulong clientId)
    {
        if (_internalClient != null) return _internalClient;
        _internalClient = new InternalClient(clientId, ClientKind.Account, this);
        return _internalClient;
    }

    // Service export latency tracking
    // Go reference: accounts.go serviceLatency / serviceExportLatencyStats.
    public ServiceLatencyTracker LatencyTracker { get; } = new();

    /// <summary>Records a service request latency sample on this account's tracker.</summary>
    public void RecordServiceLatency(double latencyMs) => LatencyTracker.RecordLatency(latencyMs);

    /// <summary>
    /// Returns all service exports registered on this account.
    /// Go reference: accounts.go exports.services iteration.
    /// </summary>
    public IReadOnlyList<ServiceExportInfo> GetAllServiceExports()
    {
        var result = new List<ServiceExportInfo>(Exports.Services.Count);
        foreach (var (subject, se) in Exports.Services)
            result.Add(ToServiceExportInfo(subject, se));
        return result;
    }

    /// <summary>
    /// Returns the service export for an exact subject match, or null if not found.
    /// Does not apply wildcard matching.
    /// Go reference: accounts.go getServiceExport (direct map lookup only).
    /// </summary>
    public ServiceExportInfo? GetExactServiceExport(string subject)
    {
        if (Exports.Services.TryGetValue(subject, out var se))
            return ToServiceExportInfo(subject, se);
        return null;
    }

    /// <summary>
    /// Finds a service export whose subject pattern matches the given subject using
    /// wildcard matching. Returns null when no export pattern matches.
    /// Go reference: accounts.go getWildcardServiceExport (line 2849).
    /// </summary>
    public ServiceExportInfo? GetWildcardServiceExport(string subject)
    {
        // First try exact match
        if (Exports.Services.TryGetValue(subject, out var exact))
            return ToServiceExportInfo(subject, exact);

        // Then scan for a wildcard pattern that matches
        foreach (var (pattern, se) in Exports.Services)
        {
            if (SubjectMatch.MatchLiteral(subject, pattern))
                return ToServiceExportInfo(pattern, se);
        }
        return null;
    }

    /// <summary>
    /// Returns true when any service export (exact or wildcard) matches the given subject.
    /// Go reference: accounts.go getServiceExport.
    /// </summary>
    public bool HasServiceExport(string subject) => GetWildcardServiceExport(subject) != null;

    private static ServiceExportInfo ToServiceExportInfo(string subject, ServiceExport se)
    {
        IReadOnlyList<string> approved = se.Auth.ApprovedAccounts != null
            ? [.. se.Auth.ApprovedAccounts]
            : [];
        bool isWildcard = subject.Contains('*') || subject.Contains('>');
        return new ServiceExportInfo(subject, se.ResponseType, approved, isWildcard);
    }

    public void AddServiceExport(string subject, ServiceResponseType responseType, IEnumerable<Account>? approved)
    {
        var auth = new ExportAuth
        {
            ApprovedAccounts = approved != null ? new HashSet<string>(approved.Select(a => a.Name)) : null,
        };
        Exports.Services[subject] = new ServiceExport
        {
            Auth = auth,
            Account = this,
            ResponseType = responseType,
        };
    }

    public void AddStreamExport(string subject, IEnumerable<Account>? approved)
    {
        var auth = new ExportAuth
        {
            ApprovedAccounts = approved != null ? new HashSet<string>(approved.Select(a => a.Name)) : null,
        };
        Exports.Streams[subject] = new StreamExport { Auth = auth };
    }

    /// <summary>
    /// Adds a service import with cycle detection.
    /// Go reference: accounts.go addServiceImport with checkForImportCycle.
    /// </summary>
    /// <exception cref="InvalidOperationException">Thrown if no export found or import would create a cycle.</exception>
    /// <exception cref="UnauthorizedAccessException">Thrown if this account is not authorized.</exception>
    public ServiceImport AddServiceImport(Account destination, string from, string to)
    {
        if (!destination.Exports.Services.TryGetValue(to, out var export))
            throw new InvalidOperationException($"No service export found for '{to}' on account '{destination.Name}'");

        if (!export.Auth.IsAuthorized(this))
            throw new UnauthorizedAccessException($"Account '{Name}' not authorized to import '{to}' from '{destination.Name}'");

        // Cycle detection: check if adding this import from destination would
        // create a path back to this account.
        if (AccountImportExport.DetectCycle(destination, this))
            throw new InvalidOperationException("Import would create a cycle");

        var si = new ServiceImport
        {
            DestinationAccount = destination,
            From = from,
            To = to,
            Export = export,
            ResponseType = export.ResponseType,
        };

        Imports.AddServiceImport(si);
        return si;
    }

    /// <summary>Removes a service import by its 'from' subject.</summary>
    /// <returns>True if the import was found and removed.</returns>
    public bool RemoveServiceImport(string from)
    {
        return Imports.Services.Remove(from);
    }

    public void AddStreamImport(Account source, string from, string to)
    {
        if (!source.Exports.Streams.TryGetValue(from, out var export))
            throw new InvalidOperationException($"No stream export found for '{from}' on account '{source.Name}'");

        if (!export.Auth.IsAuthorized(this))
            throw new UnauthorizedAccessException($"Account '{Name}' not authorized to import '{from}' from '{source.Name}'");

        var si = new StreamImport
        {
            SourceAccount = source,
            From = from,
            To = to,
        };

        Imports.Streams.Add(si);
    }

    /// <summary>Removes a stream import by its 'from' subject.</summary>
    /// <returns>True if the import was found and removed.</returns>
    public bool RemoveStreamImport(string from)
    {
        var idx = Imports.Streams.FindIndex(s => string.Equals(s.From, from, StringComparison.Ordinal));
        if (idx < 0)
            return false;
        Imports.Streams.RemoveAt(idx);
        return true;
    }

    /// <summary>
    /// Returns true if adding a stream import from <paramref name="proposedSource"/> would create a cycle.
    /// Uses DFS through the stream import graph starting at proposedSource, checking if any path leads back to this account.
    /// Go reference: accounts.go streamImportFormsCycle / checkStreamImportsForCycles.
    /// </summary>
    public bool StreamImportFormsCycle(Account proposedSource)
    {
        ArgumentNullException.ThrowIfNull(proposedSource);
        var visited = new HashSet<string>(StringComparer.Ordinal);
        return DetectStreamImportCycle(proposedSource, visited);
    }

    private bool DetectStreamImportCycle(Account current, HashSet<string> visited)
    {
        // If we've reached this account, a cycle exists.
        if (string.Equals(current.Name, Name, StringComparison.Ordinal))
            return true;

        // Guard against revisiting nodes (handles diamonds and other shared paths).
        if (!visited.Add(current.Name))
            return false;

        foreach (var sourceAccount in current.Imports.GetStreamImportSourceAccounts())
        {
            if (DetectStreamImportCycle(sourceAccount, visited))
                return true;
        }

        return false;
    }

    /// <summary>
    /// Returns the names of all accounts this account imports streams from.
    /// Go reference: accounts.go imports.streams — acc field on each streamImport.
    /// </summary>
    public IReadOnlyList<string> GetStreamImportSources()
    {
        var sources = Imports.GetStreamImportSourceAccounts();
        if (sources.Count == 0)
            return [];
        var names = new List<string>(sources.Count);
        foreach (var acc in sources)
            names.Add(acc.Name);
        return names;
    }

    /// <summary>
    /// Returns true if this account has at least one stream import from the account with the given name.
    /// </summary>
    public bool HasStreamImportFrom(string accountName) =>
        Imports.Streams.Exists(si => string.Equals(si.SourceAccount.Name, accountName, StringComparison.Ordinal));

    // Per-subject service response thresholds.
    // Go reference: server/accounts.go — serviceExport.respThresh, SetServiceExportResponseThreshold, ServiceExportResponseThreshold.
    public ConcurrentDictionary<string, TimeSpan> ServiceResponseThresholds { get; } =
        new(StringComparer.Ordinal);

    /// <summary>
    /// Sets the maximum time a service export responder may take to reply.
    /// Go reference: accounts.go SetServiceExportResponseThreshold (~line 2522).
    /// </summary>
    public void SetServiceResponseThreshold(string subject, TimeSpan threshold) =>
        ServiceResponseThresholds[subject] = threshold;

    /// <summary>
    /// Returns the threshold for <paramref name="subject"/>, or <see langword="null"/> if none is set.
    /// Go reference: accounts.go ServiceExportResponseThreshold (~line 2510).
    /// </summary>
    public TimeSpan? GetServiceResponseThreshold(string subject) =>
        ServiceResponseThresholds.TryGetValue(subject, out var t) ? t : null;

    /// <summary>
    /// Returns <see langword="true"/> if <paramref name="elapsed"/> exceeds the registered threshold
    /// for <paramref name="subject"/>. When no threshold is set the response is never considered overdue.
    /// Go reference: accounts.go — respThresh check inside response-timer logic.
    /// </summary>
    public bool IsServiceResponseOverdue(string subject, TimeSpan elapsed)
    {
        if (!ServiceResponseThresholds.TryGetValue(subject, out var threshold))
            return false;
        return elapsed > threshold;
    }

    /// <summary>
    /// Combines threshold lookup and overdue check into a single result.
    /// Go reference: accounts.go — ServiceExportResponseThreshold + response-timer logic.
    /// </summary>
    public ServiceResponseThresholdResult CheckServiceResponse(string subject, TimeSpan elapsed)
    {
        if (!ServiceResponseThresholds.TryGetValue(subject, out var threshold))
            return new ServiceResponseThresholdResult(Found: false, IsOverdue: false, Threshold: null, Elapsed: elapsed);

        var overdue = elapsed > threshold;
        return new ServiceResponseThresholdResult(Found: true, IsOverdue: overdue, Threshold: threshold, Elapsed: elapsed);
    }

    // Account expiration / TTL
    // Go reference: server/accounts.go — account expiry fields and SetExpirationTimer.
    //
    // Implementation note: Volatile.Read/Write require reference types; DateTime? is a value type.
    // We store expiry as UTC ticks in a long field. long.MinValue is the "no expiration" sentinel
    // (default value for an uninitialized long field).
    private const long NoExpiration = long.MinValue;
    private long _expiresAtTicks = NoExpiration;

    // Converts the raw ticks field to a nullable DateTime for callers.
    private DateTime? ReadExpiry()
    {
        var ticks = Interlocked.Read(ref _expiresAtTicks);
        return ticks == NoExpiration ? null : new DateTime(ticks, DateTimeKind.Utc);
    }

    /// <summary>
    /// The UTC time at which this account expires, or <see langword="null"/> when no expiration is set.
    /// Go reference: accounts.go — account.expiry field.
    /// </summary>
    public DateTime? ExpiresAt => ReadExpiry();

    /// <summary>
    /// Returns <see langword="true"/> when an expiration has been set and
    /// <see cref="DateTime.UtcNow"/> is at or past <see cref="ExpiresAt"/>.
    /// Go reference: accounts.go — isExpired() check.
    /// </summary>
    public bool IsExpired
    {
        get
        {
            var ticks = Interlocked.Read(ref _expiresAtTicks);
            return ticks != NoExpiration && DateTime.UtcNow.Ticks >= ticks;
        }
    }

    /// <summary>
    /// Returns the time remaining until expiry, <see cref="TimeSpan.Zero"/> if already expired,
    /// or <see langword="null"/> when no expiration is set.
    /// Go reference: accounts.go — expiry-based TTL computation.
    /// </summary>
    public TimeSpan? TimeToExpiry
    {
        get
        {
            var ticks = Interlocked.Read(ref _expiresAtTicks);
            if (ticks == NoExpiration)
                return null;
            var remaining = ticks - DateTime.UtcNow.Ticks;
            return remaining > 0 ? TimeSpan.FromTicks(remaining) : TimeSpan.Zero;
        }
    }

    /// <summary>
    /// Sets the UTC expiration time for this account.
    /// Go reference: accounts.go — SetExpirationTimer / account.expiry assignment.
    /// </summary>
    public void SetExpiration(DateTime expiresAtUtc) =>
        Interlocked.Exchange(ref _expiresAtTicks, DateTime.SpecifyKind(expiresAtUtc, DateTimeKind.Utc).Ticks);

    /// <summary>Removes any previously set expiration, making the account permanent.</summary>
    public void ClearExpiration() => Interlocked.Exchange(ref _expiresAtTicks, NoExpiration);

    /// <summary>
    /// Convenience method: sets the expiration to <c>DateTime.UtcNow + <paramref name="ttl"/></c>.
    /// Go reference: accounts.go — SetExpirationTimer with duration argument.
    /// </summary>
    public void SetExpirationFromTtl(TimeSpan ttl) => SetExpiration(DateTime.UtcNow + ttl);

    /// <summary>
    /// Returns a snapshot of the current expiration state for this account.
    /// Go reference: accounts.go — account expiry inspection.
    /// </summary>
    public AccountExpirationInfo GetExpirationInfo()
    {
        var ticks = Interlocked.Read(ref _expiresAtTicks);
        bool hasExpiration = ticks != NoExpiration;
        DateTime? exp = hasExpiration ? new DateTime(ticks, DateTimeKind.Utc) : null;
        bool isExpired = hasExpiration && DateTime.UtcNow.Ticks >= ticks;
        TimeSpan? tte = hasExpiration
            ? (isExpired ? TimeSpan.Zero : TimeSpan.FromTicks(ticks - DateTime.UtcNow.Ticks))
            : null;
        return new AccountExpirationInfo(Name, hasExpiration, exp, isExpired, tte);
    }

    // JWT Activation claim expiration tracking
    // Go reference: server/accounts.go — checkActivation (~line 2943), activationExpired (~line 2920).
    private readonly ConcurrentDictionary<string, ActivationClaim> _activations =
        new(StringComparer.Ordinal);

    /// <summary>
    /// Registers a JWT activation claim for the given subject.
    /// Go reference: accounts.go — checkActivation registers expiry timers for activation tokens.
    /// </summary>
    public void RegisterActivation(string subject, ActivationClaim claim) =>
        _activations[subject] = claim;

    /// <summary>
    /// Checks whether the activation for <paramref name="subject"/> has expired.
    /// Returns a result indicating whether the claim was found and whether it is expired.
    /// Go reference: accounts.go — checkActivation (~line 2943): act.Expires &lt;= tn ⇒ expired.
    /// </summary>
    public ActivationCheckResult CheckActivationExpiry(string subject)
    {
        if (!_activations.TryGetValue(subject, out var claim))
            return new ActivationCheckResult(Found: false, IsExpired: false, ExpiresAt: null, TimeToExpiry: null);

        var expired = claim.IsExpired;
        var tte = expired ? TimeSpan.Zero : claim.TimeToExpiry;
        return new ActivationCheckResult(Found: true, IsExpired: expired, ExpiresAt: claim.ExpiresAt, TimeToExpiry: tte);
    }

    /// <summary>
    /// Returns <see langword="true"/> when the activation for <paramref name="subject"/> exists
    /// and has passed its expiry time.
    /// Go reference: accounts.go — act.Expires &lt;= tn check inside checkActivation.
    /// </summary>
    public bool IsActivationExpired(string subject) =>
        _activations.TryGetValue(subject, out var claim) && claim.IsExpired;

    /// <summary>
    /// Returns all subjects whose activation claim has expired.
    /// Go reference: accounts.go — activationExpired fires per-subject when expiry timer triggers.
    /// </summary>
    public IReadOnlyList<string> GetExpiredActivations()
    {
        var expired = new List<string>();
        foreach (var (subject, claim) in _activations)
        {
            if (claim.IsExpired)
                expired.Add(subject);
        }
        return expired;
    }

    /// <summary>
    /// Removes all expired activation claims and returns the count removed.
    /// Go reference: accounts.go — activationExpired cleans up service/stream imports.
    /// </summary>
    public int RemoveExpiredActivations()
    {
        int count = 0;
        foreach (var (subject, claim) in _activations)
        {
            if (claim.IsExpired && _activations.TryRemove(subject, out _))
                count++;
        }
        return count;
    }

    /// <summary>
    /// Returns the number of activation claims that have not yet expired.
    /// Go reference: accounts.go — active activation tracking.
    /// </summary>
    public int ActiveActivationCount
    {
        get
        {
            int count = 0;
            foreach (var (_, claim) in _activations)
            {
                if (!claim.IsExpired)
                    count++;
            }
            return count;
        }
    }

    // Account claim hot-reload support.
    // Go reference: server/accounts.go — UpdateAccountClaims / updateAccountClaimsWithRefresh (~line 3287).
    private AccountClaimData? _currentClaims;
    private int _claimUpdateCount;

    /// <summary>The most recently applied claim data, or null if no claims have been applied.</summary>
    public AccountClaimData? CurrentClaims => Volatile.Read(ref _currentClaims);

    /// <summary>Returns true if at least one claim update has been applied to this account.</summary>
    public bool HasClaims => Volatile.Read(ref _currentClaims) != null;

    /// <summary>Total number of successful (changed) claim updates applied to this account.</summary>
    public int ClaimUpdateCount => Volatile.Read(ref _claimUpdateCount);

    /// <summary>
    /// Applies <paramref name="newClaims"/> to the account using diff-based comparison.
    /// Only changed fields are updated. When any field changes, the generation counter is
    /// incremented so that per-client permission caches are invalidated.
    /// Go reference: server/accounts.go UpdateAccountClaims / updateAccountClaimsWithRefresh (~line 3287).
    /// </summary>
    public AccountClaimUpdateResult UpdateAccountClaims(AccountClaimData newClaims)
    {
        ArgumentNullException.ThrowIfNull(newClaims);

        var old = Volatile.Read(ref _currentClaims);
        var changedFields = new List<string>();

        if (old == null)
        {
            // First-time application — every present field is considered "changed".
            if (newClaims.MaxConnections.HasValue)
                changedFields.Add(nameof(AccountClaimData.MaxConnections));
            if (newClaims.MaxSubscriptions.HasValue)
                changedFields.Add(nameof(AccountClaimData.MaxSubscriptions));
            if (newClaims.Nkey != null)
                changedFields.Add(nameof(AccountClaimData.Nkey));
            if (newClaims.Issuer != null)
                changedFields.Add(nameof(AccountClaimData.Issuer));
            if (newClaims.ExpiresAt.HasValue)
                changedFields.Add(nameof(AccountClaimData.ExpiresAt));
        }
        else
        {
            if (old.MaxConnections != newClaims.MaxConnections)
                changedFields.Add(nameof(AccountClaimData.MaxConnections));
            if (old.MaxSubscriptions != newClaims.MaxSubscriptions)
                changedFields.Add(nameof(AccountClaimData.MaxSubscriptions));
            if (!string.Equals(old.Nkey, newClaims.Nkey, StringComparison.Ordinal))
                changedFields.Add(nameof(AccountClaimData.Nkey));
            if (!string.Equals(old.Issuer, newClaims.Issuer, StringComparison.Ordinal))
                changedFields.Add(nameof(AccountClaimData.Issuer));
            if (old.ExpiresAt != newClaims.ExpiresAt)
                changedFields.Add(nameof(AccountClaimData.ExpiresAt));
        }

        bool changed = changedFields.Count > 0;

        if (changed)
        {
            // Apply changed fields to the live account properties.
            if (newClaims.MaxConnections.HasValue)
                MaxConnections = newClaims.MaxConnections.Value;
            if (newClaims.MaxSubscriptions.HasValue)
                MaxSubscriptions = newClaims.MaxSubscriptions.Value;
            Nkey = newClaims.Nkey;
            Issuer = newClaims.Issuer;

            Volatile.Write(ref _currentClaims, newClaims);
            var updateCount = Interlocked.Increment(ref _claimUpdateCount);
            IncrementGeneration();
            return new AccountClaimUpdateResult(Changed: true, ChangedFields: changedFields, UpdateCount: updateCount);
        }

        return new AccountClaimUpdateResult(Changed: false, ChangedFields: [], UpdateCount: Volatile.Read(ref _claimUpdateCount));
    }

    // Reverse response map for cross-account request-reply routing.
    // When a service import rewrites the reply subject, a reverse mapping is stored so that
    // when the response arrives it can be forwarded back to the original requester's account.
    // Go reference: server/accounts.go — addRespMapEntry / checkForReverseEntries.
    private readonly ConcurrentDictionary<string, ReverseResponseMapEntry> _reverseResponseMap =
        new(StringComparer.Ordinal);

    /// <summary>
    /// Adds (or overwrites) a reverse response mapping for <paramref name="replySubject"/>.
    /// Records which origin account and original reply subject to route the response back to.
    /// Go reference: accounts.go addRespMapEntry.
    /// </summary>
    public void AddReverseRespMapEntry(string replySubject, string originAccount, string originalReply) =>
        _reverseResponseMap[replySubject] = new ReverseResponseMapEntry(
            replySubject, originAccount, originalReply, DateTime.UtcNow);

    /// <summary>
    /// Looks up the reverse response map entry for <paramref name="replySubject"/>.
    /// Returns <see langword="null"/> when no mapping exists.
    /// Go reference: accounts.go checkForReverseEntries.
    /// </summary>
    public ReverseResponseMapEntry? CheckForReverseEntries(string replySubject) =>
        _reverseResponseMap.TryGetValue(replySubject, out var entry) ? entry : null;

    /// <summary>
    /// Removes the reverse response mapping for <paramref name="replySubject"/>.
    /// Returns <see langword="true"/> if the entry was found and removed.
    /// </summary>
    public bool RemoveReverseRespMapEntry(string replySubject) =>
        _reverseResponseMap.TryRemove(replySubject, out _);

    /// <summary>The number of active reverse response map entries.</summary>
    public int ReverseResponseMapCount => _reverseResponseMap.Count;

    /// <summary>Removes all reverse response map entries.</summary>
    public void ClearReverseResponseMap() => _reverseResponseMap.Clear();

    /// <summary>Returns a snapshot of all reply subjects currently in the reverse response map.</summary>
    public IReadOnlyList<string> GetReverseResponseMapKeys() => [.. _reverseResponseMap.Keys];

    /// <summary>
    /// Checks whether any local subscription in this account's SubList would shadow
    /// (intercept) messages on the given service import subject, preventing the import
    /// from receiving them.
    /// Go reference: accounts.go serviceImportShadowed (~line 2015).
    /// </summary>
    public bool ServiceImportShadowed(string importSubject)
    {
        var matchResult = SubList.Match(importSubject);
        return matchResult.PlainSubs.Length > 0 || matchResult.QueueSubs.Length > 0;
    }

    /// <summary>
    /// Returns true if this account has at least one matching subscription for the given subject.
    /// Go reference: accounts.go SubscriptionInterest.
    /// </summary>
    public bool SubscriptionInterest(string subject) => Interest(subject) > 0;

    /// <summary>
    /// Returns the total number of matching subscriptions (plain + queue) for the given subject.
    /// Go reference: accounts.go Interest.
    /// </summary>
    public int Interest(string subject)
    {
        var (plainCount, queueCount) = SubList.NumInterest(subject);
        return plainCount + queueCount;
    }

    /// <summary>
    /// Returns the total number of outstanding response mappings for service exports.
    /// Go reference: accounts.go NumPendingAllResponses.
    /// </summary>
    public int NumPendingAllResponses() => NumPendingResponses(string.Empty);

    /// <summary>
    /// Returns the number of outstanding response mappings for service exports.
    /// When <paramref name="filter"/> is empty, counts all mappings.
    /// Go reference: accounts.go NumPendingResponses.
    /// </summary>
    public int NumPendingResponses(string filter)
    {
        if (string.IsNullOrEmpty(filter))
            return Exports.Responses.Count;

        var se = GetServiceExportEntry(filter);
        if (se == null)
            return 0;

        var count = 0;
        foreach (var (_, si) in Exports.Responses)
        {
            if (ReferenceEquals(si.Export, se))
                count++;
        }

        return count;
    }

    /// <summary>
    /// Returns the number of configured service import subjects.
    /// Go reference: accounts.go NumServiceImports.
    /// </summary>
    public int NumServiceImports() => Imports.Services.Count;

    /// <summary>
    /// Removes a response service import mapping.
    /// Go reference: accounts.go removeRespServiceImport.
    /// </summary>
    public void RemoveRespServiceImport(ServiceImport? serviceImport, ResponseServiceImportRemovalReason reason = ResponseServiceImportRemovalReason.Ok)
    {
        if (serviceImport == null)
            return;

        string? replyPrefix = null;
        foreach (var (prefix, si) in Exports.Responses)
        {
            if (ReferenceEquals(si, serviceImport))
            {
                replyPrefix = prefix;
                break;
            }
        }

        if (replyPrefix == null)
            return;

        // Current parity scope removes the response mapping. Reason-specific
        // metrics/latency side effects are tracked separately.
        ResponseRouter.CleanupResponse(this, replyPrefix, serviceImport);
        _ = reason;
    }

    private ServiceExport? GetServiceExportEntry(string subject)
    {
        if (Exports.Services.TryGetValue(subject, out var exact))
            return exact;

        foreach (var (pattern, export) in Exports.Services)
        {
            if (SubjectMatch.MatchLiteral(subject, pattern))
                return export;
        }

        return null;
    }

    /// <summary>
    /// Returns all service import subjects registered on this account that are currently
    /// shadowed by a local subscription in the SubList.
    /// Go reference: accounts.go serviceImportShadowed (~line 2015).
    /// </summary>
    public IReadOnlyList<string> GetShadowedServiceImports()
    {
        var shadowed = new List<string>();
        foreach (var subject in Imports.Services.Keys)
        {
            if (ServiceImportShadowed(subject))
                shadowed.Add(subject);
        }
        return shadowed;
    }

    /// <summary>
    /// Returns <see langword="true"/> when at least one registered service import subject
    /// is shadowed by a local subscription.
    /// Go reference: accounts.go serviceImportShadowed (~line 2015).
    /// </summary>
    public bool HasShadowedImports
    {
        get
        {
            foreach (var subject in Imports.Services.Keys)
            {
                if (ServiceImportShadowed(subject))
                    return true;
            }
            return false;
        }
    }

    /// <summary>
    /// Returns a detailed <see cref="ShadowCheckResult"/> for the given import subject,
    /// including the list of local subscription subjects that shadow it.
    /// Go reference: accounts.go serviceImportShadowed (~line 2015).
    /// </summary>
    public ShadowCheckResult CheckServiceImportShadowing(string importSubject)
    {
        var matchResult = SubList.Match(importSubject);
        var shadowingSubs = new List<string>();

        foreach (var sub in matchResult.PlainSubs)
            shadowingSubs.Add(sub.Subject);

        foreach (var queueGroup in matchResult.QueueSubs)
            foreach (var sub in queueGroup)
                shadowingSubs.Add(sub.Subject);

        bool isShadowed = shadowingSubs.Count > 0;
        return new ShadowCheckResult(isShadowed, importSubject, shadowingSubs);
    }

    public void Dispose() => SubList.Dispose();
}

/// <summary>
/// Result of <see cref="Account.CheckServiceImportShadowing"/> describing whether a service import
/// subject is intercepted by a local subscription and which subscriptions are responsible.
/// Go reference: accounts.go serviceImportShadowed (~line 2015).
/// </summary>
/// <param name="IsShadowed">Whether any local subscription shadows the import subject.</param>
/// <param name="ImportSubject">The service import subject that was checked.</param>
/// <param name="ShadowingSubscriptions">The subjects of local subscriptions that match the import subject.</param>
public sealed record ShadowCheckResult(
    bool IsShadowed,
    string ImportSubject,
    IReadOnlyList<string> ShadowingSubscriptions);

/// <summary>
/// Carries the result of a <see cref="Account.CheckServiceResponse"/> call.
/// </summary>
/// <param name="Found">Whether a threshold was registered for the subject.</param>
/// <param name="IsOverdue">Whether <paramref name="Elapsed"/> exceeds the threshold.</param>
/// <param name="Threshold">The registered threshold, or <see langword="null"/> when not found.</param>
/// <param name="Elapsed">The measured elapsed time that was checked.</param>
public sealed record ServiceResponseThresholdResult(
    bool Found,
    bool IsOverdue,
    TimeSpan? Threshold,
    TimeSpan Elapsed);

/// <summary>
/// Snapshot of an account's expiration state returned by <see cref="Account.GetExpirationInfo"/>.
/// Go reference: accounts.go — account expiry fields.
/// </summary>
/// <param name="AccountName">The name of the account.</param>
/// <param name="HasExpiration">Whether an expiration time has been set.</param>
/// <param name="ExpiresAt">The UTC expiry time, or <see langword="null"/> if none is set.</param>
/// <param name="IsExpired">Whether the account is already past its expiry time.</param>
/// <param name="TimeToExpiry">
///   Time remaining until expiry; <see cref="TimeSpan.Zero"/> when already expired;
///   <see langword="null"/> when no expiry is configured.
/// </param>
public sealed record AccountExpirationInfo(
    string AccountName,
    bool HasExpiration,
    DateTime? ExpiresAt,
    bool IsExpired,
    TimeSpan? TimeToExpiry);

/// <summary>
/// Summary snapshot of an account's revocation state returned by <see cref="Account.GetRevocationInfo"/>.
/// Go reference: accounts.go — usersRevoked map and Revocations[jwt.All] global key (~line 2934).
/// </summary>
/// <param name="RevokedCount">Total number of revocation entries, including any global entry.</param>
/// <param name="HasGlobalRevocation">Whether the special "*" global revocation key is present.</param>
/// <param name="RevokedNKeys">Snapshot list of all revoked NKey strings.</param>
public sealed record RevocationInfo(
    int RevokedCount,
    bool HasGlobalRevocation,
    IReadOnlyList<string> RevokedNKeys);

/// <summary>
/// Represents a JWT activation claim for a service or stream import.
/// Go reference: server/accounts.go — jwt.ActivationClaims decoded inside checkActivation (~line 2955).
/// </summary>
public sealed class ActivationClaim
{
    public required string Subject { get; init; }
    public required DateTime IssuedAt { get; init; }
    public required DateTime ExpiresAt { get; init; }
    public string? Issuer { get; init; }

    /// <summary>
    /// Returns <see langword="true"/> when <see cref="DateTime.UtcNow"/> is at or past <see cref="ExpiresAt"/>.
    /// Go reference: accounts.go — act.Expires &lt;= tn check.
    /// </summary>
    public bool IsExpired => DateTime.UtcNow >= ExpiresAt;

    /// <summary>
    /// Time remaining until expiry, or <see cref="TimeSpan.Zero"/> if already expired.
    /// Go reference: accounts.go — expiresAt duration computed from act.Expires - time.Now().Unix().
    /// </summary>
    public TimeSpan TimeToExpiry => IsExpired ? TimeSpan.Zero : ExpiresAt - DateTime.UtcNow;
}

/// <summary>
/// Result of <see cref="Account.CheckActivationExpiry"/> for a given subject.
/// Go reference: server/accounts.go — checkActivation return value (~line 2943).
/// </summary>
/// <param name="Found">Whether an activation claim was registered for the subject.</param>
/// <param name="IsExpired">Whether the claim has passed its expiry time.</param>
/// <param name="ExpiresAt">The UTC expiry time of the claim, or <see langword="null"/> when not found.</param>
/// <param name="TimeToExpiry">
///   Time remaining until expiry; <see cref="TimeSpan.Zero"/> when already expired;
///   <see langword="null"/> when the subject has no registered claim.
/// </param>
public sealed record ActivationCheckResult(
    bool Found,
    bool IsExpired,
    DateTime? ExpiresAt,
    TimeSpan? TimeToExpiry);

/// <summary>
/// Reason for removing a response service import.
/// Go reference: accounts.go rsiReason enum.
/// </summary>
public enum ResponseServiceImportRemovalReason
{
    Ok = 0,
    NoDelivery = 1,
    Timeout = 2,
}

/// <summary>
/// Snapshot of account JWT claim fields used for hot-reload diff detection.
/// Go reference: server/accounts.go — AccountClaims / jwt.AccountClaims fields applied in updateAccountClaimsWithRefresh (~line 3374).
/// </summary>
public sealed class AccountClaimData
{
    /// <summary>Maximum number of connections (null = not specified by claim).</summary>
    public int? MaxConnections { get; init; }

    /// <summary>Maximum number of subscriptions (null = not specified by claim).</summary>
    public int? MaxSubscriptions { get; init; }

    /// <summary>The account's public NKey, if present in the claim.</summary>
    public string? Nkey { get; init; }

    /// <summary>The operator or signing key that issued the account JWT.</summary>
    public string? Issuer { get; init; }

    /// <summary>The UTC expiry time encoded in the claim, if present.</summary>
    public DateTime? ExpiresAt { get; init; }

    /// <summary>Arbitrary tags associated with the account in the JWT.</summary>
    public Dictionary<string, string> Tags { get; init; } = new();
}

/// <summary>
/// Result of <see cref="Account.UpdateAccountClaims"/> describing which fields changed.
/// Go reference: server/accounts.go — updateAccountClaimsWithRefresh output (~line 3374).
/// </summary>
/// <param name="Changed">Whether any field differed from the previous claim state.</param>
/// <param name="ChangedFields">Names of the properties that changed.</param>
/// <param name="UpdateCount">The new total claim update count after this call.</param>
public sealed record AccountClaimUpdateResult(
    bool Changed,
    IReadOnlyList<string> ChangedFields,
    int UpdateCount);

/// <summary>
/// A single entry in an account's reverse response map.
/// Maps a rewritten reply subject back to the original requester's account and subject
/// so that service responses can be routed across account boundaries.
/// Go reference: server/accounts.go — respMapEntry struct used by addRespMapEntry / checkForReverseEntries.
/// </summary>
/// <param name="ReplySubject">The rewritten reply subject used by the service provider.</param>
/// <param name="OriginAccount">The name of the account that originated the request.</param>
/// <param name="OriginalReply">The reply subject the originating client is listening on.</param>
/// <param name="CreatedAt">The UTC time at which this mapping was recorded.</param>
public sealed record ReverseResponseMapEntry(
    string ReplySubject,
    string OriginAccount,
    string OriginalReply,
    DateTime CreatedAt);
