using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Base;
using PARR.DAL.Context;
using PARR.Domain.Common.Pagination;
using PARR.Domain.Entities.Attributes;
using PARR.Domain.Entities.Base;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Base.History.Base;
using System.Reflection;

namespace PARR.DAL.Repositories.Base
{
    internal abstract class BaseRepository<T> : IBaseRepository<T> where T : class, IBaseEntity
    {
        //private readonly ILogger<BaseRepository<T>> logger;

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

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

        protected readonly ILogger logger;
        protected readonly DbSet<T> EntitySet;
        protected readonly DataContext EntityContext;

        protected BaseRepository(ILogger logger, DataContext dataContext)
        {
            this.logger = logger;
            this.EntityContext = dataContext;
            this.EntitySet = dataContext.Set<T>();
        }


        public virtual async Task<bool> AddRangeAsync(List<T> objs)
        {
            logger.LogDebug("Начинаю добавление диапазона объектов типа {EntityType}, количество: {Count}",
                typeof(T).Name, objs.Count);

            objs.ForEach(item => item.DateCreated = DateTimeOffset.UtcNow);

            try
            {
                await EntitySet.AddRangeAsync(objs);
                logger.LogDebug("Успешно добавлено {Count} объектов типа {EntityType}",
                    objs.Count, typeof(T).Name);
                return true;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Ошибка при добавлении диапазона объектов типа {EntityType}", typeof(T).Name);
                return false;
            }
        }

        public async Task<bool> CommitAsync(IHistoryInitiator? initiator = null)
        {
            logger.LogDebug("Начинаю сохранение изменений в БД для объектов типа {EntityType}", typeof(T).Name);

            #region Изменения
            var modifiedEntrities = EntityContext.ChangeTracker.Entries()
                .Where(t => t.State == EntityState.Modified/* || t.State == EntityState.Deleted*/);

            logger.LogDebug("Найдено {Count} измененных сущностей для обработки истории", modifiedEntrities.Count());

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

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

            SetInitiator(initiator);

            try
            {
                var changedCount = await EntityContext.SaveChangesAsync();
                logger.LogDebug("Успешно сохранено {ChangedCount} изменений в БД для объектов типа {EntityType}",
                    changedCount, typeof(T).Name);
                return true;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Ошибка при сохранении изменений в БД для объектов типа {EntityType}", typeof(T).Name);
                return false;
            }
        }


