using AyCode.Core.Compression;
using AyCode.Core.Enums;
using AyCode.Core.Extensions;
using AyCode.Core.Helpers;
using AyCode.Core.Interfaces;
using AyCode.Core.Loggers;
using AyCode.Core.Serializers;
using AyCode.Core.Serializers.Binaries;
using AyCode.Services.SignalRs;
using System.Collections;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

namespace AyCode.Services.SignalRs
{
    //public class TrackingItemGuid<TDataItem>(TrackingState trackingState, TDataItem currentValue, TDataItem? originalValue = null) : TrackingItem<TDataItem, Guid>(trackingState, currentValue, originalValue) 
    //    where TDataItem : class, IId<Guid> { }
    
    //public class TrackingItemInt<TDataItem>(TrackingState trackingState, TDataItem currentValue, TDataItem? originalValue = null) : TrackingItem<TDataItem, int>(trackingState, currentValue, originalValue) 
    //    where TDataItem : class, IId<int> {}

    public class TrackingItem<TDataItem, TId>(TrackingState trackingState, TDataItem currentValue, TDataItem? originalValue = null) where TDataItem : class, IId<TId> where TId : struct
    {
        public TrackingState TrackingState { get; internal set; } = trackingState;
        public TDataItem CurrentValue { get; internal set; } = currentValue;
        public TDataItem? OriginalValue { get; init; } = originalValue;

        internal TrackingItem<TDataItem, TId> UpdateItem(TrackingState trackingState, TDataItem newValue) //where TTrackingItem : TrackingItem<TDataItem, TId>
        {
            CurrentValue = newValue;

            if (TrackingState != TrackingState.Add)
                TrackingState = trackingState;

            return (TrackingItem<TDataItem, TId>)this;
        }
    }

    //public class ChangeTrackingGuid<TDataItem> : ChangeTracking<TDataItem, Guid> /*: IEnumerable<TrackingItem<T>>*/ where TDataItem : class, IId<Guid>
    //{
    //    protected override bool HasIdValue(TDataItem dataItem) => !dataItem.Id.IsNullOrEmpty();

    //    protected override int FindIndex(TDataItem newValue) => TrackingItems.FindIndex(x => x.CurrentValue.Id == newValue.Id);

    //    public override bool TryGetTrackingItem(Guid id, [NotNullWhen(true)] out TrackingItem<TDataItem, Guid>? trackingItem)
    //    {
    //        trackingItem = TrackingItems.FirstOrDefault(x => x.CurrentValue.Id == id);
    //        return trackingItem != null;
    //    }
    //}

    //public class ChangeTrackingInt<TDataItem> : ChangeTracking<TDataItem, int> /*: IEnumerable<TrackingItem<T>>*/ where TDataItem : class, IId<int>
    //{
    //    protected override bool HasIdValue(TDataItem dataItem) => true;//dataItem.Id.IsNullOrEmpty();
        
    //    protected override int FindIndex(TDataItem newValue) => TrackingItems.FindIndex(x => x.CurrentValue.Id == newValue.Id);
        
    //    public override bool TryGetTrackingItem(int id, [NotNullWhen(true)] out TrackingItem<TDataItem, int>? trackingItem)
    //    {
    //        trackingItem = TrackingItems.FirstOrDefault(x => x.CurrentValue.Id == id);
    //        return trackingItem != null;
    //    }
    //}
    
    public class ChangeTracking<TDataItem, TId> /*: IEnumerable<TrackingItem<T>>*/where TDataItem : class, IId<TId> where TId : struct
    {
        private readonly EqualityComparer<TId> _equalityComparerId = EqualityComparer<TId>.Default;
        private readonly List<TrackingItem<TDataItem, TId>> _trackingItems = []; //TODO: Dictionary... - J.

        //protected abstract bool HasIdValue(TDataItem dataItem);
        //protected abstract int FindIndex(TDataItem newValue);
        //public abstract bool TryGetTrackingItem(TId id, [NotNullWhen(true)] out TrackingItem<TDataItem, TId>? trackingItem);

        private bool HasIdValue(TDataItem dataItem) => !_equalityComparerId.Equals(dataItem.Id, default);//dataItem.Id.IsNullOrEmpty();
        public int FindIndex(TDataItem newValue) => _trackingItems.FindIndex(x => _equalityComparerId.Equals(x.CurrentValue.Id, newValue.Id));
        public bool TryGetTrackingItem(TId id, [NotNullWhen(true)] out TrackingItem<TDataItem, TId>? trackingItem)
        {
            trackingItem = _trackingItems.FirstOrDefault(x => _equalityComparerId.Equals(x.CurrentValue.Id, id));
            return trackingItem != null;
        }

        internal TrackingItem<TDataItem, TId>? AddTrackingItem(TrackingState trackingState, TDataItem newValue, TDataItem? originalValue = null)
        {
            if (!HasIdValue(newValue)) throw new ArgumentNullException(nameof(newValue), $@"currentValue.Id.IsNullOrEmpty()");

            var itemIndex = FindIndex(newValue); //_trackingItems.FindIndex(x => x.CurrentValue.Id == newValue.Id);
            TrackingItem<TDataItem, TId>? trackingItem = null;

            if (itemIndex > -1)
            {
                trackingItem = _trackingItems[itemIndex];

                if (trackingState == TrackingState.Remove && trackingItem.TrackingState == TrackingState.Add)
                {
                    _trackingItems.RemoveAt(itemIndex);
                    return null;
                }

                return trackingItem.UpdateItem(trackingState, newValue);
            }

            if (originalValue != null && Equals(newValue, originalValue))
                originalValue = TrackingItemHelpers.JsonClone(originalValue);

            //trackingItem = new TrackingItem<TDataItem>(trackingState, newValue, originalValue);
            trackingItem = new TrackingItem<TDataItem, TId>(trackingState, newValue, originalValue);
            _trackingItems.Add(trackingItem);

            return trackingItem;
        }

