using AsbCloudApp.Data;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Services;
using AsbCloudDb.Model;
using AsbCloudInfrastructure.EfCache;
using AsbCloudInfrastructure.Services.Cache;
using Mapster;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AsbCloudInfrastructure.Services
{
    public class WellService : CrudCacheServiceBase<WellDto, Well>, IWellService
    {
        private const string relationCompaniesWellsCacheTag = "RelationCompaniesWells";
        private static readonly TimeSpan relationCompaniesWellsCacheObsolence = TimeSpan.FromMinutes(15);

        private readonly ITelemetryService telemetryService;
        private readonly ICrudService<CompanyTypeDto> companyTypesService;
        private readonly ITimezoneService timezoneService;
        private readonly IWellOperationService wellOperationService;

        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);

        public WellService(IAsbCloudDbContext db, CacheDb cacheDb, ITelemetryService telemetryService, ITimezoneService timezoneService)
            : base(db, MakeQueryWell)
        {
            this.telemetryService = telemetryService;
            this.timezoneService = timezoneService;

            this.wellOperationService = new WellOperationService.WellOperationService(db, cacheDb, this);
            companyTypesService = new CrudCacheServiceBase<CompanyTypeDto, CompanyType>(dbContext);
        }

        private IEnumerable<RelationCompanyWell> GetCacheRelationCompanyWell()
            => dbContext.RelationCompaniesWells
            .Include(r => r.Company)
            .Include(r => r.Well)
            .FromCache(relationCompaniesWellsCacheTag, relationCompaniesWellsCacheObsolence);

        private Task<IEnumerable<RelationCompanyWell>> GetCacheRelationCompanyWellAsync(CancellationToken token)
            => dbContext.RelationCompaniesWells
            .Include(r => r.Company)
            .Include(r => r.Well)
            .FromCacheAsync(relationCompaniesWellsCacheTag, relationCompaniesWellsCacheObsolence, token);

        private void DropCacheRelationCompanyWell()
            => dbContext.RelationCompaniesWells.DropCache(relationCompaniesWellsCacheTag);

        public DateTimeOffset GetLastTelemetryDate(int idWell)
        {
            var well = Get(idWell);

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

            var lastTelemetryDate = telemetryService.GetLastTelemetryDate((int)well.IdTelemetry);
            return lastTelemetryDate;
        }

        public async Task<IEnumerable<WellDto>> GetWellsByCompanyAsync(int idCompany, CancellationToken token)
        {
            var relationsCache = await GetCacheRelationCompanyWellAsync(token);

            var wellsIds = relationsCache
                .Where(r => r.IdCompany == idCompany)
                .Select(r => r.IdWell);

            var wellsDtos = (await GetCacheAsync(token))
                .Where(kv => wellsIds.Contains(kv.Key))
                .Select(kv => kv.Value);

            return wellsDtos.ToList();
        }

        public override async Task<int> InsertAsync(WellDto dto, CancellationToken token = default)
        {
            if (dto.IdWellType is < 1 or > 2)
                throw new ArgumentInvalidException("Тип скважины указан неправильно.", nameof(dto));

            if (dto.IdState is < 0 or > 2)
                throw new ArgumentInvalidException("Текущее состояние работы скважины указано неправильно.", nameof(dto));

            if (dto.Id != 0 && (await GetCacheAsync(token)).ContainsKey(dto.Id))
                throw new ArgumentInvalidException($"Нельзя повторно добавить скважину с id: {dto.Id}", nameof(dto));

            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 = default)
        {
            if (dto.IdWellType is < 1 or > 2)
                throw new ArgumentInvalidException("Тип скважины указан неправильно.", nameof(dto));

            if (dto.IdState is < 0 or > 2)
                throw new ArgumentInvalidException("Текущее состояние работы скважины указано неправильно.", nameof(dto));

            var oldRelations = (await GetCacheRelationCompanyWellAsync(token))
                .Where(r => r.IdWell == dto.Id);

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

                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 bool IsCompanyInvolvedInWell(int idCompany, int idWell)
            => GetCacheRelationCompanyWell()
                .Any(r => r.IdWell == idWell && r.IdCompany == idCompany);

        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 GetAsync(idWell, token).ConfigureAwait(false);
            var dto = Convert(entity);
            return dto.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;
        }

        private IEnumerable<CompanyDto> GetCompanies(int idWell)
        {
            var relations = GetCacheRelationCompanyWell().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 GetAsync(idWell, token);

            if (well is null)
                return null;

            var cache = await GetCacheAsync(token);

            var clusterWellsIds = cache.Values
                .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)
        {
            if (entity is null)
                return null;

            var dto = base.Convert(entity);

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

            dto.StartDate = wellOperationService.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.GetLastTelemetryDate((int)entity.IdTelemetry);
            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.Get(entity.IdCompanyType).Caption;
            return dto;
        }

        public async Task EnshureTimezonesIsSetAsync(CancellationToken token)
        {
            var cache = await GetCacheAsync(token);
            if (!cache.Values.Any(w => w.Timezone is null))
                return;

            var defaultTimeZone = new SimpleTimezone
            {
                Hours = 5,
                IsOverride = false,
                TimezoneId = "Assumed",
            };

            await dbSet.Where(w => w.Timezone == null)
                .ForEachAsync(w => w.Timezone = defaultTimeZone, token);

            await dbContext.SaveChangesAsync(token);
            DropCache();
        }

        public SimpleTimezoneDto GetTimezone(int idWell)
        {
            var well = Get(idWell);
            if (well == null)
                throw new ArgumentInvalidException($"idWell: {idWell} does not exist.", nameof(idWell));
            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);
            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.GetByCoordinates((double)point.Latitude, (double)point.Longitude);
                    if (timezone is not null)
                    {
                        return timezone;
                    }
                }
            }

            throw new Exception($"Can't find timezone for well {wellDto.Caption} 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;

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

            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 = Get(idWell);
            if (well is null)
                throw new Exception($"Well id: {idWell} does not exist.");

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

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