namespace ZB.MOM.NatsNet.Server;

internal sealed partial class JsAccount
{
    internal (ulong Mem, ulong Store) ReservedStorage(string tier)
    {
        ulong mem = 0;
        ulong store = 0;

        Lock.EnterReadLock();
        try
        {
            foreach (var stream in Streams.Values.OfType<NatsStream>())
            {
                var cfg = stream.Config;
                if (!string.IsNullOrEmpty(tier) && !string.Equals(tier, JetStreamEngine.TierName(cfg.Replicas), StringComparison.Ordinal))
                    continue;
                if (cfg.MaxBytes <= 0)
                    continue;

                if (cfg.Storage == StorageType.FileStorage)
                    store += (ulong)cfg.MaxBytes;
                else if (cfg.Storage == StorageType.MemoryStorage)
                    mem += (ulong)cfg.MaxBytes;
            }
        }
        finally
        {
            Lock.ExitReadLock();
        }

        return (mem, store);
    }

    internal void RemoteUpdateUsage(byte[] message)
    {
        if (message.Length < 16)
            return;

        UsageLock.EnterWriteLock();
        try
        {
            if (!Usage.TryGetValue(string.Empty, out var usage))
            {
                usage = new JsaStorage();
                Usage[string.Empty] = usage;
            }

            usage.Total.Mem = BitConverter.ToInt64(message, 0);
            usage.Total.Store = BitConverter.ToInt64(message, 8);
        }
        finally
        {
            UsageLock.ExitWriteLock();
        }
    }

    internal void CheckAndSyncUsage(string tier, StorageType storageType)
    {
        if (Interlocked.CompareExchange(ref Sync, 1, 0) != 0)
            return;

        try
        {
            Lock.EnterReadLock();
            try
            {
                long total = 0;
                foreach (var stream in Streams.Values.OfType<NatsStream>())
                {
                    if (!string.Equals(JetStreamEngine.TierName(stream.Config.Replicas), tier, StringComparison.Ordinal))
                        continue;
                    if (stream.Config.Storage != storageType)
                        continue;
                    total += (long)stream.State().Bytes;
                }

                UsageLock.EnterWriteLock();
                try
                {
                    Usage.TryGetValue(tier, out var usage);
                    usage ??= new JsaStorage();
                    Usage[tier] = usage;

                    if (storageType == StorageType.MemoryStorage)
                    {
                        usage.Local.Mem = total;
                        usage.Total.Mem = total;
                    }
                    else
                    {
                        usage.Local.Store = total;
                        usage.Total.Store = total;
                    }
                }
                finally
                {
                    UsageLock.ExitWriteLock();
                }
            }
            finally
            {
                Lock.ExitReadLock();
            }
        }
        finally
        {
            Interlocked.Exchange(ref Sync, 0);
        }
    }

    internal void UpdateUsage(string tier, StorageType storageType, long delta)
    {
        UsageLock.EnterWriteLock();
        try
        {
            Usage.TryGetValue(tier, out var usage);
            usage ??= new JsaStorage();
            Usage[tier] = usage;

            if (storageType == StorageType.MemoryStorage)
            {
                usage.Local.Mem += delta;
                usage.Total.Mem += delta;
            }
            else
            {
                usage.Local.Store += delta;
                usage.Total.Store += delta;
            }
        }
        finally
        {
            UsageLock.ExitWriteLock();
        }
    }

    internal void SendClusterUsageUpdateTimer()
    {
        UsageLock.EnterWriteLock();
        try
        {
            SendClusterUsageUpdate();
        }
        finally
        {
            UsageLock.ExitWriteLock();
        }
    }

    internal void SendClusterUsageUpdate()
    {
        var now = DateTime.UtcNow;
        if (!JetStreamEngine.ShouldSendUsageUpdate(LUpdate))
            return;
        LUpdate = now;

        // Cluster bus publish is wired in later cluster sessions.
        UsageApi = ApiTotal;
        UsageErr = ApiErrors;
    }

    internal (JetStream? JetStream, bool Clustered) JetStreamAndClustered()
    {
        Lock.EnterReadLock();
        try
        {
            var js = Js as JetStream;
            return (js, js?.Cluster != null);
        }
        finally
        {
            Lock.ExitReadLock();
        }
    }

    internal bool StreamAssigned(string stream)
    {
        Lock.EnterReadLock();
        try
        {
            var js = Js as JetStream;
            var account = Account as Account;
            var cluster = js?.Cluster as JetStreamCluster;
            if (js == null || account == null || cluster == null)
                return false;

            js.Lock.EnterReadLock();
            try
            {
                return cluster.IsStreamAssigned(account, stream);
            }
            finally
            {
                js.Lock.ExitReadLock();
            }
        }
        finally
        {
            Lock.ExitReadLock();
        }
    }

