using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.Extensions.Logging;
using PARR.Common.Domain;
using PARR.DAL.Context;
using PARR.DAL.DomainModels;
using PARR.DAL.Models.Base;
using PARR.DAL.Models.Base.History.Base;
using PARR.DAL.Services.Interfaces.Base;
using System.Reflection;

namespace PARR.DAL.Services.Abstracts
{
    internal abstract class BaseService<T> : IBaseService<T> where T : class, IBase
    {
        private readonly ILogger<BaseService<T>> logger;

        protected abstract DbSet<T> EntitySet { get; }
        protected abstract DataContext EntitiContext { get; }

        public BaseService(ILogger<BaseService<T>> logger)
        {
            this.logger = logger;
        }

        public virtual async Task<bool> AddRangeAsync(List<T> objs)
        {
            objs.ForEach(item => item.DateCreated = DateTimeOffset.UtcNow);

            try
            {
                await EntitySet.AddRangeAsync(objs);
                return true;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Ошибка при добавлении диапазона в БД.");
                return false;
            }
        }

        public async Task<bool> CommitAsync(IHistoryInitiator? initiator = null)
        {
            #region Изменения
            var modifiedEntrities = EntitiContext.ChangeTracker.Entries()
                .Where(t => t.State == EntityState.Modified/* || t.State == EntityState.Deleted*/);

            foreach (var obj in modifiedEntrities)
            {
                // Если нунжо, обновляем дату изменения
                DateModifiedResolver(obj);

                //Если нужно, пишем историю
                TableHistoryResolver(obj);
            }
            #endregion

            SetInitiator(initiator);

            try
            {
                await EntitiContext.SaveChangesAsync();
                return true;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Ошибка при Commit");
                return false;
            }
        }


        /// <summary>
        /// Установить инициатора
        /// </summary>
        /// <param name="initiator"></param>
        private void SetInitiator(IHistoryInitiator? initiator)
        {
            // Задаем инициатора только для новых и измененных записей
            var entrities = EntitiContext.ChangeTracker.Entries()
               .Where(t => t.State == EntityState.Modified || t.State == EntityState.Added);

            // смотрим есть ли у объекта интерфейс IHistoryInitiator, если есть, задаём значения
            foreach (var obj in entrities)
            {
                // для всех объектов которые наследуются от IHistoryInitiator и не являются наследниками IHistoryTable
                if (obj.Entity is IHistoryInitiator && obj.Entity is IHistoryTable == false)
                {
                    (obj.Entity as IHistoryInitiator)!.InitiatorIp = initiator?.InitiatorIp ?? null;
                    (obj.Entity as IHistoryInitiator)!.InitiatorParrComponentId = initiator?.InitiatorParrComponentId ?? null;
                    (obj.Entity as IHistoryInitiator)!.InitiatorComment = initiator?.InitiatorComment ?? null;
                }
            }
        }


        /// <summary>
        /// Обновления DateModified у таблиц с IBase
        /// </summary>
        /// <param name="obj"></param>
        private void DateModifiedResolver(EntityEntry obj)
        {
            //if (obj.Entity is IBase)
            //    (obj.Entity as IBase)!.DateModified = DateTimeOffset.UtcNow;

            if (obj.Entity is IBaseDateModified)
                (obj.Entity as IBaseDateModified)!.DateModified = DateTimeOffset.UtcNow;
        }

        /// <summary>
        /// При необходимости, записывать историю таблиц 
        /// </summary>
        /// <param name="obj"></param>
        private void TableHistoryResolver(EntityEntry obj)
        {
            var myHistoryInterface = obj.Entity.GetType().GetInterfaces()
                                            .Where(t => t.IsGenericType)
                                            .Where(t => t.GetGenericTypeDefinition() == typeof(IMyHistory<>))
                                            .FirstOrDefault();

            // у этого объекта нет интерфейса IMyHistory<>. Не ведем историю
            if (myHistoryInterface == null)
                return;

            // !!! Эта таблица хочет хранить историю !!!

            // Получаем тип таблицы где хранится история
            var historyType = myHistoryInterface.GetGenericArguments().First();
            var historyProps = historyType.GetProperties(/*BindingFlags.DeclaredOnly | */ /*BindingFlags.Public*/).ToList();

            var historyInstance = Activator.CreateInstance(historyType);
            if (historyInstance == null)
            {
                logger.LogError($"Не смог создать инстанс для ведения истории {historyType.Name}");
                return;
            }

            // Заполняем поля истории, полями которые есть в исходной таблице
            FillHistoryProps(obj, ref historyInstance, historyProps);

            // Заполняем поля интерфейса IHistoryTable
            FillHistoryProp(ref historyInstance, nameof(IHistoryTable.DateAddedToHistory), DateTimeOffset.UtcNow);

            // Заполняем Id
            FillHistoryProp(ref historyInstance, nameof(IBase.Id), Guid.NewGuid());

            try
            {
                EntitiContext.Add(historyInstance);
            }
            catch (Exception ex)
            {
                logger.LogError(ex, $"Ошибка при добавлении объекта в историю {historyType.Name}");
            }
        }


