using Microsoft.EntityFrameworkCore;
using DD.Persistence.Database.Entity;
using DD.Persistence.Models;
using DD.Persistence.Repositories;

namespace DD.Persistence.Repository.Repositories;

/// <summary>
/// Репозиторий для хранения разных наборов данных временных рядов.
/// idDiscriminator - идентифицирует конкретный набор данных, прим.: циклы измерения АСИБР, или отчет о DrillTest.
/// idDiscriminator формируют клиенты и только им известно что они обозначают.
/// Так как данные приходят редко, то их прореживания для построения графиков не предусмотрено.
/// </summary>
public class TimestampedSetRepository : ITimestampedSetRepository
{
    private readonly DbContext db;

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

    public Task<int> AddRange(Guid idDiscriminator, IEnumerable<TimestampedSetDto> sets, CancellationToken token)
    {
        var entities = sets.Select(set => new TimestampedSet(idDiscriminator, set.Timestamp.ToUniversalTime(), set.Set));
        var dbSet = db.Set<TimestampedSet>();
        dbSet.AddRange(entities);
        return db.SaveChangesAsync(token);
    }

    public async Task<IEnumerable<TimestampedSetDto>> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
    {
        var dbSet = db.Set<TimestampedSet>();
        var query = dbSet.Where(entity => entity.IdDiscriminator == idDiscriminator);

        if (geTimestamp.HasValue)
            query = ApplyGeTimestamp(query, geTimestamp.Value);

        query = query
            .OrderBy(item => item.Timestamp)
            .Skip(skip)
            .Take(take);

        var data = await Materialize(query, token);

        if (columnNames is not null && columnNames.Any())
            data = ReduceSetColumnsByNames(data, columnNames);

        return data;
    }

    public async Task<IEnumerable<TimestampedSetDto>> GetLast(Guid idDiscriminator, IEnumerable<string>? columnNames, int take, CancellationToken token)
    {
        var dbSet = db.Set<TimestampedSet>();
        var query = dbSet.Where(entity => entity.IdDiscriminator == idDiscriminator);

        query = query.OrderByDescending(entity => entity.Timestamp)
            .Take(take)
            .OrderBy(entity => entity.Timestamp);

        var data = await Materialize(query, token);

        if (columnNames is not null && columnNames.Any())
            data = ReduceSetColumnsByNames(data, columnNames);

        return data;
    }

    public Task<int> Count(Guid idDiscriminator, CancellationToken token)
    {
        var dbSet = db.Set<TimestampedSet>();
        var query = dbSet.Where(entity => entity.IdDiscriminator == idDiscriminator);
        return query.CountAsync(token);
    }

    public async Task<DatesRangeDto?> GetDatesRange(Guid idDiscriminator, CancellationToken token)
    {
        var query = db.Set<TimestampedSet>()
            .GroupBy(entity => entity.IdDiscriminator)
            .Select(group => new
            {
                Min = group.Min(entity => entity.Timestamp),
                Max = group.Max(entity => entity.Timestamp),
            });

        var item = await query.FirstOrDefaultAsync(token);
        if (item is null)
            return null;

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

    private static async Task<IEnumerable<TimestampedSetDto>> Materialize(IQueryable<TimestampedSet> query, CancellationToken token)
    {
        var dtoQuery = query.Select(entity => new TimestampedSetDto(entity.Timestamp, entity.Set));
        var dtos = await dtoQuery.ToArrayAsync(token);
        return dtos;
    }

    private static IQueryable<TimestampedSet> ApplyGeTimestamp(IQueryable<TimestampedSet> query, DateTimeOffset geTimestamp)
    {
        var geTimestampUtc = geTimestamp.ToUniversalTime();
        return query.Where(entity => entity.Timestamp >= geTimestampUtc);
    }

    private static IEnumerable<TimestampedSetDto> ReduceSetColumnsByNames(IEnumerable<TimestampedSetDto> query, IEnumerable<string> columnNames)
    {
        var newQuery = query
            .Select(entity => new TimestampedSetDto(
                entity.Timestamp,
                entity.Set
                    .Where(prop => columnNames.Contains(prop.Key))
                    .ToDictionary(prop => prop.Key, prop => prop.Value)
            ));
        return newQuery;
    }
}