using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Luticate2.Utils.DataAccess.Npgsql;
using Luticate2.Utils.Dbo.Basic;
using Luticate2.Utils.Dbo.Filter;
using Luticate2.Utils.Dbo.OrderBy;
using Luticate2.Utils.Dbo.Result;
using Luticate2.Utils.Interfaces;
using Luticate2.Utils.Utils;
using Microsoft.EntityFrameworkCore;

namespace Luticate2.Utils.DataAccess
{
    public abstract class LuEfCrudDataAccess<TModel, TDboCreate, TDboRead, TDboUpdate, TDbContext> :
            LuEfDataAccess<TModel, TDbContext>, ILuCrudInterface<TDboCreate, TDboRead, TDboUpdate>
        where TModel : class
        where TDboCreate : class
        where TDboRead : class
        where TDboUpdate : class
        where TDbContext : DbContext
    {
        protected LuEfCrudDataAccess(TDbContext db, DbSet<TModel> table) : base(db, table)
        {
        }

        protected abstract TModel GetModelFromTCreate(TDboCreate obj);

        protected abstract void EditModelFromTUpdate(TDboUpdate obj, TModel model);

        protected abstract TDboRead GetDboFromModel(TModel model);

        protected virtual Expression<Func<TModel, object>> GetOrderByFieldExpression(string fieldName)
        {
            fieldName = fieldName.ToSnakeCase();
            if (!typeof(TModel).HasProperty(fieldName))
            {
                return null;
            }
            var param = Expression.Parameter(typeof(TModel), "x");
            var prop = Expression.Property(param, fieldName);
            var converted = Expression.Convert(prop, typeof(object));
            var exp = Expression.Lambda<Func<TModel, object>>(converted, param);
            return exp;
        }

        protected virtual Expression<Func<TModel, bool>> GetFilterExpression(LuFilterDbo filter)
        {
            return model => true;
        }

        protected Func<TDboRead, T> GetIdFunc<T>()
        {
            return x => ((dynamic) x).Id;
        }

        protected TModel GetModelFromTUpdate(TDboUpdate obj, TModel model)
        {
            EditModelFromTUpdate(obj, model);
            return model;
        }

        protected Guid? GetGuid(string id)
        {
            Guid guid;
            if (Guid.TryParse(id, out guid))
            {
                return guid;
            }
            return null;
        }

        protected LuResult<T> GetNotFoundResult<T>()
        {
            return LuResult<T>.Error(LuStatus.NotFound, typeof(TModel).Name + ": Value not found", "");
        }

        protected IQueryable<TModel> GetFilterQuery(LuFilterDbo filter)
        {
            return Table.Where(GetFilterExpression(filter));
        }




        public LuResult<T> Add<T>(IEnumerable<TDboCreate> objs, Func<IEnumerable<TDboRead>, T> returnFunc)
        {
            return Execute(() =>
            {
                var models = objs.Select(GetModelFromTCreate).ToList();
                Table.AddRange(models);
                Db.SaveChanges();
                var dbos = models.Select(GetDboFromModel).ToList();
                var res = returnFunc(dbos);
                return LuResult<T>.Ok(res);
            });
        }

        public LuResult<T> Add<T>(TDboCreate obj, Func<TDboRead, T> returnFunc)
        {
            return Add(new List<TDboCreate> {obj}, list => returnFunc(list.First()));
        }


        public LuResult<IEnumerable<string>> AddGuid(IEnumerable<TDboCreate> objs)
        {
            var func = GetIdFunc<string>();
            return Add(objs, list => list.Select(func));
        }

        public LuResult<string> AddGuid(TDboCreate obj)
        {
            return AddGuid(new List<TDboCreate> {obj}).To(list => list.First());
        }


        public LuResult<IEnumerable<long>> AddId(IEnumerable<TDboCreate> obj)
        {
            var func = GetIdFunc<long>();
            return Add(obj, list => list.Select(func));
        }

        public LuResult<long> AddId(TDboCreate obj)
        {
            return AddId(new List<TDboCreate> {obj}).To(list => list.First());
        }


        public LuResult<IEnumerable<TDboRead>> AddDbo(IEnumerable<TDboCreate> obj)
        {
            return Add(obj, read => read);
        }

        public LuResult<TDboRead> AddDbo(TDboCreate obj)
        {
            return AddDbo(new List<TDboCreate> {obj}).To(list => list.First());
        }




        public LuResult<TDboRead> GetSingle(Expression<Func<TModel, bool>> predicate)
        {
            return Execute(() =>
            {
                var model = Table.FirstOrDefault(predicate);
                if (model == default(TModel))
                {
                    return GetNotFoundResult<TDboRead>();
                }
                var dbo = GetDboFromModel(model);
                return LuResult<TDboRead>.Ok(dbo);
            });
        }

        public LuResult<TDboRead> GetSingleByKeys(params KeyValuePair<string, object>[] keys)
        {
            return GetSingle(GetExpression(keys));
        }

        public LuResult<TDboRead> GetSingleById(string id)
        {
            var guid = GetGuid(id);
            if (guid == null)
            {
                return GetNotFoundResult<TDboRead>();
            }
            return GetSingleByKeys(new KeyValuePair<string, object>("id", guid));
        }

        public LuResult<TDboRead> GetSingleById(long id)
        {
            return GetSingleByKeys(new KeyValuePair<string, object>("id", id));
        }


        public LuResult<LuPaginatedDbo<TDboRead>> GetMultiple(Func<DbSet<TModel>, IOrderedQueryable<TModel>> orderBy,
            Expression<Func<TModel, bool>> predicate, int page = 0, int perPage = int.MaxValue,
            params Func<IOrderedQueryable<TModel>, IOrderedQueryable<TModel>>[] otherOrderBy)
        {
            return Execute(() =>
            {
                var count = Table.Count(predicate);
                var ordered = orderBy(Table);
                foreach (var func in otherOrderBy)
                {
                    ordered = func(ordered);
                }
                var data = ordered.Where(predicate).Skip(page * perPage).Take(perPage).Select(GetDboFromModel).ToList();
                var result = new LuPaginatedDbo<TDboRead>
                {
                    Count = count,
                    Data = data
                };
                return LuResult<LuPaginatedDbo<TDboRead>>.Ok(result);
            });
        }

        public LuResult<LuPaginatedDbo<TDboRead>> GetMultiple(Expression<Func<TModel, object>> orderBy,
            Expression<Func<TModel, bool>> predicate, int page = 0, int perPage = int.MaxValue,
            params Expression<Func<TModel, object>>[] otherOrderBy)
        {
            return GetMultiple(table => table.OrderBy(orderBy), predicate, page, perPage,
                otherOrderBy.Select<Expression<Func<TModel, object>>, Func<IOrderedQueryable<TModel>,
                    IOrderedQueryable<TModel>>>(expression => (t => t.ThenBy(expression))).ToArray());
        }

        public LuResult<LuPaginatedDbo<TDboRead>> GetMultiple(Func<DbSet<TModel>, IOrderedQueryable<TModel>> orderBy,
            int page = 0, int perPage = int.MaxValue, params Func<IOrderedQueryable<TModel>, IOrderedQueryable<TModel>>[] otherOrderBy)
        {
            return GetMultiple(orderBy, x => true, page, perPage, otherOrderBy);
        }

        public LuResult<LuPaginatedDbo<TDboRead>> GetMultiple(Expression<Func<TModel, object>> orderBy,
            int page = 0, int perPage = int.MaxValue, params Expression<Func<TModel, object>>[] otherOrderBy)
        {
            return GetMultiple(orderBy, x => true, page, perPage, otherOrderBy);
        }

        public LuResult<LuPaginatedDbo<TDboRead>> GetMultiple(LuOrderByDbo orderBy, LuFilterDbo filter,
            int page = 0, int perPage = int.MaxValue)
        {
            return Execute(() =>
            {
                var queryable = GetFilterQuery(filter);
                var count = queryable.Count();
                IOrderedQueryable<TModel> ordered = null;
                foreach (var field in orderBy.Fields)
                {
                    var exp = GetOrderByFieldExpression(field.Name);
                    if (exp == null)
                    {
                        return LuResult<LuPaginatedDbo<TDboRead>>.Error(LuStatus.InputError,
                            string.Format("LuEfCrudDataAccess: {0}", field.Name), "Invalid order by field");
                    }
                    if (ordered != null)
                    {
                        ordered = field.Asc ? ordered.ThenBy(exp) : ordered.ThenByDescending(exp);
                    }
                    else
                    {
                        ordered = field.Asc ? queryable.OrderBy(exp) : queryable.OrderByDescending(exp);
                    }
                }

                var data = ordered.Skip(page * perPage).Take(perPage).Select(GetDboFromModel).ToList();
                var result = new LuPaginatedDbo<TDboRead>
                {
                    Count = count,
                    Data = data
                };
                return LuResult<LuPaginatedDbo<TDboRead>>.Ok(result);
            });
        }

        public LuResult<LuPaginatedDbo<TDboRead>> GetMultiple(LuOrderByDbo orderBy, int page = 0, int perPage = int.MaxValue)
        {
            var filter = new LuFilterDbo
            {
                Query = ""
            };
            return GetMultiple(orderBy, filter, page, perPage);
        }



        public LuResult<T> Edit<T>(Expression<Func<TModel, bool>> predicate, Action<TModel> update,
            Func<IEnumerable<TDboRead>, T> returnFunc)
        {
            return Execute(() =>
            {
                var models = Table.Where(predicate);
                var editedDbos = new List<TDboRead>();
                foreach (var model in models)
                {
                    update(model);
                    editedDbos.Add(GetDboFromModel(model));
                    Db.Entry(model).State = EntityState.Modified;
                }
                Db.SaveChanges();
                var res = returnFunc(editedDbos);
                return LuResult<T>.Ok(res);
            });
        }

        public LuResult<IEnumerable<string>> EditGuid(Expression<Func<TModel, bool>> predicate, Action<TModel> update)
        {
            var func = GetIdFunc<string>();
            return Edit(predicate, update, reads => reads.Select(func));
        }

        public LuResult<IEnumerable<long>> EditId(Expression<Func<TModel, bool>> predicate, Action<TModel> update)
        {
            var func = GetIdFunc<long>();
            return Edit(predicate, update, reads => reads.Select(func));
        }

        public LuResult<IEnumerable<TDboRead>> EditDbo(Expression<Func<TModel, bool>> predicate, Action<TModel> update)
        {
            return Edit(predicate, update, read => read);
        }


        public LuResult<T> EditSingleById<T>(long id, Action<TModel> update, Func<TDboRead, T> returnFunc)
        {
            return Edit(GetExpression(new KeyValuePair<string, object>("id", id)), update,
                list => returnFunc(list.FirstOrDefault()));
        }

        public LuResult<long> EditSingleByIdId(long id, Action<TModel> update)
        {
            var func = GetIdFunc<long>();
            return EditSingleById(id, update, func);
        }

        public LuResult<TDboRead> EditSingleByIdDbo(long id, Action<TModel> update)
        {
            return EditSingleById(id, update, read => read);
        }


        public LuResult<T> EditSingleById<T>(string id, Action<TModel> update, Func<TDboRead, T> returnFunc)
        {
            var guid = GetGuid(id);
            if (guid == null)
            {
                return GetNotFoundResult<T>();
            }
            return Edit(GetExpression(new KeyValuePair<string, object>("id", guid)), update,
                list => returnFunc(list.FirstOrDefault()));
        }

        public LuResult<string> EditSingleByIdGuid(string id, Action<TModel> update)
        {
            return EditSingleById(id, update, GetIdFunc<string>());
        }

        public LuResult<TDboRead> EditSingleByIdDbo(string id, Action<TModel> update)
        {
            return EditSingleById(id, update, read => read);
        }


        public LuResult<T> EditSingleById<T>(long id, TDboUpdate update, Func<TDboRead, T> returnFunc)
        {
            return Edit(GetExpression(new KeyValuePair<string, object>("id", id)),
                model => EditModelFromTUpdate(update, model), list => returnFunc(list.FirstOrDefault()));
        }

        public LuResult<long> EditSingleByIdId(long id, TDboUpdate update)
        {
            return EditSingleById(id, update, GetIdFunc<long>());
        }

        public LuResult<TDboRead> EditSingleByIdDbo(long id, TDboUpdate update)
        {
            return EditSingleById(id, update, read => read);
        }


        public LuResult<T> EditSingleById<T>(string id, TDboUpdate update, Func<TDboRead, T> returnFunc)
        {
            var guid = GetGuid(id);
            if (guid == null)
            {
                return GetNotFoundResult<T>();
            }
            return Edit(GetExpression(new KeyValuePair<string, object>("id", guid)),
                model => EditModelFromTUpdate(update, model), list => returnFunc(list.FirstOrDefault()));
        }

        public LuResult<string> EditSingleByIdGuid(string id, TDboUpdate update)
        {
            return EditSingleById(id, update, GetIdFunc<string>());
        }

        public LuResult<TDboRead> EditSingleByIdDbo(string id, TDboUpdate update)
        {
            return EditSingleById(id, update, read => read);
        }




        public LuResult<T> Delete<T>(Expression<Func<TModel, bool>> predicate,
            Func<IEnumerable<TDboRead>, T> returnFunc)
        {
            return Execute(() =>
            {
                var models = Table.Where(predicate).ToList();
                Table.RemoveRange(models);
                Db.SaveChanges();
                var dbos = models.Select(GetDboFromModel);
                return LuResult<T>.Ok(returnFunc(dbos));
            });
        }

        public LuResult<IEnumerable<string>> DeleteGuid(Expression<Func<TModel, bool>> predicate)
        {
            var func = GetIdFunc<string>();
            return Delete(predicate, reads => reads.Select(func));
        }

        public LuResult<IEnumerable<long>> DeleteId(Expression<Func<TModel, bool>> predicate)
        {
            var func = GetIdFunc<long>();
            return Delete(predicate, reads => reads.Select(func));
        }

        public LuResult<IEnumerable<TDboRead>> DeleteDbo(Expression<Func<TModel, bool>> predicate)
        {
            return Delete(predicate, read => read);
        }


        public LuResult<T> DeleteSingleById<T>(string id, Func<TDboRead, T> returnFunc)
        {
            var guid = GetGuid(id);
            if (guid == null)
            {
                return GetNotFoundResult<T>();
            }
            return Delete(GetExpression(new KeyValuePair<string, object>("id", guid)),
                reads => returnFunc(reads.FirstOrDefault()));
        }

        public LuResult<string> DeleteSingleByIdGuid(string id)
        {
            var func = GetIdFunc<string>();
            return DeleteSingleById(id, func);
        }

        public LuResult<TDboRead> DeleteSingleByIdDbo(string id)
        {
            return DeleteSingleById(id, read => read);
        }


        public LuResult<T> DeleteSingleById<T>(long id, Func<TDboRead, T> returnFunc)
        {
            return Delete(GetExpression(new KeyValuePair<string, object>("id", id)),
                reads => returnFunc(reads.First()));
        }

        public LuResult<long> DeleteSingleByIdId(long id)
        {
            var func = GetIdFunc<long>();
            return DeleteSingleById(id, func);
        }

        public LuResult<TDboRead> DeleteSingleByIdDbo(long id)
        {
            return DeleteSingleById(id, read => read);
        }
    }
}