using AsbCloudApp.Data.DetectedOperation;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories;
using AsbCloudApp.Requests;
using AsbCloudApp.Services;
using AsbCloudDb;
using AsbCloudDb.Model;
using Mapster;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AsbCloudInfrastructure.Repository;

public class DetectedOperationRepository : IDetectedOperationRepository
{
    private readonly IAsbCloudDbContext db;
    private readonly ITelemetryService telemetryService;

    public DetectedOperationRepository(
        IAsbCloudDbContext db, 
        ITelemetryService telemetryService)
    {
        this.db = db;
        this.telemetryService = telemetryService;
    }

    public async Task<int> Delete(int idUser, DetectedOperationByTelemetryRequest request, CancellationToken token)
    {
        var query = BuildQuery(request);
        db.Set<DetectedOperation>().RemoveRange(query);
        return await db.SaveChangesAsync(token);
    }

    public async Task<int> DeleteRange(int idUser, IEnumerable<int> ids, CancellationToken token)
    {
        var query = db.Set<DetectedOperation>()
            .Where(e => ids.Contains( e.Id));

        db.Set<DetectedOperation>()
            .RemoveRange(query);

        return await db.SaveChangesAsync(token);
    }

    public async Task<IEnumerable<DetectedOperationDto>> Get(DetectedOperationByTelemetryRequest request, CancellationToken token)
    {
        var query = BuildQuery(request)
            .Include(o => o.OperationCategory);
        var entities = await query.ToArrayAsync(token);
        var offset = telemetryService.GetTimezone(request.IdTelemetry).Offset;
        var dtos = entities.Select(o => Convert(o, offset));

        return dtos;
    }

    public async Task<int> Insert(int? idUser, IEnumerable<DetectedOperationDto> dtos, CancellationToken token)
    {
        if(!dtos.Any())
            return 0;

        var entities = dtos.Select(Convert);
        var dbset = db.Set<DetectedOperation>();
        foreach(var entity in entities)
        {
            entity.Id = default;
            dbset.Add(entity);
        }

        return await db.SaveChangesWithExceptionHandling(token);
    }

    public async Task<int> Update(int idUser, IEnumerable<DetectedOperationDto> dtos, CancellationToken token)
    {
        if (!dtos.Any())
            return 0;

        var ids = dtos
            .Select(o => o.Id)
            .Distinct()
            .ToArray();

        if (ids.Any(id => id == default))
            throw new ArgumentInvalidException(nameof(dtos), "Все записи должны иметь Id");

        if (ids.Length != dtos.Count())
            throw new ArgumentInvalidException(nameof(dtos), "Все записи должны иметь уникальные Id");

        var dbSet = db.Set<DetectedOperation>();
        
        var existingEntitiesCount = await dbSet
            .Where(o => ids.Contains(o.Id))
            .CountAsync(token);

        if (ids.Length != existingEntitiesCount)
            throw new ArgumentInvalidException(nameof(dtos), "Все записи должны существовать в БД");

        var entities = dtos
            .Select(Convert)
            .ToArray();

        var entries = new Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry<DetectedOperation>[entities.Length];
        for(var i = 0; i < entities.Length; i++)
            entries[i] = dbSet.Update(entities[i]);       

        var result = await db.SaveChangesWithExceptionHandling(token);
        
        for (var i = 0; i < entries.Length; i++)
            entries[i].State = EntityState.Detached;

        return result;
    }

    public async Task<int> UpdateOrInsert(int idUser, IEnumerable<DetectedOperationDto> dtos, CancellationToken token)
    {
        var result = 0;

        var itemsToInsert = dtos.Where(e => e.Id == 0);
        if (itemsToInsert.Any())
            result += await Insert(idUser, itemsToInsert, token);

        var itemsToUpdate = dtos.Where(e => e.Id != 0);
        if (itemsToUpdate.Any())
            result += await Update(idUser, itemsToUpdate, token);

        return result;
    }

    private IQueryable<DetectedOperation> BuildQuery(DetectedOperationByTelemetryRequest request)
    {
        var query = db.Set<DetectedOperation>()
            .Where(o => o.IdTelemetry == request.IdTelemetry);

        if (request.IdsCategories.Any())
            query = query.Where(o => request.IdsCategories.Contains(o.IdCategory));

        if (request.GeDepthStart is not null)
            query = query.Where(o => o.DepthStart >= request.GeDepthStart);

        if (request.LeDepthEnd is not null)
            query = query.Where(o => o.DepthEnd <= request.LeDepthEnd);

        if (request.GeDateStart is not null)
        {
            var geDate = request.GeDateStart.Value.ToUniversalTime();
            query = query.Where(o => o.DateStart >= geDate);
        }
        
        if (request.LeDateEnd is not null)
        {
            var leDate = request.LeDateEnd.Value.ToUniversalTime();
            query = query.Where(o => o.DateEnd <= leDate);
        }

        if (request.SortFields?.Any() == true)
        {
            query = query.SortBy(request.SortFields);
        }
        else
            query = query
                .OrderBy(o => o.DateStart)
                .ThenBy(o => o.DepthStart);

        if (request.Skip.HasValue)
            query = query.Skip((int)request.Skip);

        if (request.Take.HasValue)
            query = query.Take((int)request.Take);

        return query;
    }

    private static DetectedOperationDto Convert(DetectedOperation entity, TimeSpan offset)
    {
        var dto = entity.Adapt<DetectedOperationDto>();
        dto.DateStart = entity.DateStart.ToOffset(offset);
        dto.DateEnd = entity.DateEnd.ToOffset(offset);
        return dto;
    }

    private static DetectedOperation Convert(DetectedOperationDto dto)
    {
        var entity = dto.Adapt<DetectedOperation>();
        entity.DateStart = dto.DateStart.ToUniversalTime();
        entity.DateEnd = dto.DateEnd.ToUniversalTime();
        return entity;
    }
}