using InABox.Core;
using IQueryProvider = InABox.Core.IQueryProvider;

namespace InABox.Database;

public delegate void LogEvent(LogType type, string message);

public interface IProviderFactory
{
    string URL { get; set; }
    Type[] Types { get; set; }

    void ForceRecreateViews();

    void Start();

    IProvider NewProvider(Logger logger);
}

public interface IProvider
{
    Logger Logger { get; set; }

    IEnumerable<object[]> List<T>(Filter<T>? filter = null, Columns<T>? columns = null, SortOrder<T>? sort = null, CoreRange? range = null) where T : Entity, new();
    
    bool TableExists<T>();
    bool TableExists(Type t);
    bool TableExists(string name);
    
    CoreTable? GetTable<T>();
    CoreTable? GetTable(Type t);
    CoreTable? GetTable(string name);
    
    void DropTable<T>();
    void DropTable(Type t);
    void DropTable(string name);
    
    IEnumerable<object[]> List(string sql);
    
    CoreTable Query(string sql);
    
    int Update(string sql);

    string CreateSQL<T>(
        Filter<T>? filter,
        Columns<T>? columns,
        SortOrder<T>? sort = null,
        CoreRange? range = null,
        bool distinct = false);
    
    CoreTable Query<T>(Filter<T>? filter = null, Columns<T>? columns = null, SortOrder<T>? sort = null, 
        CoreRange? range = null, bool log = false, bool distinct = false) where T : Entity, new();
    
    CoreTable Query(Type type, IFilter? filter = null, IColumns? columns = null, ISortOrder? sort = null, 
        CoreRange? range = null, bool log = false, bool distinct = false);
    
    /// <summary>
    /// Same as <see cref="Query{T}(Filter{T}, Columns{T}, SortOrder{T}, CoreRange, bool, bool)"/>, but only for deleted items
    /// </summary>
    CoreTable QueryDeleted<T>(Deletion deletion, Filter<T>? filter = null, Columns<T>? columns = null, SortOrder<T>? sort = null, CoreRange? range = null, bool deleted = false) where T : Entity, new();

    T[] Load<T>(Filter<T>? filter = null, SortOrder<T>? sort = null, CoreRange? range = null) where T : Entity, new();

    void Save<T>(T entity) where T : Entity;
    void Save<T>(IEnumerable<T> entities) where T : Entity;

    void Save(Type type, Entity entity);
    void Save(Type type, IEnumerable<Entity> entities);

    void Delete<T>(T entity, string userID) where T : Entity, new();
    void Delete<T>(IEnumerable<T> entities, string userID) where T : Entity, new();

    void Purge<T>(T entity) where T : Entity;
    void Purge<T>(IEnumerable<T> entities) where T : Entity;

    void Purge(Deletion deletion);
    void Recover(Deletion deletion);

    List<Type> GetDeletionTypes(Deletion deletion);
}

public static class ProviderExtensions
{
    public static void EnsureColumns<TEntity>(this IProvider provider, TEntity entity, Columns<TEntity> columns)
        where TEntity : Entity, new()
    {
        var newColumns = Columns.None<TEntity>()
            .AddRange(columns.Where(x => !entity.HasColumn(x.Property)));
        if (newColumns.Count > 0)
        {
            var row = provider.Query(new Filter<TEntity>(x => x.ID).IsEqualTo(entity.ID), newColumns).Rows.FirstOrDefault();
            row?.FillObject(entity);
        }
    }