        public int Count => _trackingItems.Count;
        internal void Clear() => _trackingItems.Clear();
        public List<TrackingItem<TDataItem, TId>> ToList() => _trackingItems.ToList();

        internal void Remove(TrackingItem<TDataItem, TId> trackingItem) => _trackingItems.Remove(trackingItem);

        //public IEnumerator<TrackingItem<T>> GetEnumerator()
        //{
        //    return _trackingItems.GetEnumerator();
        //}

        //IEnumerator IEnumerable.GetEnumerator()
        //{
        //    return GetEnumerator();
        //}
    }

    //[Serializable]
    //[DebuggerDisplay("Count = {Count}")]
    //public abstract class AcSignalRDataSourceGuid<TDataItem, TIList> : AcSignalRDataSource<TDataItem, Guid, TIList> 
    //    where TDataItem : class, IId<Guid> where TIList : class, IList<TDataItem>

    //{
    //    public AcSignalRDataSourceGuid(AcSignalRClientBase signalRClient, SignalRCrudTags signalRCrudTags, object[]? contextIds = null)
    //        : base(signalRClient, signalRCrudTags, contextIds)
    //    {
    //    }

    //    protected override bool HasIdValue(TDataItem dataItem) => !dataItem.Id.IsNullOrEmpty();
    //    protected override bool IdEquals(Guid id1, Guid id2) => id1 == id2;

    //    protected override int FindIndexInnerList(Guid id) => InnerList.FindIndex(x => x.Id == id);
    //    protected override TDataItem? FirstOrDefaultInnerList(Guid id) => InnerList.FirstOrDefault(x => x.Id == id);
    //}

    //[Serializable]
    //[DebuggerDisplay("Count = {Count}")]
    //public abstract class AcSignalRDataSourceInt<TDataItem, TIList> : AcSignalRDataSource<TDataItem, int, TIList> 
    //    where TDataItem : class, IId<int> where TIList : class, IList<TDataItem>
    //{
    //    public AcSignalRDataSourceInt(AcSignalRClientBase signalRClient, SignalRCrudTags signalRCrudTags, object[]? contextIds = null)
    //        : base(signalRClient, signalRCrudTags, contextIds)
    //    {
    //    }

    //    protected override bool HasIdValue(TDataItem dataItem) => true;
    //    protected override bool IdEquals(int id1, int id2) => id1 == id2;

    //    protected override int FindIndexInnerList(int id) => InnerList.FindIndex(x => x.Id == id);
    //    protected override TDataItem? FirstOrDefaultInnerList(int id) => InnerList.FirstOrDefault(x => x.Id == id);
    //}

    [Serializable]
    [DebuggerDisplay("Count = {Count}")]
    public abstract class AcSignalRDataSource<TDataItem, TId, TIList> : IList<TDataItem>, IList, IReadOnlyList<TDataItem>
        where TDataItem : class, IId<TId> where TId : struct 
        where TIList : class, IList<TDataItem>
    {
        private readonly object _syncRoot = new();
        private readonly SemaphoreSlim _asyncLock = new(1, 1);
        private readonly EqualityComparer<TId> _equalityComparerId = EqualityComparer<TId>.Default;

        protected TIList InnerList = Activator.CreateInstance<TIList>();
        protected readonly ChangeTracking<TDataItem, TId> TrackingItems = new();

        public object[]? ContextIds;
        /// <summary>
        /// Consumer-supplied base filter criteria (a DevExpress CriteriaOperator string) appended as the LAST
        /// positional GetAll parameter; the server parses + whitelists it. Renamed FilterText → CustomFilterText →
        /// CustomFilterCriteria (2026-07-16) as the semantics settled to a criteria string. Under the ADR-0004
        /// unified model it is AND-merged with the grid's automatic FilterCriteria.
        /// </summary>
        public string? CustomFilterCriteria { get; set; }

        public AcSignalRClientBase SignalRClient;
        protected readonly SignalRCrudTags SignalRCrudTags;

        /// <summary>
        /// Serializer type for RawBytes path. Matches server's SerializerOptions.SerializerType.
        /// Default: Binary. Override for JSON/GZip datasources.
        /// </summary>
        public AcSerializerType SerializerType { get; set; } = AcSerializerType.Binary;

        public Func<ItemChangedEventArgs<TDataItem>, Task>? OnDataSourceItemChanged;
        public Func<Task>? OnDataSourceLoaded;
        
        /// <summary>
        /// Event fired when synchronization state changes (true = syncing started, false = syncing ended)
        /// </summary>
        public event Action<bool>? OnSyncingStateChanged;
        
        private int _activeSyncOperations;
        
        /// <summary>
        /// Indicates whether any synchronization operation is in progress
        /// </summary>
        public bool IsSyncing => _activeSyncOperations > 0;
        
        private void BeginSync()
        {
            var wasZero = Interlocked.Increment(ref _activeSyncOperations) == 1;
            if (wasZero) OnSyncingStateChanged?.Invoke(true);
        }
        
        private void EndSync()
        {
            var isZero = Interlocked.Decrement(ref _activeSyncOperations) == 0;
            if (isZero) OnSyncingStateChanged?.Invoke(false);
        }

        protected bool HasIdValue(TDataItem dataItem) => !_equalityComparerId.Equals(dataItem.Id, default);
        protected bool IdEquals(TId id1, TId id2) => _equalityComparerId.Equals(id1, id2);
        protected int FindIndexInnerListUnsafe(TId id) => InnerList.FindIndex(x => IdEquals(x.Id, id));
        protected TDataItem? FirstOrDefaultInnerListUnsafe(TId id) => InnerList.FirstOrDefault(x => IdEquals(x.Id, id));

        private IAcLoggerBase? _logger;

        public AcSignalRDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags signalRCrudTags, IAcLoggerBase? logger, object[]? contextIds = null)
        {
            _logger = logger;

            ContextIds = contextIds;
            SignalRCrudTags = signalRCrudTags;
            SignalRClient = signalRClient;
        }