        /// <summary>
        /// Установить инициатора
        /// </summary>
        /// <param name="initiator"></param>
        private void SetInitiator(IHistoryInitiator? initiator)
        {
            if (initiator == null)
                return;

            logger.LogDebug("Устанавливаю инициатора для изменений");

            // Задаем инициатора только для новых и измененных записей
            var entrities = EntityContext.ChangeTracker.Entries()
               .Where(t => t.State == EntityState.Modified || t.State == EntityState.Added);

            var entityCount = entrities.Count();
            logger.LogDebug("Найдено {Count} сущностей для установки инициатора", entityCount);

            // смотрим есть ли у объекта интерфейс 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;

                    logger.LogDebug("Установлен инициатор для сущности типа {EntityType}", obj.Entity.GetType().Name);
                }
            }
        }


        /// <summary>
        /// Обновления DateModified у таблиц с IBase
        /// </summary>
        /// <param name="obj"></param>
        private void DateModifiedResolver(EntityEntry obj)
        {
            if (obj.Entity is IBaseEntityDateModified entity)
            {
                //logger.LogDebug("Обновляю DateModified для сущности типа {EntityType}", obj.Entity.GetType().Name);
                //(obj.Entity as IBaseEntityDateModified)!.DateModified = DateTimeOffset.UtcNow;

                var entityType = entity.GetType();

                //Ищем DateModified
                var properyInfo = entityType.GetProperty(nameof(IBaseEntityDateModified.DateModified));

                // Проверяем наличие атрибута ManualControlAttribute на этом свойстве
                bool isManual = properyInfo?.GetCustomAttributes(typeof(ManualControlAttribute), false).Any() ?? false;

                if (!isManual)
                {
                    logger.LogDebug("Обновляю DateModified для сущности типа {EntityType}", obj.Entity.GetType().Name);
                    entity.DateModified = DateTimeOffset.UtcNow;
                }
                else
                {
                    logger.LogDebug("Пропуск обновления DateModified (ManualControl) для {EntityType}", entityType.Name);
                }
            }
        }

        /// <summary>
        /// При необходимости, записывать историю таблиц 
        /// </summary>
        /// <param name="obj"></param>
        private void TableHistoryResolver(EntityEntry obj)
        {
            logger.LogDebug("Проверяю необходимость создания истории для сущности типа {EntityType}", obj.Entity.GetType().Name);

            var myHistoryInterface = obj.Entity.GetType().GetInterfaces()
                                            .Where(t => t.IsGenericType)
                                            .Where(t => t.GetGenericTypeDefinition() == typeof(IMyHistory<>))
                                            .FirstOrDefault();

            // у этого объекта нет интерфейса IMyHistory<>. Не ведем историю
            if (myHistoryInterface == null)
            {
                logger.LogDebug("Сущность типа {EntityType} не требует ведения истории", obj.Entity.GetType().Name);
                return;
            }

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

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

            logger.LogDebug("Создаю историю для сущности типа {EntityType}, тип истории: {HistoryType}",
                obj.Entity.GetType().Name, historyType.Name);

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

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

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

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

            try
            {
                EntityContext.Add(historyInstance);
                logger.LogDebug("История добавлена для сущности типа {EntityType}", obj.Entity.GetType().Name);
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Ошибка при добавлении объекта в историю {HistoryType}", 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)
        {
            logger.LogDebug("Заполняю историю для сущности типа {EntityType}", originalObj.Entity.GetType().Name);

            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(IBaseEntity.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);
            }

            logger.LogDebug("Завершено заполнение истории для сущности типа {EntityType}", originalObj.Entity.GetType().Name);
        }


        /// <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("При изменении объекта для БД, не найдено свойство {PropertyName}", propName);
                return;
            }

            // сравним типы
            if (histProp.PropertyType != typeof(TValue))
            {
                logger.LogError("При изменении объекта для БД, не совпадают типы у свойства {PropertyName}, {PropertyType}!={ValueType}",
                    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)
        {
            logger.LogDebug("Начинаю создание объекта типа {EntityType}", typeof(T).Name);

            if (obj.DateCreated == DateTimeOffset.MinValue)
                obj.DateCreated = DateTimeOffset.UtcNow;

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

        public virtual bool Delete(T obj)
        {
            logger.LogDebug("Начинаю удаление объекта типа {EntityType}", obj.GetType().Name);

            try
            {
                EntitySet.Remove(obj);
                logger.LogDebug("Объект типа {EntityType} удален из контекста", obj.GetType().Name);
                return true;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Ошибка при удалении объекта типа {EntityType} из БД", obj.GetType().Name);
                return false;
            }
        }

        public virtual async Task<bool> DeleteAsync(Guid id)
        {
            logger.LogDebug("Начинаю удаление объекта типа {EntityType} по ID: {Id}", typeof(T).Name, id);

            try
            {
                var exist = await GetAsync(id);
                if (exist == null)
                {
                    logger.LogError("Ошибка при удалении из БД. Не найдена запись в БД типа {EntityType} с id: {Id}",
                        typeof(T).Name, id);
                    return false;
                }

                EntitySet.Remove(exist);
                logger.LogDebug("Объект типа {EntityType} с ID {Id} удален из контекста", typeof(T).Name, id);
                return true;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Ошибка при удалении объекта типа {EntityType} из БД по ID: {Id}", typeof(T).Name, id);
                return false;
            }
        }

        public virtual IQueryable<T> Get()
        {
            logger.LogDebug("Получаю набор объектов типа {EntityType}", typeof(T).Name);
            return EntitySet;
        }

        public virtual async Task<T?> GetAsync(Guid id)
        {
            logger.LogDebug("Получаю объект типа {EntityType} по ID: {Id}", typeof(T).Name, id);
            return await EntitySet.FirstOrDefaultAsync(t => t.Id == id);
        }

        public virtual IQueryable<T> GetPage(IQueryable<T> query, PaginationFilter paginationFilter)
        {
            logger.LogDebug("Получаю страницу объектов типа {EntityType}, страница: {PageNumber}, размер: {PageSize}",
                typeof(T).Name, paginationFilter.PageNumber, paginationFilter.PageSize);

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

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