using Mapster;
using Microsoft.EntityFrameworkCore;
using DD.Persistence.Database.Model;
using DD.Persistence.Models;
using DD.Persistence.Models.Requests;
using DD.Persistence.Repositories;
using UuidExtensions;

namespace DD.Persistence.Repository.Repositories;
public class ChangeLogRepository : IChangeLogRepository
{
    private readonly DbContext db;

    public ChangeLogRepository(DbContext db)
    {
        this.db = db;
    }

    public async Task<int> AddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token)
    {
        var entities = new List<ChangeLog>();
        foreach (var dto in dtos)
        {
            var entity = CreateEntityFromDto(idAuthor, idDiscriminator, dto);
            entities.Add(entity);
        }
        db.Set<ChangeLog>().AddRange(entities);

        var result = await db.SaveChangesAsync(token);

        return result;
    }

    public async Task<int> MarkAsDeleted(Guid idEditor, IEnumerable<Guid> ids, CancellationToken token)
    {
        var query = db.Set<ChangeLog>()
            .Where(s => ids.Contains(s.Id))
            .Where(s => s.Obsolete == null);

        if (query.Count() != ids.Count())
        {
            throw new ArgumentException("Count of active items not equal count of ids", nameof(ids));
        }

        var entities = await query.ToArrayAsync(token);

        var result = await MarkAsObsolete(idEditor, entities, token);

        return result;
    }

    public async Task<int> MarkAsDeleted(Guid idEditor, Guid idDiscriminator, CancellationToken token)
    {
        var query = db.Set<ChangeLog>()
            .Where(s => s.IdDiscriminator == idDiscriminator)
            .Where(e => e.Obsolete == null);

        var entities = await query.ToArrayAsync(token);

        var result = await MarkAsObsolete(idEditor, entities, token);

        return result;
    }

    private async Task<int> MarkAsObsolete(Guid idEditor, IEnumerable<ChangeLog> entities, CancellationToken token)
    {
        var updateTime = DateTimeOffset.UtcNow;

        foreach (var entity in entities)
        {
            entity.Obsolete = updateTime;
            entity.IdEditor = idEditor;
        }

        return await db.SaveChangesAsync(token);
    }

    public async Task<int> ClearAndAddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token)
    {
        var result = 0;

        using var transaction = await db.Database.BeginTransactionAsync(token);

        result += await MarkAsDeleted(idAuthor, idDiscriminator, token);
        result += await AddRange(idAuthor, idDiscriminator, dtos, token);

        await transaction.CommitAsync(token);


        return result;
    }

    public async Task<int> UpdateRange(Guid idEditor, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token)
    {
        var dbSet = db.Set<ChangeLog>();

        var updatedIds = dtos.Select(d => d.Id);
        var updatedEntities = dbSet
            .Where(s => updatedIds.Contains(s.Id))
            .ToDictionary(s => s.Id);

        var result = 0;
        using var transaction = await db.Database.BeginTransactionAsync(token);

        foreach (var dto in dtos)
        {
            var updatedEntity = updatedEntities.GetValueOrDefault(dto.Id);
            if (updatedEntity is null)
            {
                throw new ArgumentException($"Entity with id = {dto.Id} doesn't exist in Db", nameof(dto));
            }

            var newEntity = CreateEntityFromDto(idEditor, updatedEntity.IdDiscriminator, dto);
            dbSet.Add(newEntity);

            updatedEntity.IdNext = newEntity.Id;
            updatedEntity.Obsolete = DateTimeOffset.UtcNow;
            updatedEntity.IdEditor = idEditor;
        }

        result = await db.SaveChangesAsync(token);
        await transaction.CommitAsync(token);

        return result;


    }

    public async Task<PaginationContainer<DataWithWellDepthAndSectionDto>> GetByDate(
       Guid idDiscriminator,
       DateTimeOffset momentUtc,
       SectionPartRequest filterRequest,
       PaginationRequest paginationRequest,
       CancellationToken token)
    {
        var query = CreateQuery(idDiscriminator);
        query = query.Apply(momentUtc);
        query = query.Apply(filterRequest);

        var result = await query.ApplyPagination(paginationRequest, Convert, token);

        return result;
    }

    private IQueryable<ChangeLog> CreateQuery(Guid idDiscriminator)
    {
        var query = db.Set<ChangeLog>().Where(e => e.IdDiscriminator == idDiscriminator);

        return query;
    }

    public async Task<IEnumerable<ChangeLogDto>> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token)
    {
        var query = db.Set<ChangeLog>().Where(s => s.IdDiscriminator == idDiscriminator);

        var min = new DateTimeOffset(dateBegin.ToUniversalTime().Date, TimeSpan.Zero);
        var max = new DateTimeOffset(dateEnd.ToUniversalTime().Date, TimeSpan.Zero);

        var createdQuery = query.Where(e => e.Creation >= min && e.Creation <= max);
        var editedQuery = query.Where(e => e.Obsolete != null && e.Obsolete >= min && e.Obsolete <= max);

        query = createdQuery.Union(editedQuery);
        var entities = await query.ToArrayAsync(token);

        var dtos = entities.Select(e => e.Adapt<ChangeLogDto>());

        return dtos;
    }



    public async Task<IEnumerable<DateOnly>> GetDatesChange(Guid idDiscriminator, CancellationToken token)
    {
        var query = db.Set<ChangeLog>().Where(e => e.IdDiscriminator == idDiscriminator);

        var datesCreateQuery = query
            .Select(e => e.Creation)
            .Distinct();

        var datesCreate = await datesCreateQuery.ToArrayAsync(token);

        var datesUpdateQuery = query
            .Where(e => e.Obsolete != null)
            .Select(e => e.Obsolete!.Value)
            .Distinct();

        var datesUpdate = await datesUpdateQuery.ToArrayAsync(token);

        var dates = Enumerable.Concat(datesCreate, datesUpdate);
        var datesOnly = dates
            .Select(d => new DateOnly(d.Year, d.Month, d.Day))
            .Distinct()
            .OrderBy(d => d);

        return datesOnly;
    }

    private static ChangeLog CreateEntityFromDto(Guid idAuthor, Guid idDiscriminator, DataWithWellDepthAndSectionDto dto)
    {
        var entity = new ChangeLog()
        {
            Id = Uuid7.Guid(),
            Creation = DateTimeOffset.UtcNow,
            IdAuthor = idAuthor,
            IdDiscriminator = idDiscriminator,
            IdEditor = idAuthor,

            Value = dto.Value,
            IdSection = dto.IdSection,
            DepthStart = dto.DepthStart,
            DepthEnd = dto.DepthEnd,
        };

        return entity;
    }

    public async Task<IEnumerable<DataWithWellDepthAndSectionDto>> GetGtDate(Guid idDiscriminator, DateTimeOffset dateBegin, CancellationToken token)
    {
        var date = dateBegin.ToUniversalTime();
        var query = this.db.Set<ChangeLog>()
            .Where(e => e.IdDiscriminator == idDiscriminator)
            .Where(e => e.Creation >= date || e.Obsolete >= date);

        var entities = await query.ToArrayAsync(token);

        var dtos = entities.Select(Convert);

        return dtos;
    }

    public async Task<DatesRangeDto?> GetDatesRange(Guid idDiscriminator, CancellationToken token)
    {
        var query = db.Set<ChangeLog>()
            .Where(e => e.IdDiscriminator == idDiscriminator)
            .GroupBy(e => 1)
            .Select(group => new
            {
                Min = group.Min(e => e.Creation),
                Max = group.Max(e => (e.Obsolete.HasValue && e.Obsolete > e.Creation)
                    ? e.Obsolete.Value
                    : e.Creation),
            });

        var values = await query.FirstOrDefaultAsync(token);

        if (values is null)
        {
            return null;
        }

        return new DatesRangeDto
        {
            From = values.Min,
            To = values.Max,
        };
    }

    private DataWithWellDepthAndSectionDto Convert(ChangeLog entity) => entity.Adapt<DataWithWellDepthAndSectionDto>();
}