        public bool IsSynchronized => true;
        public object SyncRoot => _syncRoot;
        public bool IsFixedSize => false;

        public bool HasWorkingReferenceList { get; private set; }

        public void SetWorkingReferenceList(TIList? workingIList)
        {
            if (workingIList == null) return;

            lock (_syncRoot)
            {
                HasWorkingReferenceList = true;
                if (ReferenceEquals(InnerList, workingIList)) return;

                if (workingIList.Count == 0) AddRangeUnsafe(InnerList, workingIList);

                ClearUnsafe(true);
                InnerList = workingIList;
            }
        }

        public TIList GetReferenceInnerList() => InnerList;

        private object[]? GetContextParams()
        {
            var parameters = new List<object>();
            if (ContextIds != null) parameters.AddRange(ContextIds);
            if (CustomFilterCriteria != null) parameters.Add(CustomFilterCriteria);

            if (parameters.Count == 0) parameters = null;
            return parameters?.ToArray();
        }

        #region Load Methods

        /// <summary>
        /// GetAllMessageTag - Synchronous wait version
        /// </summary>
        public async Task LoadDataSource(bool clearChangeTracking = true)
        {
            if (SignalRCrudTags.GetAllMessageTag == AcSignalRTags.None)
                throw new ArgumentException($"SignalRCrudTags.GetAllMessageTag == SignalRTags.None");

            BeginSync();
            try
            {
                // RawBytes: T=byte[] → server pre-serializes, protocol passes through.
                // Single serialize (server) → single deserialize (PopulateMerge below).
                var rawBytes = await SignalRClient.GetAllAsync<byte[]>(SignalRCrudTags.GetAllMessageTag, GetContextParams())
                    ?? throw new NullReferenceException("LoadDataSource; null response");

                await LoadDataSourceFromResponseData(rawBytes, SerializerType, false, false, clearChangeTracking);
            }
            finally
            {
                EndSync();
            }
        }

        /// <summary>
        /// GetAllMessageTag - Async callback version.
        /// RawBytes path: server pre-serializes → byte[] flows through protocol → PopulateMerge.
        /// </summary>
        public Task LoadDataSourceAsync(bool clearChangeTracking = true)
        {
            if (SignalRCrudTags.GetAllMessageTag == AcSignalRTags.None)
                throw new ArgumentException($"SignalRCrudTags.GetAllMessageTag == SignalRTags.None");

            BeginSync();
            return SignalRClient.GetAllAsync<byte[]>(SignalRCrudTags.GetAllMessageTag, GetContextParams())
                .ContinueWith(async responseTask =>
                {
                    try
                    {
                        var rawBytes = await responseTask ?? throw new NullReferenceException("LoadDataSourceAsync; null response");

                        await LoadDataSourceFromResponseData(rawBytes, SerializerType, false, false, clearChangeTracking);
                    }
                    finally
                    {
                        EndSync();
                    }
                }).Unwrap();
        }

        /// <summary>
        /// Wraps a deserialize/populate call so a fault is logged with the concrete <typeparamref name="TDataItem"/>
        /// type before it propagates. Placed directly around the deserialize call (inside any BeginUpdate/EndUpdate
        /// scope, before EndUpdate runs) so the genuine root exception is captured even if EndUpdate's NotifyReset
        /// throws a secondary exception that would otherwise overwrite it. Rethrows — callers' behaviour is unchanged.
        /// </summary>
        private void TryDeserialize(Action deserialize)
        {
            try
            {
                deserialize();
            }
            catch (Exception ex)
            {
                _logger?.Error($"Deserialize failed for {typeof(TDataItem).Name}", ex);
                throw;
            }
        }

        /// <inheritdoc cref="TryDeserialize(Action)"/>
        private T TryDeserialize<T>(Func<T> deserialize)
        {
            try
            {
                return deserialize();
            }
            catch (Exception ex)
            {
                _logger?.Error($"Deserialize failed for {typeof(TDataItem).Name}", ex);
                throw;
            }
        }