    internal bool ConsumerAssigned(string stream, string consumer)
    {
        Lock.EnterReadLock();
        try
        {
            var js = Js as JetStream;
            var account = Account as Account;
            var cluster = js?.Cluster as JetStreamCluster;
            if (js == null || account == null || cluster == null)
                return false;

            js.Lock.EnterReadLock();
            try
            {
                return cluster.IsConsumerAssigned(account, stream, consumer);
            }
            finally
            {
                js.Lock.ExitReadLock();
            }
        }
        finally
        {
            Lock.ExitReadLock();
        }
    }

    internal Account? Acc() => Account as Account;

    internal (JetStreamAccountLimits Limits, string Tier, bool Found) SelectLimits(int replicas)
    {
        UsageLock.EnterReadLock();
        try
        {
            if (Limits.TryGetValue(string.Empty, out var selected))
                return (selected, string.Empty, true);

            var tier = JetStreamEngine.TierName(replicas);
            if (Limits.TryGetValue(tier, out selected))
                return (selected, tier, true);

            return (new JetStreamAccountLimits(), string.Empty, false);
        }
        finally
        {
            UsageLock.ExitReadLock();
        }
    }

    internal int CountStreams(string tier, StreamConfig cfg)
    {
        Lock.EnterReadLock();
        try
        {
            var count = 0;
            foreach (var stream in Streams.Values.OfType<NatsStream>())
            {
                if ((!string.IsNullOrEmpty(tier) && !JetStreamEngine.IsSameTier(stream.Config, cfg)) ||
                    string.Equals(stream.Config.Name, cfg.Name, StringComparison.Ordinal))
                    continue;
                count++;
            }

            return count;
        }
        finally
        {
            Lock.ExitReadLock();
        }
    }

    internal long TieredReservation(string tier, StreamConfig cfg)
    {
        long reservation = 0;

        Lock.EnterReadLock();
        try
        {
            foreach (var stream in Streams.Values.OfType<NatsStream>())
            {
                var streamCfg = stream.Config;
                if (streamCfg.MaxBytes <= 0 || string.Equals(streamCfg.Name, cfg.Name, StringComparison.Ordinal))
                    continue;

                if (string.IsNullOrEmpty(tier))
                {
                    if (streamCfg.Storage == cfg.Storage)
                        reservation += Math.Max(1, streamCfg.Replicas) * streamCfg.MaxBytes;
                    continue;
                }

                if (streamCfg.Replicas == cfg.Replicas && JetStreamEngine.IsSameTier(streamCfg, cfg))
                    reservation += Math.Max(1, streamCfg.Replicas) * streamCfg.MaxBytes;
            }

            return reservation;
        }
        finally
        {
            Lock.ExitReadLock();
        }
    }

    internal (ulong Mem, ulong Store) StorageTotals()
    {
        UsageLock.EnterReadLock();
        try
        {
            ulong mem = 0;
            ulong store = 0;
            foreach (var usage in Usage.Values)
            {
                mem += (ulong)Math.Max(0, usage.Total.Mem);
                store += (ulong)Math.Max(0, usage.Total.Store);
            }

            return (mem, store);
        }
        finally
        {
            UsageLock.ExitReadLock();
        }
    }

    internal (bool Exceeded, JsApiError? Error) LimitsExceeded(StorageType storageType, string tier, int replicas) =>
        WouldExceedLimits(storageType, tier, replicas, string.Empty, null, null);

    internal (bool Exceeded, JsApiError? Error) WouldExceedLimits(
        StorageType storageType,
        string tier,
        int replicas,
        string subject,
        byte[]? headers,
        byte[]? payload)
    {
        UsageLock.EnterReadLock();
        try
        {
            if (!Limits.TryGetValue(tier, out var selected))
                return (true, JsApiErrors.NewJSNoLimitsError());
            if (!Usage.TryGetValue(tier, out var inUse))
                return (false, null);

            var r = Math.Max(1, replicas);
            var lr = string.IsNullOrEmpty(tier) ? 1 : r;
            var bytes = (subject.Length + (headers?.Length ?? 0) + (payload?.Length ?? 0)) * r;

            if (storageType == StorageType.MemoryStorage)
            {
                var totalMem = inUse.Total.Mem + bytes;
                if (selected.MemoryMaxStreamBytes > 0 && totalMem > selected.MemoryMaxStreamBytes * lr)
                    return (true, null);
                if (selected.MaxMemory >= 0 && totalMem > selected.MaxMemory * lr)
                    return (true, null);
            }
            else
            {
                var totalStore = inUse.Total.Store + bytes;
                if (selected.StoreMaxStreamBytes > 0 && totalStore > selected.StoreMaxStreamBytes * lr)
                    return (true, null);
                if (selected.MaxStore >= 0 && totalStore > selected.MaxStore * lr)
                    return (true, null);
            }

            return (false, null);
        }
        finally
        {
            UsageLock.ExitReadLock();
        }
    }

    internal void Delete()
    {
        Lock.EnterWriteLock();
        try
        {
            Streams.Clear();
            Inflight.Clear();
            UpdatesSub = null;
            UpdatesPub = string.Empty;
        }
        finally
        {
            Lock.ExitWriteLock();
        }
    }
}
