using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Timers;
using InABox.Core;

namespace InABox.Clients
{
    public enum SerializerProtocol
    {
        Rest,
        RPC
    }

    public class QueryMultipleResults
    {
        private readonly Dictionary<string, CoreTable> Results;

        internal QueryMultipleResults(Dictionary<string, CoreTable> results)
        {
            Results = results;
        }

        public CoreTable this[string name] => Results[name];

        public CoreTable Get<T>() => Results[typeof(T).Name];

        /// <summary>
        /// Like <see cref="Get{T}"/>, but calls <see cref="CoreTable.ToObjects{T}"/> on the table.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public IEnumerable<T> GetObjects<T>()
            where T: BaseObject, new() 
            => Results[typeof(T).Name].ToObjects<T>();

        public CoreTable Get(string name) => Results[name];

        public CoreTable GetOrDefault(string name) => Results.GetValueOrDefault(name);
    }

    public abstract class Client
    {
        public abstract CoreTable Query(IFilter? filter = null, IColumns? columns = null, ISortOrder? sortOrder = null);
        public abstract void Save(Entity entity, string auditNote);
        public abstract void Save(IEnumerable<Entity> entity, string auditNote);

        private static IClient CheckClient()
        {
            return ClientFactory.CreateClient<User>();
        }

        public static Dictionary<string, CoreTable> QueryMultiple(Dictionary<string, IQueryDef> queries)
        {
            try
            {
                using var timer = new Profiler(false);
                var result = CheckClient().QueryMultiple(queries);
                timer.Log(result.Sum(x => x.Value.Rows.Count));
                return result;
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        private static IClient<TEntity> CheckClient<TEntity>() where TEntity : Entity, IRemotable, IPersistent, new()
        {
            return ClientFactory.CreateClient<TEntity>();
        }

        public static void EnsureColumns<TEntity>(TEntity entity, Columns<TEntity> columns)
            where TEntity : Entity, IRemotable, IPersistent, new()
        {
            var newColumns = Columns.None<TEntity>()
                .AddRange(columns.Where(x => !entity.HasColumn(x.Property)));
            if (newColumns.Count > 0)
            {
                var row = Query(new Filter<TEntity>(x => x.ID).IsEqualTo(entity.ID), newColumns).Rows.FirstOrDefault();
                row?.FillObject(entity);
            }
        }
        public static void EnsureColumns<TEntity>(ICollection<TEntity> entities, Columns<TEntity> columns)
            where TEntity : Entity, IRemotable, IPersistent, new()
        {
            var newColumns = Columns.None<TEntity>()
                .AddRange(columns.Where(x => entities.Any(entity => !entity.HasColumn(x.Property))));
            if (newColumns.Count > 0)
            {
                newColumns.Add(x => x.ID);
                var table = Query(new Filter<TEntity>(x => x.ID).InList(entities.Select(x => x.ID).ToArray()), newColumns);
                foreach(var row in table.Rows)
                {
                    var id = row.Get<TEntity, Guid>(x => x.ID);
                    var entity = entities.FirstOrDefault(x => x.ID == id);
                    if(entity is null)
                    {
                        // Shouldn't happen, but just in case.
                        continue;
                    }
                    row?.FillObject(entity);
                }
            }
        }

        public static CoreTable Query<TEntity>(Filter<TEntity>? filter = null, Columns<TEntity>? columns = null, SortOrder<TEntity>? orderby = null)
            where TEntity : Entity, IRemotable, IPersistent, new()
        {
            return new Client<TEntity>().Query(filter, columns, orderby);
        }

        public static void Query<TEntity>(Filter<TEntity>? filter, Columns<TEntity>? columns, SortOrder<TEntity>? orderby, Action<CoreTable?, Exception?> callback)
            where TEntity : Entity, IRemotable, IPersistent, new()
        {
            new Client<TEntity>().Query(filter, columns, orderby, callback);
        }

        public static void Save<TEntity>(TEntity entity, string auditNote)
            where TEntity : Entity, IRemotable, IPersistent, new()
        {
            new Client<TEntity>().Save(entity, auditNote);
        }
        public static void Save<TEntity>(IEnumerable<TEntity> entities, string auditNote)
            where TEntity : Entity, IRemotable, IPersistent, new()
        {
            new Client<TEntity>().Save(entities, auditNote);
        }

        public static void Save<TEntity>(TEntity entity, string auditNote, Action<TEntity, Exception?> callback)
            where TEntity : Entity, IRemotable, IPersistent, new()
        {
            new Client<TEntity>().Save(entity, auditNote, callback);
        }
        public static void Save<TEntity>(IEnumerable<TEntity> entities, string auditNote, Action<IEnumerable<TEntity>, Exception?> callback)
            where TEntity : Entity, IRemotable, IPersistent, new()
        {
            new Client<TEntity>().Save(entities, auditNote, callback);
        }

        public static void Delete<TEntity>(TEntity entity, string auditNote)
            where TEntity : Entity, IRemotable, IPersistent, new()
        {
            new Client<TEntity>().Delete(entity, auditNote);
        }

        public static void Delete<TEntity>(TEntity entity, string auditNote, Action<TEntity, Exception?> callback)
            where TEntity : Entity, IRemotable, IPersistent, new()
        {
            new Client<TEntity>().Delete(entity, auditNote, callback);
        }

        public static void Delete<TEntity>(IEnumerable<TEntity> entities, string auditNote)
            where TEntity : Entity, IRemotable, IPersistent, new()
        {
            new Client<TEntity>().Delete(entities, auditNote);
        }

        public static void QueryMultiple(
            Action<Dictionary<string, CoreTable>?, Exception?> callback,
            Dictionary<string, IQueryDef> queries)
        {
            try
            {
                using var timer = new Profiler(false);
                CheckClient().QueryMultiple((result, e) =>
                {
                    timer.Dispose(result != null ? result.Sum(x => x.Value.Rows.Count) : -1);
                    callback?.Invoke(result, e);
                }, queries);
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public static QueryMultipleResults QueryMultiple(params IKeyedQueryDef[] queries) =>
            new QueryMultipleResults(QueryMultiple(queries.ToDictionary(x => x.Key, x => x as IQueryDef)));

        public static void QueryMultiple(Action<QueryMultipleResults?, Exception?> callback, params IKeyedQueryDef[] queries) =>
            QueryMultiple((results, e) =>
            {
                if (results != null)
                {
                    callback?.Invoke(new QueryMultipleResults(results), e);
                }
                else
                {
                    callback?.Invoke(null, e);
                }
            }, queries.ToDictionary(x => x.Key, x => x as IQueryDef));

        public static QueryMultipleResults QueryMultiple(IEnumerable<IKeyedQueryDef> queries) =>
            new QueryMultipleResults(QueryMultiple(queries.ToDictionary(x => x.Key, x => x as IQueryDef)));

        public static void QueryMultiple(Action<QueryMultipleResults?, Exception?> callback, IEnumerable<IKeyedQueryDef> queries) =>
            QueryMultiple((results, e) =>
            {
                if(results != null)
                {
                    callback?.Invoke(new QueryMultipleResults(results), e);
                }
                else
                {
                    callback?.Invoke(null, e);
                }
            }, queries.ToDictionary(x => x.Key, x => x as IQueryDef));

        public static IValidationData Validate(Guid session)
        {
            try
            {
                using (new Profiler(true))
                    return CheckClient().Validate(session);
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public static IValidationData Validate(string pin, Guid session = default)
        {
            try
            {
                using (new Profiler(true))
                    return CheckClient().Validate(pin, session);
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public static IValidationData Validate(string userid, string password, Guid session = default)
        {
            try
            {
                using (new Profiler(true))
                    return CheckClient().Validate(userid, password, session);
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public static bool Check2FA(string code, Guid? session = null)
        {
            try
            {
                using (new Profiler(true))
                    return CheckClient().Check2FA(code, session);
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public static bool Ping()
        {
            try
            {
                return CheckClient().Ping();
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public static DatabaseInfo Info()
        {
            try
            {
                using (new Profiler(true))
                    return CheckClient().Info();
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public static string Version()
        {
            try
            {
                using (new Profiler(true))
                    return CheckClient().Version();
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public static string ReleaseNotes()
        {
            try
            {
                using (new Profiler(true))
                    return CheckClient().ReleaseNotes();
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public static byte[]? Installer()
        {
            try
            {
                using (new Profiler(true))
                    return CheckClient().Installer();
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public static Client Create(Type TEntity) => 
            (Activator.CreateInstance(typeof(Client<>).MakeGenericType(TEntity)) as Client)!;

    }

    public class Client<TEntity> : Client, IDisposable where TEntity : Entity, IPersistent, IRemotable, new()
    {
        private IClient<TEntity> _client;

        public Client()
        {
            _client = ClientFactory.CreateClient<TEntity>();
        }

        public void Dispose()
        {
        }


        private void CheckSupported()
        {
            if (!ClientFactory.IsSupported<TEntity>())
                throw new NotSupportedException(string.Format("{0} is not supported in this context", typeof(TEntity).EntityName()));
        }

        public CoreTable Query(Filter<TEntity>? filter = null, Columns<TEntity>? columns = null, SortOrder<TEntity>? orderby = null)
        {
            try
            {
                using var timer = new Profiler<TEntity>(false);
                CheckSupported();
                var result = _client.Query(filter, columns, orderby);
                timer.Log(result.Rows.Count);
                return result;
            }
            catch(RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }
        public override CoreTable Query(IFilter? filter, IColumns? columns, ISortOrder? sortOrder)
        {
            return Query(filter as Filter<TEntity>, columns as Columns<TEntity>, sortOrder as SortOrder<TEntity>);
        }

        public void Query(Filter<TEntity>? filter, Columns<TEntity>? columns, SortOrder<TEntity>? sort, Action<CoreTable?, Exception?> callback)
        {
            try
            {
                var timer = new Profiler<TEntity>(false);
                CheckSupported();
                _client.Query(filter, columns, sort, (c, e) =>
                {
                    timer.Log(c != null ? c.Rows.Count : -1);
                    callback?.Invoke(c, e);
                });
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public TEntity[] Load(Filter<TEntity>? filter = null, SortOrder<TEntity>? sort = null)
        {
            try
            {

                using (var timer = new Profiler<TEntity>(false))
                {
                    CheckSupported();
                    var result = _client.Load(filter, sort);
                    foreach (var entity in result)
                        entity.CommitChanges();
                    timer.Log(result.Length);
                    return result;
                }
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public void Load(Filter<TEntity> filter, SortOrder<TEntity> sort, Action<TEntity[]?, Exception?> callback)
        {
            try
            {
                var timer = new Profiler<TEntity>(false);
                CheckSupported();
                _client.Load(filter, sort, (i, e) =>
                {
                    timer.Dispose(i != null ? i.Length : -1);
                    callback?.Invoke(i, e);
                });
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public override void Save(Entity entity, string auditNote)
        {
            try
            {
                Save((entity as TEntity)!, auditNote);
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }
        public override void Save(IEnumerable<Entity> entities, string auditNote)
        {
            try
            {
                Save(entities.Cast<TEntity>(), auditNote);
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }
        public void Save(TEntity entity, string auditnote)
        {
            try
            {

                using (new Profiler<TEntity>(true))
                {
                    CheckSupported();
                    entity.LastUpdate = DateTime.Now;
                    entity.LastUpdateBy = ClientFactory.UserID;
                    _client.Save(entity, auditnote);
                    entity.CommitChanges();
                }
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public void Save(TEntity entity, string auditnote, Action<TEntity, Exception?> callback)
        {
            try
            {
                var timer = new Profiler<TEntity>(false);
                CheckSupported();
                _client.Save(entity, auditnote, (i, c) =>
                {
                    timer.Dispose();
                    callback?.Invoke(i, c);
                });
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public void Save(IEnumerable<TEntity> entities, string auditnote)
        {
            try
            {
                using var timer = new Profiler<TEntity>(false);
                CheckSupported();
                var items = entities.AsArray();
                if (items.Any())
                    _client.Save(items, auditnote);
                timer.Log(items.Length);
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public void Save(IEnumerable<TEntity> entities, string auditnote, Action<IEnumerable<TEntity>, Exception?> callback)
        {
            try
            {
                var timer = new Profiler<TEntity>(false);
                CheckSupported();
                var items = entities.AsArray();
                if (items.Any())
                {
                    _client.Save(items, auditnote, (i, e) =>
                    {
                        timer.Dispose(i.Count());
                        callback?.Invoke(i, e);
                    });
                }
                else
                {
                    timer.Dispose(0);
                    callback?.Invoke(items, null);
                }
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }


        public void Delete(TEntity entity, string auditnote)
        {
            try
            {
                using (new Profiler<TEntity>(true))
                {
                    CheckSupported();
                    _client.Delete(entity, auditnote);
                }
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public void Delete(TEntity entity, string auditnote, Action<TEntity, Exception?> callback)
        {
            try
            {
                var timer = new Profiler<TEntity>(true);
                CheckSupported();
                _client.Delete(entity, auditnote, (i, e) =>
                {
                    timer.Dispose();
                    callback?.Invoke(i, e);
                });
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public void Delete(IEnumerable<TEntity> entities, string auditnote)
        {
            try
            {
                using var timer = new Profiler<TEntity>(false);
                CheckSupported();
                var items = entities.AsArray();
                _client.Delete(items, auditnote);
                timer.Log(items.Length);
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public void Delete(IEnumerable<TEntity> entities, string auditnote, Action<IList<TEntity>, Exception?> callback)
        {
            try
            {
                var timer = new Profiler<TEntity>(false);
                CheckSupported();
                var items = entities.AsArray();
                _client.Delete(items, auditnote, (i, e) =>
                {
                    timer.Dispose(i.Count);
                    callback?.Invoke(i, e);
                });
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public IEnumerable<string> SupportedTypes()
        {
            try
            {
                using (new Profiler(true))
                    return _client.SupportedTypes();
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public new DatabaseInfo Info()
        {
            try
            {
                using (new Profiler(true))
                    return _client.Info();
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }
    }
}