        /// <summary>
        /// Loads data source from response data.
        /// responseData is either a typed object (protocol deserialized) or byte[] (raw path).
        /// </summary>
        public async Task LoadDataSourceFromResponseData(object responseData, AcSerializerType serializerType,
            bool refreshDataFromDbAsync = false, bool setSourceToWorkingReferenceList = false, bool clearChangeTracking = true)
        {
            await _asyncLock.WaitAsync();
            try
            {
                if (responseData is byte[] rawBytes)
                {
                    // Raw byte[] path — populate from binary bytes
                    if (!setSourceToWorkingReferenceList)
                    {
                        if (serializerType == AcSerializerType.Binary)
                        {
                            if (InnerList is IAcObservableCollection observable)
                            {
                                observable.BeginUpdate();
                                try
                                {
                                    TryDeserialize(() => AcBinaryDeserializer.PopulateMerge(rawBytes, 0, rawBytes.Length, InnerList));
                                }
                                finally
                                {
                                    observable.EndUpdate();
                                }
                            }
                            else
                            {
                                TryDeserialize(() => AcBinaryDeserializer.Populate(rawBytes, 0, rawBytes.Length, InnerList));
                            }
                        }
                        else
                        {
                            var json = GzipHelper.DecompressToString(rawBytes);
                            if (InnerList is IAcObservableCollection observable)
                            {
                                observable.PopulateFromJson(json);
                            }
                            else
                            {
                                json.JsonTo(InnerList);
                            }
                        }
                    }
                    else
                    {
                        TIList? fromSource;
                        if (serializerType == AcSerializerType.Binary) fromSource = TryDeserialize(() => AcBinaryDeserializer.Deserialize<TIList>(rawBytes, 0, rawBytes.Length));
                        else fromSource = GzipHelper.DecompressToString(rawBytes).JsonTo<TIList>();

                        if (fromSource != null)
                        {
                            ClearUnsafe(clearChangeTracking);
                            SetWorkingReferenceListUnsafe(fromSource);
                        }
                    }
                }
                else if (responseData is TIList typedList)
                {
                    // Typed object path — protocol already deserialized to target type
                    if (!setSourceToWorkingReferenceList)
                    {
                        if (InnerList is IAcObservableCollection observable)
                        {
                            observable.BeginUpdate();
                            try
                            {
                                InnerList.Clear();
                                foreach (var item in typedList) InnerList.Add(item);
                            }
                            finally
                            {
                                observable.EndUpdate();
                            }
                        }
                        else
                        {
                            InnerList.Clear();
                            foreach (var item in typedList) InnerList.Add(item);
                        }
                    }
                    else
                    {
                        ClearUnsafe(clearChangeTracking);
                        SetWorkingReferenceListUnsafe(typedList);
                    }
                }
                else
                {
                    // Fallback: incompatible collection type (e.g., List<T> in test scenarios without protocol).
                    // Re-serialize to byte[] then process inline. Pass explicit runtime type — the
                    // generic ToBinary<T>() overload would infer T = object here.
                    var reBytes = responseData.ToBinary(responseData.GetType());
                    if (!setSourceToWorkingReferenceList)
                    {
                        if (InnerList is IAcObservableCollection observable2)
                        {
                            observable2.BeginUpdate();
                            try
                            {
                                TryDeserialize(() => AcBinaryDeserializer.PopulateMerge(reBytes, 0, reBytes.Length, InnerList));
                            }
                            finally
                            {
                                observable2.EndUpdate();
                            }
                        }
                        else
                        {
                            TryDeserialize(() => AcBinaryDeserializer.Populate(reBytes, 0, reBytes.Length, InnerList));
                        }
                    }
                    else
                    {
                        var fromSource = TryDeserialize(() => AcBinaryDeserializer.Deserialize<TIList>(reBytes, 0, reBytes.Length));
                        if (fromSource != null)
                        {
                            ClearUnsafe(clearChangeTracking);
                            SetWorkingReferenceListUnsafe(fromSource);
                        }
                    }
                }

                if (clearChangeTracking) TrackingItems.Clear();

                // Tag nélküli (lokális/readonly) datasource: nincs honnan frissíteni — a szerver-refresh kimarad.
                // (Az explicit LoadDataSourceAsync hívás tag nélkül továbbra is dob — az valódi konfigurációs hiba.)
                if (refreshDataFromDbAsync && SignalRCrudTags.GetAllMessageTag != AcSignalRTags.None)
                {
                    LoadDataSourceAsync(false).Forget();
                    return;
                }
            }
            finally
            {
                _asyncLock.Release();
            }

            if (OnDataSourceLoaded != null) await OnDataSourceLoaded.Invoke();
        }

        public async Task LoadDataSource(TIList fromSource, bool refreshDataFromDbAsync = false, bool setSourceToWorkingReferenceList = false, bool clearChangeTracking = true)
        {
            await _asyncLock.WaitAsync();
            try
            {
                if (!ReferenceEquals(InnerList, fromSource))
                {
                    if (!setSourceToWorkingReferenceList)
                    {
                        // CopyTo uses JSON serialization which already handles BeginUpdate/EndUpdate
                        // for IAcObservableCollection via AcJsonDeserializer.Populate
                        fromSource.CopyTo(InnerList);
                    }
                    else
                    {
                        ClearUnsafe(clearChangeTracking);
                        SetWorkingReferenceListUnsafe(fromSource);
                    }
                }
                else if (clearChangeTracking) TrackingItems.Clear();

                // Tag nélküli (lokális/readonly) datasource: nincs honnan frissíteni — a szerver-refresh kimarad.
                // (Az explicit LoadDataSourceAsync hívás tag nélkül továbbra is dob — az valódi konfigurációs hiba.)
                if (refreshDataFromDbAsync && SignalRCrudTags.GetAllMessageTag != AcSignalRTags.None)
                {
                    LoadDataSourceAsync(false).Forget();
                    return;
                }
            }
            finally
            {
                _asyncLock.Release();
            }

            if (OnDataSourceLoaded != null) await OnDataSourceLoaded.Invoke();
        }

