using AsbCloudApp.Data;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories;
using AsbCloudApp.Requests;
using AsbCloudApp.Services;
using AsbCloudDb.Model;
using AsbCloudInfrastructure.Repository;
using Mapster;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AsbCloudInfrastructure.Services
{
    public class WellService : CrudCacheRepositoryBase<WellDto, Well>, IWellService
    {
        private readonly ITelemetryService telemetryService;
        private readonly ICrudRepository<CompanyTypeDto> companyTypesService;
        private readonly ITimezoneService timezoneService;
        private readonly WellInfoService wellInfoService;
        private readonly IWellOperationRepository wellOperationRepository;

        public ITelemetryService TelemetryService => telemetryService;

        private static IQueryable<Well> MakeQueryWell(DbSet<Well> dbSet)
            => dbSet
                .Include(w => w.Cluster)
                    .ThenInclude(c => c.Deposit)
                .Include(w => w.Telemetry)
                .Include(w => w.WellType)
                .Include(w => w.RelationCompaniesWells)
                .ThenInclude(r => r.Company)
                .AsNoTracking();

        public WellService(IAsbCloudDbContext db, IMemoryCache memoryCache, ITelemetryService telemetryService, ITimezoneService timezoneService, WellInfoService wellInfoService)
            : base(db, memoryCache, MakeQueryWell)
        {
            this.telemetryService = telemetryService;
            this.timezoneService = timezoneService;
            this.wellInfoService = wellInfoService;
            this.wellOperationRepository = new WellOperationRepository(db, memoryCache, this);
            companyTypesService = new CrudCacheRepositoryBase<CompanyTypeDto, CompanyType>(dbContext, memoryCache);
        }

        private Task<IEnumerable<RelationCompanyWell>> GetCacheRelationCompanyWellAsync(CancellationToken token)
        {
            return memoryCache.GetOrCreateBasicAsync(
                dbContext.Set<RelationCompanyWell>()
                    .Include(r => r.Company)
                    .Include(r => r.Well)
                , token);
        }

        private void DropCacheRelationCompanyWell()
            => memoryCache.DropBasic<RelationCompanyWell>();

        public DateTime GetLastTelemetryDate(int idWell)
        {
            var well = GetOrDefault(idWell);

            if (well?.IdTelemetry is null)
                return DateTime.MinValue;

            var datesRange = telemetryService.GetDatesRange(well.IdTelemetry.Value);
            return datesRange.To;
        }

        /// <inheritdoc/>
        public async Task<IEnumerable<DepositBranchDto>> GetWellTreeAsync(int idCompany, CancellationToken token)
        {
            var wells = await GetEntitiesAsync(new() { IdCompany = idCompany }, token);

            var groupedWells = wells
                .GroupBy(w => w.Cluster)
                .GroupBy(g => g.Key.Deposit);

            var depositTree = groupedWells.Select(
                gDeposit => new DepositBranchDto
                {
                    Id = gDeposit.Key.Id,
                    Caption = gDeposit.Key.Caption,
                    Latitude = gDeposit.Key.Latitude,
                    Longitude = gDeposit.Key.Longitude,
                    Clusters = gDeposit.Select(gCluster => new ClusterBranchDto
                    {
                        Id = gCluster.Key.Id,
                        Caption = gCluster.Key.Caption,
                        Latitude = gCluster.Key.Latitude ?? gDeposit.Key.Latitude,
                        Longitude = gCluster.Key.Longitude ?? gDeposit.Key.Longitude,
                        Wells = gCluster.Select(well =>
                        {
                            var dto = wellInfoService.FirstOrDefault(w => w.Id == well.Id && well.IdState == 1);
                            dto ??= well.Adapt<WellMapInfoWithTelemetryStat>();
                            dto.Latitude ??= gCluster.Key.Latitude ?? gDeposit.Key.Latitude;
                            dto.Longitude ??= gCluster.Key.Longitude ?? gDeposit.Key.Longitude;
                            return dto;
                        }),
                    }),
                });
            return depositTree;
        }

        public async Task<WellMapInfoWithTelemetryStat?> GetOrDefaultStatAsync(int idWell, CancellationToken token)
        {
            var well = await GetOrDefaultAsync(idWell, token);

            if (well is null)
                return null;
            
            var wellInfo = wellInfoService.FirstOrDefault(well => well.Id == idWell);

            if (wellInfo is null)
                return well.Adapt<WellMapInfoWithTelemetryStat>();
                
            wellInfo.IdState = well.IdState;
            return wellInfo;
        }

        public async Task<IEnumerable<WellDto>> GetAsync(WellRequest request, CancellationToken token)
        {
            var wells = await GetEntitiesAsync(request, token);
            var wellsDtos = wells.Select(Convert);
            return wellsDtos;
        }

        private async Task<IEnumerable<Well>> GetEntitiesAsync(WellRequest request, CancellationToken token)
        {
            var wells = await GetCacheAsync(token);

            if (request.Ids?.Any() == true)
                wells = wells.Where(well => request.Ids.Contains(well.Id));

            if (request.IdCompany.HasValue)
                wells = wells.Where(well => well.RelationCompaniesWells.Any(r => r.IdCompany == request.IdCompany.Value));

            if (request.IdState.HasValue)
                wells = wells.Where(well => well.IdState == request.IdState.Value);

            return wells;
        }

        public override async Task<int> InsertAsync(WellDto dto, CancellationToken token)
        {
            if (IsTelemetryAssignedToDifferentWell(dto))
                throw new ArgumentInvalidException(nameof(dto), "Телеметрия уже была привязана к другой скважине.");
            
            if (dto.Id != 0 && (await GetCacheAsync(token)).Any(w => w.Id == dto.Id))
                throw new ArgumentInvalidException(nameof(dto), $"Нельзя повторно добавить скважину с id: {dto.Id}");

            var entity = Convert(dto);

            var result = await base.InsertAsync(dto, token);

            if (dto.Companies.Any())
            {
                var newRelations = dto.Companies.Select(c => new RelationCompanyWell { IdWell = result, IdCompany = c.Id });
                dbContext.RelationCompaniesWells.AddRange(newRelations);
                await dbContext.SaveChangesAsync(token);
                DropCacheRelationCompanyWell();
            }

            return result;
        }

        public override Task<int> InsertRangeAsync(IEnumerable<WellDto> dtos, CancellationToken token)
        {
            throw new NotImplementedException();
        }

        public override async Task<int> UpdateAsync(WellDto dto, 
            CancellationToken token)
        {
            if (IsTelemetryAssignedToDifferentWell(dto))
                throw new ArgumentInvalidException(nameof(dto), "Телеметрия уже была привязана к другой скважине.");
            
            var oldRelations = (await GetCacheRelationCompanyWellAsync(token))
                .Where(r => r.IdWell == dto.Id).ToArray();

            if (dto.Companies.Count() != oldRelations.Length ||
                dto.Companies.Any(c => oldRelations.All(oldC => oldC.IdCompany != c.Id)))
            {
                dbContext.RelationCompaniesWells
                    .RemoveRange(dbContext.RelationCompaniesWells
                        .Where(r => r.IdWell == dto.Id));
                
                DropCacheRelationCompanyWell();

                var newRelations = dto.Companies
                    .Select(c => new RelationCompanyWell
                    {
                        IdWell = dto.Id, 
                        IdCompany = c.Id
                    });
                
                dbContext.RelationCompaniesWells.AddRange(newRelations);
            }

            var result = await base.UpdateAsync(dto, token);
            return result;
        }

        public async Task<bool> IsCompanyInvolvedInWellAsync(int idCompany, int idWell, CancellationToken token)
            => (await GetCacheRelationCompanyWellAsync(token))
                .Any(r => r.IdWell == idWell && r.IdCompany == idCompany);

        public async Task<string> GetWellCaptionByIdAsync(int idWell, CancellationToken token)
        {
            var entity = await GetOrDefaultAsync(idWell, token).ConfigureAwait(false);            
            return entity!.Caption;
        }

        public async Task<IEnumerable<CompanyDto>> GetCompaniesAsync(int idWell, CancellationToken token)
        {
            var relations = (await GetCacheRelationCompanyWellAsync(token))
                .Where(r => r.IdWell == idWell);
            var dtos = relations.Select(r => Convert(r.Company));
            return dtos;
        }

        public string GetStateText(int state)
        {
            return state switch
            {
                1 => "В работе",
                2 => "Завершена",
                _ => "Неизвестно",
            };
        }

        public async Task<IEnumerable<int>> GetClusterWellsIdsAsync(int idWell, CancellationToken token)
        {
            var well = await GetOrDefaultAsync(idWell, token);

            if (well is null)
                return Enumerable.Empty<int>();

            var cache = await GetCacheAsync(token);

            var clusterWellsIds = cache
                .Where((w) => w.IdCluster == well.IdCluster)
                .Select(w => w.Id);

            return clusterWellsIds;
        }

        protected override Well Convert(WellDto dto)
        {
            var entity = dto.Adapt<Well>();

            entity.IdTelemetry = entity.IdTelemetry ?? dto.IdTelemetry ?? dto.Telemetry?.Id;

            if (dto.Timezone is null)
                entity.Timezone = GetTimezone(dto.Id)
                    .Adapt<SimpleTimezone>();

            return entity;
        }

        protected override WellDto Convert(Well entity)
        {
            var dto = base.Convert(entity);

            if (entity.Timezone is null)
                dto.Timezone = GetTimezone(entity.Id);

            dto.StartDate = wellOperationRepository.FirstOperationDate(entity.Id)?.ToRemoteDateTime(dto.Timezone.Hours);
            dto.WellType = entity.WellType.Caption;
            dto.Cluster = entity.Cluster.Caption;
            dto.Deposit = entity.Cluster.Deposit.Caption;
            if (entity.IdTelemetry is not null)
                dto.LastTelemetryDate = telemetryService.GetDatesRange(entity.IdTelemetry.Value).To;
            dto.Companies = entity.RelationCompaniesWells
                .Select(r => Convert(r.Company))
                .ToList();
            return dto;
        }

        private CompanyDto Convert(Company entity)
        {
            var dto = entity.Adapt<CompanyDto>();
            dto.CompanyTypeCaption = entity.CompanyType?.Caption
                ?? companyTypesService.GetOrDefault(entity.IdCompanyType)?.Caption
                ?? string.Empty;
            return dto;
        }

        public SimpleTimezoneDto GetTimezone(int idWell)
        {
            var well = GetOrDefault(idWell)
                ?? throw new ArgumentInvalidException(nameof(idWell), $"idWell: {idWell} does not exist.");
            return GetTimezone(well);
        }

        private SimpleTimezoneDto GetTimezone(WellDto wellDto)
        {
            if (wellDto.Timezone is not null)
                return wellDto.Timezone;

            if (wellDto.Telemetry is not null)
            {
                var timezone = telemetryService.GetTimezone(wellDto.Telemetry.Id);
                if (timezone is not null)
                    return timezone;
            }

            var well = GetQuery().FirstOrDefault(w => w.Id == wellDto.Id);

            if (well is not null)
            {
                var point = GetCoordinates(well);
                if (point is not null)
                {
                    if (point.Timezone is not null)
                    {
                        return point.Timezone.Adapt<SimpleTimezoneDto>();
                    }

                    if (point.Latitude is not null & point.Longitude is not null)
                    {
                        var timezone = timezoneService.GetOrDefaultByCoordinates(point.Latitude!.Value, point.Longitude!.Value);
                        if (timezone is not null)
                        {
                            return timezone;
                        }
                    }
                }
            }

            throw new Exception($"Can't find timezone for well {wellDto.Caption} id: {wellDto.Id}");
        }

        private bool IsTelemetryAssignedToDifferentWell(WellDto wellDto)
        {
            if (!wellDto.IdTelemetry.HasValue)
                return false;

            var existingWellWithAssignedTelemetry = GetCache()
                .FirstOrDefault(x => x.IdTelemetry == wellDto.IdTelemetry);

            if (existingWellWithAssignedTelemetry is null)
                return false;
            
            return existingWellWithAssignedTelemetry.Id != wellDto.Id;
        }

        private static AsbCloudDb.Model.IMapPoint GetCoordinates(Well well)
        {
            if (well is null)
                throw new ArgumentNullException(nameof(well));

            if (well.Latitude is not null & well.Longitude is not null)
                return well;

            var cluster = well.Cluster;

            if (cluster.Latitude is not null & cluster.Longitude is not null)
                return cluster;

            if (cluster.Deposit is null)
                throw new Exception($"Can't find coordinates of well by cluster {cluster.Caption} id: {cluster.Id}");

            var deposit = cluster.Deposit;

            if (deposit.Latitude is not null & deposit.Longitude is not null)
                return deposit;

            throw new Exception($"Can't find coordinates of well by deposit {deposit.Caption} id: {deposit.Id}");
        }

        public DatesRangeDto GetDatesRange(int idWell)
        {
            var well = GetOrDefault(idWell);
            if (well is null)
                throw new Exception($"Well id: {idWell} does not exist.");

            if (well.IdTelemetry is null)
                throw new KeyNotFoundException($"Well id: {idWell} does not contain telemetry.");

            return telemetryService.GetDatesRange((int)well.IdTelemetry);
        }
    }

}