using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Text;
using Quadarax.Foundation.Core.Collections;
using Quadarax.Foundation.Core.Logging;
using Quadarax.Foundation.Core.QMonitor.Attributes;
using Quadarax.Foundation.Core.Reflection.Extensions;

namespace Quadarax.Foundation.Core.QMonitor
{
    public class QMonClient : QMonHostBase<QMonClientConfiuration>
    {
        #region *** Properties ***

        protected override Uri UriGeneral => Configuration.TargetUriGeneral;
        protected override Uri UriData => Configuration.TargetUriData;
        protected override TimeSpan IntervalGeneral => Configuration.EmitGeneralInterval;
        protected override TimeSpan? IntervalData => Configuration.EmitDataInterval;

        public string InstanceIdentifier { get; }
        #endregion

        #region *** Fields ***
        private readonly IDictionary<string, MonItemGeneral> _generalCache = new Dictionary<string, MonItemGeneral>();
        private readonly IList<MonData> _dataCache = new LockedList<MonData>();
        #endregion

        #region *** Constructors ***
        public QMonClient(string instanceIdentifier, QMonClientConfiuration configuration, ILogger? externalLogger) : base(configuration, externalLogger)
        {
            InstanceIdentifier = instanceIdentifier ?? throw new ArgumentNullException(nameof(instanceIdentifier));
            ScanAssemblies();
        }

        private void ScanAssemblies()
        {
            var assemblies = AppDomain.CurrentDomain.GetAssemblies();
            foreach (var assembly in assemblies)
            {
                var types = assembly.GetTypesWithAttribute<MonitoredClassAttribute>(true);
                foreach (var type in types)
                {
                    var monItemGeneral = new MonItemGeneral
                    {
                        Caption = type.GetCustomAttribute<MonitoredClassAttribute>()?.Caption ?? type.Name,
                        Description = type.GetCustomAttribute<MonitoredClassAttribute>()?.Description ?? string.Empty,
                        ViewType = MonItemGeneral.MonViewType.Item,
                        Name = type.FullName,
                    };

                    var properties = type.GetPropertiesWithAttribute<MonitoredPropertyAttribute>(true);
                    var keys = new List<Tuple<string, int>>();

                    foreach (var property in properties)
                    {
                        var itemDescriptor = new MonItemGeneral.ItemDescriptor
                        {
                            Name = property.Name,
                            TypeName = property.PropertyType.Name,
                            Label = property.GetCustomAttribute<MonitoredPropertyAttribute>()?.Label ?? property.Name,
                            Description = property.GetCustomAttribute<MonitoredPropertyAttribute>()?.Description ?? string.Empty,
                            Order = property.GetCustomAttribute<MonitoredPropertyAttribute>()?.Order ?? 0,
                            _propertyInfo = property
                        };
                        monItemGeneral.Items.Add(itemDescriptor);
                        if (property.GetCustomAttribute<MonitoredPropertyKeyAttribute>() != null)
                        {
                            keys.Add(new Tuple<string, int>(property.Name, property.GetCustomAttribute<MonitoredPropertyKeyAttribute>()!.KeyOrder));
                        }
                    }

                    foreach (var key in keys.OrderBy(x => x.Item2))
                    {
                        var ord = monItemGeneral.Items.FindIndex(x => x.Name == key.Item1);
                        if (ord >= 0)
                            monItemGeneral.KeyOrdinals.Add(ord);
                    }
                    _generalCache.Add(type.FullName!, monItemGeneral);
                }
            }
        }

        #endregion

        #region *** Public Operations ***
        public void Notify(object? monitoredItem) 
        {
            if (monitoredItem == null) return;
            var fullName = monitoredItem.GetType().FullName;
            if (fullName != null && !_generalCache.ContainsKey(fullName)) throw new NotSupportedException($"Type {monitoredItem.GetType().FullName} is not flagged by MonitoredClassAttribute as monitored");

            var data = new MonData()
            {
                InstanceIdentifier = InstanceIdentifier,
                Name = fullName,
                TimestampFtUtc = DateTime.Now.ToFileTimeUtc(),
                Data = Binder.SaveToString(monitoredItem)
            };
            _dataCache.Add(data);
        }
        #endregion

        #region *** Protected Operations ***
        protected override void OnDisposing()
        {
            base.OnDisposing();
            _generalCache.Clear();
        }

        protected override async Task OnTimerData(QMonWorkerContext context)
        {
            if (_dataCache.Count == 0) return;
            try
            {
                var start = DateTime.Now;
                var itmCnt = 0;
                var declaredSize = 0;
                var sentSize = 0;
                var dataToSent = _dataCache.ToArray();
                foreach (var data in dataToSent)
                {
                    var dataOut = Serialize(data);
                    declaredSize += dataOut.Length;
                    sentSize += await context.Client.SendAsync(dataOut, context.Cancel);
                    _dataCache.Remove(data);
                    itmCnt++;
                }
                Log(LogSeverityEnum.Trace, $"[{InstanceIdentifier}] Sent data blocks = {itmCnt} of declared {declaredSize} bytes/sent {sentSize} bytes @ {DateTime.Now.Subtract(start).TotalMilliseconds} ms.");
            }
            catch (Exception ex)
            {
                Log(LogSeverityEnum.Error, $"[{InstanceIdentifier}] Error sending general data", ex);
            }
        }

        protected override async Task OnTimerGeneral(QMonWorkerContext context)
        {
            try
            {

                var data = new MonGeneral()
                {
                    InstanceIdentifier = InstanceIdentifier,
                    Items = new List<MonItemGeneral>()
                };
                data.Items.AddRange(_generalCache.Values);
                var dataOut = Serialize(data);
                await context.Client.SendAsync(dataOut, context.Cancel);
                Log(LogSeverityEnum.Trace, $"[{InstanceIdentifier}] Sent General packet size = {dataOut.Length} bytes");
            }
            catch (Exception ex)
            {
                Log(LogSeverityEnum.Error, $"[{InstanceIdentifier}] Error sending general data", ex);
            }
        }

        private byte[] Serialize(object data)
        {
            if (data == null) throw new ArgumentNullException(nameof(data));
            var json = Binder.SaveToString(data);
            return Encoding.UTF8.GetBytes(json);
        }
        protected override UdpClient CreateUdpClient(Uri targetUri)
        {
            var ep = new IPEndPoint(IPAddress.Parse(targetUri.DnsSafeHost), targetUri.Port);
            var udpClient = new UdpClient();
            udpClient.Connect(ep);
            return udpClient;
        }

        protected override void OnOpen()
        {
            Log(LogSeverityEnum.Info, $"Started.");
        }

        protected override void OnClose()
        {
            Log(LogSeverityEnum.Info, $"Closed.");
        }
        #endregion

        #region *** Nested Types ***
        #endregion
    }
}