        public async Task<TDataItem?> LoadItem(TId id)
        {
            if (SignalRCrudTags.GetItemMessageTag == AcSignalRTags.None) 
                throw new ArgumentException($"SignalRCrudTags.GetItemMessageTag == SignalRTags.None");

            var resultitem = await SignalRClient.GetByIdAsync<TDataItem>(SignalRCrudTags.GetItemMessageTag, id);
            if (resultitem == null) return null;

            await _asyncLock.WaitAsync();
            try
            {
                var index = FindIndexInnerListUnsafe(id);
                if (index >= 0) resultitem.CopyTo(InnerList[index]);
                else InnerList.Add(resultitem);
            }
            finally
            {
                _asyncLock.Release();
            }

            var eventArgs = new ItemChangedEventArgs<TDataItem>(resultitem, TrackingState.Get);
            if (OnDataSourceItemChanged != null) await OnDataSourceItemChanged.Invoke(eventArgs);

            return resultitem;
        }

        #endregion

        #region Indexer

        public TDataItem this[int index]
        {
            get
            {
                lock (_syncRoot)
                {
                    if ((uint)index >= (uint)InnerList.Count) 
                        throw new ArgumentOutOfRangeException(nameof(index));
                    return InnerList[index];
                }
            }
            set
            {
                lock (_syncRoot)
                {
                    UpdateUnsafe(index, value);
                }
            }
        }

        #endregion

        #region Add Methods

        public void Add(TDataItem newValue)
        {
            if (!HasIdValue(newValue)) 
                throw new ArgumentNullException(nameof(newValue), @"Add->HasIdValue(newValue) == false");

            lock (_syncRoot)
            {
                if (ContainsUnsafe(newValue))
                    throw new ArgumentException($@"It already contains this Id! {newValue}", nameof(newValue));

                AddUnsafe(newValue);
            }
        }

        public async Task<TDataItem> Add(TDataItem newValue, bool autoSave)
        {
            if (!HasIdValue(newValue)) 
                throw new ArgumentNullException(nameof(newValue), @"Add->HasIdValue(newValue) == false");

            await _asyncLock.WaitAsync();
            try
            {
                if (ContainsUnsafe(newValue))
                    throw new ArgumentException($@"It already contains this Id! {newValue}", nameof(newValue));

                AddUnsafe(newValue);
            }
            finally
            {
                _asyncLock.Release();
            }

            return autoSave ? await SaveItem(newValue, TrackingState.Add) : newValue;
        }

        public async Task<TDataItem> AddOrUpdate(TDataItem newValue, bool autoSave)
        {
            if (!HasIdValue(newValue)) 
                throw new ArgumentNullException(nameof(newValue), @"AddOrUpdate->newValue.Id.IsNullOrEmpty()");

            int index;
            await _asyncLock.WaitAsync();
            try
            {
                index = FindIndexInnerListUnsafe(newValue.Id);
                if (index > -1)
                {
                    UpdateUnsafe(index, newValue);
                }
                else
                {
                    AddUnsafe(newValue);
                }
            }
            finally
            {
                _asyncLock.Release();
            }

            return autoSave 
                ? await SaveItem(newValue, index > -1 ? TrackingState.Update : TrackingState.Add) 
                : newValue;
        }

        private void AddUnsafe(TDataItem newValue)
        {
            TrackingItems.AddTrackingItem(TrackingState.Add, newValue);
            InnerList.Add(newValue);
        }

        public void AddRange(IEnumerable<TDataItem> source)
        {
            lock (_syncRoot)
            {
                AddRangeUnsafe(source, InnerList);
            }
        }

        private void AddRangeUnsafe(IEnumerable<TDataItem> source, TIList destination)
        {
            switch (destination)
            {
                case IAcObservableCollection dest:
                    dest.AddRange(source);
                    break;
                case List<TDataItem> dest:
                    dest.AddRange(source);
                    break;
                default:
                    foreach (var dataItem in source)
                        destination.Add(dataItem);
                    break;
            }
        }

        #endregion

        #region Insert Methods

        public void Insert(int index, TDataItem newValue)
        {
            if (!HasIdValue(newValue)) 
                throw new ArgumentNullException(nameof(newValue), @"Insert->newValue.Id.IsNullOrEmpty()");

            lock (_syncRoot)
            {
                if (ContainsUnsafe(newValue))
                    throw new ArgumentException($@"Insert; It already contains this Id! {newValue}", nameof(newValue));

                TrackingItems.AddTrackingItem(TrackingState.Add, newValue);
                InnerList.Insert(index, newValue);
            }
        }

        public async Task<TDataItem> Insert(int index, TDataItem newValue, bool autoSave)
        {
            if (!HasIdValue(newValue)) 
                throw new ArgumentNullException(nameof(newValue), @"Insert->newValue.Id.IsNullOrEmpty()");

            await _asyncLock.WaitAsync();
            try
            {
                if (ContainsUnsafe(newValue))
                    throw new ArgumentException($@"Insert; It already contains this Id! {newValue}", nameof(newValue));

                TrackingItems.AddTrackingItem(TrackingState.Add, newValue);
                InnerList.Insert(index, newValue);
            }
            finally
            {
                _asyncLock.Release();
            }

            return autoSave ? await SaveItem(newValue, TrackingState.Add) : newValue;
        }

        #endregion

        #region Update Methods

        public Task<TDataItem> Update(TDataItem newItem, bool autoSave)
        {
            int index;
            lock (_syncRoot)
            {
                index = FindIndexInnerListUnsafe(newItem.Id);
            }
            return Update(index, newItem, autoSave);
        }