        /// <summary>
        /// Заполнить объект истории
        /// </summary>
        /// <param name="originalObj"></param>
        /// <param name="historyInstance"></param>
        /// <param name="propsList"></param>
        private void FillHistoryProps(EntityEntry originalObj, ref object historyInstance, List<PropertyInfo> propsList)
        {
            foreach (var prop in propsList)
            {
                if (originalObj.OriginalValues.Properties.FirstOrDefault(t => t.Name == prop.Name) == null)
                    continue;

                var sourceObjProp = originalObj.Property(prop.Name);
                if (sourceObjProp == null)
                    return;

                //Смотрим, если это Id, записываем его в ParentId
                var histPropName = prop.Name == nameof(IBase.Id) ? nameof(IHistoryTable.ParentId) : prop.Name;

                var histProp = historyInstance.GetType().GetProperty(histPropName);
                if (histProp == null) continue;

                //сравним типы
                if (sourceObjProp.Metadata.PropertyInfo?.PropertyType != histProp.PropertyType)
                    continue;

                var origValues = originalObj.Property(prop.Name).OriginalValue;
                histProp.SetValue(historyInstance, origValues);
            }
        }


        /// <summary>
        /// Заполнить поле объекта
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="instanceObj"></param>
        /// <param name="propName"></param>
        /// <param name="value"></param>
        private void FillHistoryProp<TValue>(ref object instanceObj, string propName, TValue value)
        {
            var histProp = instanceObj.GetType().GetProperty(propName);
            if (histProp == null)
            {
                logger.LogError($"При изменении объекта для БД, не найдено свойство ${propName}");
                return;
            }

            // сравним типы
            if (histProp.PropertyType != typeof(TValue))
            {
                logger.LogError($"При изменении объекта для БД, не совпадают типы у свойства {propName}, {histProp.PropertyType.Name}!={typeof(TValue).Name}");
                return;
            }

            histProp.SetValue(instanceObj, value);
        }


        #region test, comments
        //private void SaveHistory(EntityEntry obj)
        //{
        //    // пока просто попробуем для шаблонов
        //    if (obj.Entity is Template == false)
        //        return;

        //    var template = (Template)obj.Entity;
        //    // https://stackoverflow.com/questions/15012621/how-to-get-original-entity-from-changetracker
        //    var history = new TemplateHistory();
        //    // {
        //    history.Id = Guid.NewGuid();
        //    history.ParentId = template.Id;
        //    history.DateAddedToHistory = DateTimeOffset.UtcNow;
        //    history.InitiatorIp = null;
        //    history.InitiatorParrComponentId = null;
        //    history.InitiatorComment = null;
        //    //---
        //    //DateModified="",
        //    //Name ="",
        //    //IsActiveTemplate = null,
        //    //IsActiveSchedule = null,
        //    //LastRun = null,
        //    //NextRun = null

        //    //    DateModified = template.DateModified,
        //    //    Name = template.Name,
        //    //    IsActiveTemplate = template.IsActiveTemplate,
        //    //    IsActiveSchedule = template.IsActiveSchedule,
        //    //    LastRun = template.LastRun,
        //    //    NextRun = template.NextRun
        //    //};

        //    // obj.OriginalValues.GetValue

        //    //ITemplateBase  хочу получить все поля из интерфейса, потом заполнить по ним в хистори
        //    var interfaceType = typeof(ITemplateGeneralProps);
        //    foreach (var prop in interfaceType.GetProperties())
        //    {
        //        var propName = prop.Name;
        //        //var propType = prop.PropertyType;   

        //        var originalPropValue = obj.OriginalValues.GetValue<object>(propName);

        //        var histProp = history.GetType().GetProperty(propName);
        //        histProp.SetValue(histProp, originalPropValue);
        //    }


        //    EntitiContext.Add(history);


        //}
        #endregion


        public virtual async Task<bool> CreateAsync(T obj)
        {
            obj.DateCreated = DateTimeOffset.UtcNow;

            try
            {
                await EntitySet.AddAsync(obj);
                return true;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Ошибка при добавлении в БД.");
                return false;
            }
        }

        public virtual bool Delete(T obj)
        {
            try
            {
                EntitySet.Remove(obj);
                return true;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Ошибка при удалении из БД.");
                return false;
            }
        }

        public virtual async Task<bool> DeleteAsync(Guid id)
        {
            try
            {
                var exist = await GetAsync(id);
                if (exist == null)
                {
                    logger.LogError($"Ошибка при удалении из БД. Не найдена запись в БД с id: {id}");
                    return false;
                }

                EntitySet.Remove(exist);
                return true;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Ошибка при удалении из БД.");
                return false;
            }
        }

        public virtual IQueryable<T> Get()
        {
            return EntitySet;
        }

        public virtual async Task<T?> GetAsync(Guid id)
        {
            return await EntitySet.FirstOrDefaultAsync(t => t.Id == id);
        }

        public virtual IQueryable<T> GetPage(IQueryable<T> query, PaginationFilter paginationFilter)
        {
            //if (paginationFilter.IsDisabledPagination)
            //    return query;


            int skip = (paginationFilter.PageNumber - 1) * paginationFilter.PageSize;

            return query.Skip(skip).Take(paginationFilter.PageSize);
        }
    }
}