    public static void EnsureColumns<TEntity>(this IProvider provider, ICollection<TEntity> entities, Columns<TEntity> columns)
        where TEntity : Entity, IRemotable, 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 = provider.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);
            }
        }
    }

    private class ProviderQueryProvider<TEntity>(IProvider provider, string userID) : IQueryProvider<TEntity>
        where TEntity : Entity, new()
    {

        public bool ExcludeCustomProperties { get; set; }
        
        private IProvider Provider = provider;

        private string UserID = userID;

        public CoreTable Query(Filter<TEntity>? filter = null, Columns<TEntity>? columns = null, SortOrder<TEntity>? sort = null, CoreRange? range = null)
        {
            return Provider.Query(filter, columns, sort, range);
        }

        public void Query(Filter<TEntity>? filter, Columns<TEntity>? columns, SortOrder<TEntity>? sort, CoreRange? range, Action<CoreTable?, Exception?> action)
        {
            Task.Run(() =>
            {
                try
                {
                    var result = Provider.Query(filter, columns, sort, range);
                    action(result, null);
                }
                catch(Exception e)
                {
                    action(null, e);
                }
            });
        }

        public void Delete(TEntity entity, string auditNote)
        {
            Provider.Delete(entity, UserID);
        }

        public void Delete(IEnumerable<TEntity> entities, string auditNote)
        {
            Provider.Delete(entities, UserID);
        }

        public void Delete(TEntity entity, string auditnote, Action<TEntity, Exception?> callback)
        {
            Task.Run(() =>
            {
                try
                {
                    Provider.Delete(entity, UserID);
                    callback(entity, null);
                }
                catch (Exception e)
                {
                    callback(entity, e);
                }
            });
        }

        public void Delete(IEnumerable<TEntity> entities, string auditnote, Action<IList<TEntity>, Exception?> callback)
        {
            var ents = entities.AsIList();
            Task.Run(() =>
            {
                try
                {
                    Provider.Delete(ents, UserID);
                    callback(ents, null);
                }
                catch (Exception e)
                {
                    callback(ents, e);
                }
            });
        }

        public void Save(TEntity entity, string auditNote)
        {
            Provider.Save(entity);
        }

        public void Save(IEnumerable<TEntity> entities, string auditNote)
        {
            Provider.Save(entities);
        }

        public void Save(TEntity entity, string auditnote, Action<TEntity, Exception?> callback)
        {
            Task.Run(() =>
            {
                try
                {
                    Provider.Save(entity);
                    callback(entity, null);
                }
                catch (Exception e)
                {
                    callback(entity, e);
                }
            });
        }

        public void Save(IEnumerable<TEntity> entities, string auditnote, Action<IEnumerable<TEntity>, Exception?> callback)
        {
            var ents = entities.AsIList();
            Task.Run(() =>
            {
                try
                {
                    Provider.Save(ents);
                    callback(ents, null);
                }
                catch (Exception e)
                {
                    callback(ents, e);
                }
            });
        }

        #region Non-generics

        public CoreTable Query(IFilter? filter = null, IColumns? columns = null, ISortOrder? sort = null, CoreRange? range = null)
        {
            return Provider.Query(typeof(TEntity), filter, columns, sort, range);
        }

        #endregion
    }

    public static IQueryProvider<TEntity> QueryProvider<TEntity>(this IProvider provider, string userID) where TEntity : Entity, new()
    {
        return new ProviderQueryProvider<TEntity>(provider, userID);
    }
    public static IQueryProvider<TEntity> QueryProvider<TEntity>(this IStore store) where TEntity : Entity, new()
    {
        return new ProviderQueryProvider<TEntity>(store.Provider, store.UserID);
    }

    private class ProviderQueryProviderFactory(IProvider provider, string userID) : IQueryProviderFactory
    {
        public bool ExcludeCustomProperties => false;

        public IQueryProvider Create(Type T)
        {
            return (Activator.CreateInstance(typeof(ProviderQueryProvider<>).MakeGenericType(T), provider, userID) as IQueryProvider)!;
        }
    }

    public static IQueryProviderFactory GetQueryProviderFactory(this IProvider provider, string userID)
    {
        return new ProviderQueryProviderFactory(provider, userID);
    }
    public static IQueryProviderFactory GetQueryProviderFactory(this IStore store)
    {
        return new ProviderQueryProviderFactory(store.Provider, store.UserID);
    }
}