        public async Task<TDataItem> Update(int index, TDataItem newValue, bool autoSave)
        {
            await _asyncLock.WaitAsync();
            try
            {
                UpdateUnsafe(index, newValue);
            }
            finally
            {
                _asyncLock.Release();
            }

            return autoSave ? await SaveItem(newValue, TrackingState.Update) : newValue;
        }

        private void UpdateUnsafe(int index, TDataItem newValue)
        {
            if (default(TDataItem) != null && newValue == null) 
                throw new NullReferenceException(nameof(newValue));
            if (!HasIdValue(newValue)) 
                throw new ArgumentNullException(nameof(newValue), @"UpdateUnsafe->newValue.Id.IsNullOrEmpty()");
            if ((uint)index >= (uint)InnerList.Count) 
                throw new ArgumentOutOfRangeException(nameof(index));

            var currentItem = InnerList[index];

            if (!IdEquals(currentItem.Id, newValue.Id))
                throw new ArgumentException($@"UpdateUnsafe; currentItem.Id != item.Id! {newValue}", nameof(newValue));

            TrackingItems.AddTrackingItem(TrackingState.Update, newValue, currentItem);
            InnerList[index] = newValue;
        }

        #endregion

        #region Remove Methods

        public bool Remove(TDataItem item)
        {
            lock (_syncRoot)
            {
                var index = FindIndexInnerListUnsafe(item.Id);
                if (index < 0) return false;

                RemoveAtUnsafe(index);
                return true;
            }
        }

        public async Task<bool> Remove(TId id, bool autoSave)
        {
            TDataItem? item;
            await _asyncLock.WaitAsync();
            try
            {
                item = FirstOrDefaultInnerListUnsafe(id);
                if (item == null) return true;

                var index = FindIndexInnerListUnsafe(id);
                if (index < 0) return false;

                RemoveAtUnsafe(index);
            }
            finally
            {
                _asyncLock.Release();
            }

            if (autoSave)
            {
                await SaveItem(item, TrackingState.Remove);
            }
            return true;
        }

        public async Task<bool> Remove(TDataItem item, bool autoSave)
        {
            bool result;
            await _asyncLock.WaitAsync();
            try
            {
                var index = FindIndexInnerListUnsafe(item.Id);
                if (index < 0) 
                {
                    result = false;
                }
                else
                {
                    RemoveAtUnsafe(index);
                    result = true;
                }
            }
            finally
            {
                _asyncLock.Release();
            }

            if (autoSave && result)
            {
                await SaveItem(item, TrackingState.Remove);
            }
            return result;
        }

        public bool TryRemove(TId id, out TDataItem? item)
        {
            lock (_syncRoot)
            {
                item = FirstOrDefaultInnerListUnsafe(id);
                if (item == null) return false;

                var index = FindIndexInnerListUnsafe(id);
                if (index < 0) return false;

                RemoveAtUnsafe(index);
                return true;
            }
        }

        public void RemoveAt(int index)
        {
            lock (_syncRoot)
            {
                RemoveAtUnsafe(index);
            }
        }

        private void RemoveAtUnsafe(int index)
        {
            var currentItem = InnerList[index];
            if (!HasIdValue(currentItem)) 
                throw new ArgumentNullException(nameof(currentItem), $@"RemoveAt->item.Id.IsNullOrEmpty(); index: {index}");

            TrackingItems.AddTrackingItem(TrackingState.Remove, currentItem, currentItem);
            InnerList.RemoveAt(index);
        }

        public async Task RemoveAt(int index, bool autoSave)
        {
            TDataItem currentItem;
            await _asyncLock.WaitAsync();
            try
            {
                currentItem = InnerList[index];
                RemoveAtUnsafe(index);
            }
            finally
            {
                _asyncLock.Release();
            }

            if (autoSave)
            {
                await SaveItem(currentItem, TrackingState.Remove);
            }
        }

        #endregion

        #region Tracking Methods

        public List<TrackingItem<TDataItem, TId>> GetTrackingItems()
        {
            lock (_syncRoot)
            {
                return TrackingItems.ToList();
            }
        }

        public void SetTrackingStateToUpdate(TDataItem item)
        {
            lock (_syncRoot)
            {
                if (TrackingItems.TryGetTrackingItem(item.Id, out var trackingItem))
                {
                    if (trackingItem.TrackingState != TrackingState.Add)
                        trackingItem.TrackingState = TrackingState.Update;
                    return;
                }

                var originalItem = FirstOrDefaultInnerListUnsafe(item.Id);
                if (originalItem == null) return;
                TrackingItems.AddTrackingItem(TrackingState.Update, item, originalItem);
            }
        }

        public bool TryGetTrackingItem(TId id, [NotNullWhen(true)] out TrackingItem<TDataItem, TId>? trackingItem)
        {
            lock (_syncRoot)
            {
                return TrackingItems.TryGetTrackingItem(id, out trackingItem);
            }
        }

        #endregion

        #region Save Methods

        public async Task<List<TrackingItem<TDataItem, TId>>> SaveChanges()
        {
            BeginSync();
            try
            {
                List<TrackingItem<TDataItem, TId>> itemsToSave;
                await _asyncLock.WaitAsync();
                try
                {
                    itemsToSave = TrackingItems.ToList();
                }
                finally
                {
                    _asyncLock.Release();
                }

                foreach (var trackingItem in itemsToSave)
                {
                    try
                    {
                        await SaveTrackingItemUnsafe(trackingItem);
                    }
                    catch (Exception)
                    {
                        TryRollbackItem(trackingItem.CurrentValue.Id, out _);
                    }
                }

                lock (_syncRoot)
                {
                    return TrackingItems.ToList();
                }
            }
            finally
            {
                EndSync();
            }
        }

        public async Task SaveChangesAsync()
        {
            BeginSync();
            try
            {
                List<TrackingItem<TDataItem, TId>> itemsToSave;
                await _asyncLock.WaitAsync();
                try
                {
                    itemsToSave = TrackingItems.ToList();
                }
                finally
                {
                    _asyncLock.Release();
                }

                foreach (var trackingItem in itemsToSave)
                {
                    try
                    {
                        await SaveTrackingItemUnsafeAsync(trackingItem);
                    }
                    catch (Exception)
                    {
                        TryRollbackItem(trackingItem.CurrentValue.Id, out _);
                    }
                }
            }
            finally
            {
                EndSync();
            }
        }

        public async Task<TDataItem> SaveItem(TId id)
        {
            TrackingItem<TDataItem, TId>? trackingItem;
            await _asyncLock.WaitAsync();
            try
            {
                if (!TrackingItems.TryGetTrackingItem(id, out trackingItem))
                    throw new NullReferenceException($"SaveItem; trackingItem not found for id: {id}");
            }
            finally
            {
                _asyncLock.Release();
            }

            return await SaveTrackingItemUnsafe(trackingItem);
        }

        public async Task<TDataItem> SaveItem(TId id, TrackingState trackingState)
        {
            TDataItem? item;
            lock (_syncRoot)
            {
                item = FirstOrDefaultInnerListUnsafe(id);
            }

            if (item == null) 
                throw new NullReferenceException($"SaveItem; item not found for id: {id}");

            return await SaveItem(item, trackingState);
        }

        public Task<TDataItem> SaveItem(TDataItem item, TrackingState trackingState) 
            => SaveItemUnsafe(item, trackingState);

        private Task<TDataItem> SaveTrackingItemUnsafe(TrackingItem<TDataItem, TId> trackingItem)
            => SaveItemUnsafe(trackingItem.CurrentValue, trackingItem.TrackingState);

        private async Task SaveTrackingItemUnsafeAsync(TrackingItem<TDataItem, TId> trackingItem)
            => SaveItemUnsafeAsync(trackingItem.CurrentValue, trackingItem.TrackingState);

        private Task<TDataItem> SaveItemUnsafe(TDataItem item, TrackingState trackingState)
        {
            var messageTag = SignalRCrudTags.GetMessageTagByTrackingState(trackingState);
            if (messageTag == AcSignalRTags.None) 
                throw new ArgumentException($"SaveItemUnsafe; messageTag == SignalRTags.None");

            return SignalRClient.PostDataAsync(messageTag, item).ContinueWith(task =>
            {
                if (task.Result == null)
                {
                    if (TryRollbackItem(item.Id, out _)) return item;
                    throw new NullReferenceException($"SaveItemUnsafe; result == null");
                }

                ProcessSavedResponseItem(task.Result, trackingState, item.Id);
                return task.Result;
            }, TaskScheduler.Default);
        }

        /// <summary>
        /// Saves item in background (fire-and-forget friendly). Does not block UI.
        /// </summary>
        private Task SaveItemUnsafeAsync(TDataItem item, TrackingState trackingState)
        {
            var messageTag = SignalRCrudTags.GetMessageTagByTrackingState(trackingState);
            if (messageTag == AcSignalRTags.None) return Task.CompletedTask;

            return SignalRClient.PostDataAsync(messageTag, item, response =>
            {
                if (response.Status != SignalResponseStatus.Success || response.RawResponseData == null)
                {
                    if (TryRollbackItem(item.Id, out _)) return;
                    throw new NullReferenceException($"SaveItemUnsafeAsync; Status: {response.Status}");
                }

                var resultItem = response.GetResponseData<TDataItem>();
                ProcessSavedResponseItem(resultItem, trackingState, item.Id);
            });
        }

        private Task ProcessSavedResponseItem(TDataItem? resultItem, TrackingState trackingState, TId originalId)
        {
            if (resultItem == null) return Task.CompletedTask;

            lock (_syncRoot)
            {
                if (TrackingItems.TryGetTrackingItem(originalId, out var trackingItem))
                    TrackingItems.Remove(trackingItem);

                var index = FindIndexInnerListUnsafe(originalId);
                if (index >= 0)
                {
                    resultItem.CopyTo(InnerList[index]);
                }
            }

            var eventArgs = new ItemChangedEventArgs<TDataItem>(resultItem, trackingState);
            if (OnDataSourceItemChanged != null) return OnDataSourceItemChanged.Invoke(eventArgs);

            return Task.CompletedTask;
        }

        #endregion

        #region Rollback Methods

        private void RollbackItemUnsafe(TrackingItem<TDataItem, TId> trackingItem)
        {
            var index = FindIndexInnerListUnsafe(trackingItem.CurrentValue.Id);
            if (index >= 0)
            {
                if (trackingItem.TrackingState == TrackingState.Add) 
                    InnerList.RemoveAt(index);
                else 
                    trackingItem.OriginalValue!.CopyTo(InnerList[index]);
            }
            else if (trackingItem.TrackingState != TrackingState.Add)
            {
                InnerList.Add(trackingItem.OriginalValue!);
            }

            TrackingItems.Remove(trackingItem);
        }

        public bool TryRollbackItem(TId id, out TDataItem? originalValue)
        {
            lock (_syncRoot)
            {
                if (TrackingItems.TryGetTrackingItem(id, out var trackingItem))
                {
                    originalValue = trackingItem.OriginalValue;
                    RollbackItemUnsafe(trackingItem);
                    return true;
                }

                originalValue = null;
                return false;
            }
        }

        public void Rollback()
        {
            lock (_syncRoot)
            {
                foreach (var trackingItem in TrackingItems.ToList())
                    RollbackItemUnsafe(trackingItem);
            }
        }

        #endregion

        #region Collection Properties and Methods

        public int Count
        {
            get
            {
                lock (_syncRoot)
                {
                    return InnerList.Count;
                }
            }
        }

        public void Clear() => Clear(true);

        public void Clear(bool clearChangeTracking)
        {
            lock (_syncRoot)
            {
                ClearUnsafe(clearChangeTracking);
            }
        }

        private void ClearUnsafe(bool clearChangeTracking)
        {
            if (clearChangeTracking) TrackingItems.Clear();
            InnerList.Clear();
        }

        public int IndexOf(TId id)
        {
            lock (_syncRoot)
            {
                return FindIndexInnerListUnsafe(id);
            }
        }

        public int IndexOf(TDataItem item) => IndexOf(item.Id);
        
        public bool TryGetIndex(TId id, out int index) => (index = IndexOf(id)) > -1;

        public bool Contains(TDataItem item) => IndexOf(item) > -1;

        private bool ContainsUnsafe(TDataItem item) => FindIndexInnerListUnsafe(item.Id) > -1;

        public bool TryGetValue(TId id, [NotNullWhen(true)] out TDataItem? item)
        {
            lock (_syncRoot)
            {
                item = FirstOrDefaultInnerListUnsafe(id);
                return item != null;
            }
        }

        public void CopyTo(TDataItem[] array) => CopyTo(array, 0);

        public void CopyTo(TDataItem[] array, int arrayIndex)
        {
            lock (_syncRoot)
            {
                InnerList.CopyTo(array, arrayIndex);
            }
        }

        public int BinarySearch(int index, int count, TDataItem item, IComparer<TDataItem>? comparer)
        {
            throw new NotImplementedException($"BinarySearch");
        }

        public int BinarySearch(TDataItem item) => BinarySearch(0, Count, item, null);
        public int BinarySearch(TDataItem item, IComparer<TDataItem>? comparer) => BinarySearch(0, Count, item, comparer);

        public IEnumerator<TDataItem> GetEnumerator()
        {
            lock (_syncRoot)
            {
                // Return a copy to avoid modification during enumeration
                return InnerList.ToList().GetEnumerator();
            }
        }

        public ReadOnlyCollection<TDataItem> AsReadOnly() => new(this);
        
        private static bool IsCompatibleObject(object? value) 
            => (value is TDataItem) || (value == null && default(TDataItem) == null);

        private void SetWorkingReferenceListUnsafe(TIList workingIList)
        {
            HasWorkingReferenceList = true;
            if (ReferenceEquals(InnerList, workingIList)) return;

            if (workingIList.Count == 0) AddRangeUnsafe(InnerList, workingIList);

            ClearUnsafe(true);
            InnerList = workingIList;
        }

        #endregion

        #region IList, ICollection Interface Implementation

        bool IList.IsReadOnly => false;

        object? IList.this[int index]
        {
            get => this[index];
            set
            {
                if (default(TDataItem) != null && value == null) 
                    throw new NullReferenceException(nameof(value));

                try
                {
                    this[index] = (TDataItem)value!;
                }
                catch (InvalidCastException)
                {
                    throw new InvalidCastException(nameof(value));
                }
            }
        }

        int IList.Add(object? item)
        {
            if (default(TDataItem) != null && item == null) 
                throw new NullReferenceException(nameof(item));

            try
            {
                Add((TDataItem)item!);
            }
            catch (InvalidCastException)
            {
                throw new InvalidCastException(nameof(item));
            }

            return Count - 1;
        }

        void IList.Clear() => Clear(true);
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
        bool IList.Contains(object? item) => IsCompatibleObject(item) && Contains((TDataItem)item!);
        int IList.IndexOf(object? item) => IsCompatibleObject(item) ? IndexOf((TDataItem)item!) : -1;

        void IList.Insert(int index, object? item)
        {
            if (default(TDataItem) != null && item == null) 
                throw new NullReferenceException(nameof(item));

            try
            {
                Insert(index, (TDataItem)item!);
            }
            catch (InvalidCastException)
            {
                throw new InvalidCastException(nameof(item));
            }
        }

        void IList.Remove(object? item)
        {
            if (IsCompatibleObject(item)) Remove((TDataItem)item!);
        }

        void ICollection<TDataItem>.Clear() => Clear(true);

        void ICollection.CopyTo(Array array, int arrayIndex)
        {
            if (array != null && array.Rank != 1)
                throw new ArgumentException("Multi-dimensional arrays not supported");

            try
            {
                lock (_syncRoot)
                {
                    Array.Copy(InnerList.ToArray(), 0, array!, arrayIndex, InnerList.Count);
                }
            }
            catch (ArrayTypeMismatchException)
            {
                throw new ArrayTypeMismatchException();
            }
        }

        int ICollection.Count => Count;
        int ICollection<TDataItem>.Count => Count;
        bool ICollection<TDataItem>.IsReadOnly => false;
        void IList<TDataItem>.RemoveAt(int index) => RemoveAt(index);
        int IReadOnlyCollection<TDataItem>.Count => Count;

        #endregion
    }

    public class ItemChangedEventArgs<T> where T : class
    {
        internal ItemChangedEventArgs(T item, TrackingState trackingState)
        {
            Item = item;
            TrackingState = trackingState;
        }

        public T Item { get; }
        public TrackingState TrackingState { get; }
    }
}
