using AsbCloudApp.Data;
using AsbCloudApp.Services;
using AsbCloudDb.Model;
using AsbCloudInfrastructure.Services.Cache;
using Mapster;
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 static readonly TypeAdapterConfig typeAdapterConfig = TypeAdapterConfig<WellDto, Well>
            .NewConfig()
            .Ignore(dst => dst.Cluster,
                dst => dst.RelationCompaniesWells,
                dst => dst.Telemetry,
                dst => dst.WellComposites,
                dst => dst.WellCompositeSrcs,
                dst => dst.WellOperations,
                dst => dst.WellType)
            .Config;

        private readonly ITelemetryService telemetryService;
        private readonly CacheTable<RelationCompanyWell> cacheRelationCompaniesWells;
        private readonly CacheTable<CompanyType> cacheCompanyWellTypes;
        private readonly ITimezoneService timezoneService;

        public ITelemetryService TelemetryService => telemetryService;

        public WellService(IAsbCloudDbContext db, CacheDb cacheDb, ITelemetryService telemetryService, ITimezoneService timezoneService)
            :base(db, cacheDb)
        {
            this.telemetryService = telemetryService;
            this.timezoneService = timezoneService;
            cacheRelationCompaniesWells = cacheDb.GetCachedTable<RelationCompanyWell>((AsbCloudDbContext)db, nameof(RelationCompanyWell.Company), nameof(RelationCompanyWell.Well));
            cacheCompanyWellTypes = cacheDb.GetCachedTable<CompanyType>((AsbCloudDbContext)db);
            Includes.Add($"{nameof(Well.Cluster)}.{nameof(Cluster.Deposit)}");
            Includes.Add(nameof(Well.Telemetry));
            Includes.Add($"{nameof(Well.RelationCompaniesWells)}.{nameof(RelationCompanyWell.Company)}");
            Includes.Add(nameof(Well.WellType));
        }

        public DateTimeOffset GetLastTelemetryDate(int idWell)
        {
            var well = Cache.FirstOrDefault(w => w.Id == 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 relations = await cacheRelationCompaniesWells
                .WhereAsync(r => r.IdCompany == idCompany, token);

            var wellsIds = relations.Select(r => r.IdWell);
            var wells = await Cache.WhereAsync(w => wellsIds.Contains(w.Id), token);

            var dtos = wells.Select(Convert);
            return dtos;
        }

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

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

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

            var entity = Convert(dto);

            var result = await Cache.InsertAsync(entity, token);

            if (dto.Companies.Any())
            {
                var newRelations = dto.Companies.Select(c => new RelationCompanyWell { IdWell = result.Id, IdCompany = c.Id });
                await cacheRelationCompaniesWells.InsertAsync(newRelations, token);
            }

            return result.Id;
        }

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

        public override async Task<int> UpdateAsync(int idWell, WellDto dto, 
            CancellationToken token = default)
        {
            if (dto.IdWellType is < 1 or > 2)
                throw new ArgumentException("Тип скважины указан неправильно.", nameof(dto));
            
            if (dto.IdState is < 0 or > 2)
                throw new ArgumentException("Текущее состояние работы скважины указано неправильно.", nameof(dto));
            
            if(dto.Id != idWell)
                throw new ArgumentException($"Нельзя поменять id для скважины: {idWell} => {dto.Id}.", nameof(dto));

            var entity = Convert(dto);

            var oldRelations = await cacheRelationCompaniesWells
                .WhereAsync(r => r.IdWell == idWell, token);

            if(dto.Companies.Count() != oldRelations.Count() ||
                dto.Companies.Any(c => !oldRelations.Any(oldC => oldC.IdCompany == c.Id)))
            {
                await cacheRelationCompaniesWells.RemoveAsync(r => r.IdWell == idWell, token);
                var newRelations = dto.Companies.Select(c=> new RelationCompanyWell {IdWell = idWell, IdCompany = c.Id });
                await cacheRelationCompaniesWells.InsertAsync(newRelations, token);
            }

            var result = await Cache.UpsertAsync(entity, token);
            return result;
        }

        public bool IsCompanyInvolvedInWell(int idCompany, int idWell)
        => cacheRelationCompaniesWells.Contains(r => r.IdWell == idWell && r.IdCompany == idCompany);

        public async Task<bool> IsCompanyInvolvedInWellAsync(int idCompany, int idWell, CancellationToken token)
         => await cacheRelationCompaniesWells.ContainsAsync(r => r.IdWell == idWell &&
                r.IdCompany == idCompany, token).ConfigureAwait(false);

        public async Task<string> GetWellCaptionByIdAsync(int idWell, CancellationToken token)
        {
            var entity = await Cache.FirstOrDefaultAsync(w => w.Id == idWell, token).ConfigureAwait(false);
            var dto = Convert(entity);
            return dto.Caption;
        }

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

        private IEnumerable<CompanyDto> GetCompanies(int idWell)
        {
            var relations = cacheRelationCompaniesWells.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 Cache.FirstOrDefaultAsync(w => w.Id == idWell, token)
                    .ConfigureAwait(false);
    
            if (well is null) 
                return null;
                
            var clusterWells = await Cache.WhereAsync(w => w.IdCluster == well.IdCluster, token)
                            .ConfigureAwait(false);
                   
            return clusterWells.Select(w => w.Id);
        }

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

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

            if (dto.Timezone is null)
                if (TryGetTimezone(dto.Id, out var timezoneDto))
                    entity.Timezone = timezoneDto.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)
                if(TryGetTimezone(entity, out var timezone))
                    dto.Timezone = timezone;                

            dto.WellType = entity.WellType?.Caption;
            dto.Cluster = entity.Cluster?.Caption;
            dto.Deposit = entity.Cluster?.Deposit?.Caption;
            dto.LastTelemetryDate = GetLastTelemetryDate(entity.Id).DateTime;
            dto.Companies = GetCompanies(entity.Id);
            return dto;
        }

        private CompanyDto Convert(Company entity)
        {
            var dto = entity.Adapt<CompanyDto>();
            dto.CompanyTypeCaption = entity.CompanyType?.Caption 
                ?? cacheCompanyWellTypes.FirstOrDefault(c => c.Id == entity.IdCompanyType).Caption;
            return dto;
        }

        public void EnshureTimezonesIsSet()
        {
            var wells = Cache.Where(w => w.Timezone is null).ToList();
            foreach (var well in wells)
            {
                if (TryGetTimezone(well, out var timezone))
                    well.Timezone = timezone.Adapt<SimpleTimezone>();
                else
                    well.Timezone = new SimpleTimezone
                    {
                        Hours = 5,
                        IsOverride = false,
                        TimeZoneId = "Assumed",
                    };
            }

            var wellsWithTz = wells.Where(w => w.Timezone is not null);
            if (wellsWithTz.Any())
            {
                var adaptedWells = wellsWithTz.Adapt<WellDto>().Select(Convert);
                Cache.Upsert(adaptedWells);
            }                
        }

        private bool TryGetTimezone(int idWell, out SimpleTimezoneDto timezone)
        {
            timezone = null;
            try
            {
                timezone = GetTimezone(idWell);
                return timezone is not null;
            }
            catch
            {
                return false;
            }
        }

        public SimpleTimezoneDto GetTimezone(int idWell)
        {
            var well = Cache.FirstOrDefault(c => c.Id == idWell);
            if (well == null)
                throw new ArgumentException($"idWell: {idWell} does not exist.", nameof(idWell));
            return GetTimezone(well);
        }

        private bool TryGetTimezone(Well well, out SimpleTimezoneDto timezone)
        {
            timezone = null;
            try
            {
                timezone = GetTimezone(well);
                return timezone is not null;
            }
            catch 
            {
                return false;
            }            
        }

        private SimpleTimezoneDto GetTimezone(Well well)
        {
            if (well == null)
                throw new ArgumentNullException(nameof(well));

            if (well.Timezone is not null)
                return well.Timezone.Adapt<SimpleTimezoneDto>();

            if (well.Telemetry is not null)
            {
                var timezone = telemetryService.GetTimezone(well.Telemetry.Id);
                if (timezone is not null)
                {
                    well.Timezone = timezone.Adapt<SimpleTimezone>();
                    return timezone;
                }
            }

            var point = GetCoordinates(well);
            if (point is not null)
            {
                if (point.Timezone is not null)
                {
                    well.Timezone = point.Timezone;
                    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)
                    {
                        well.Timezone = timezone.Adapt<SimpleTimezone>();
                        return timezone;
                    }
                }
            }

            throw new Exception($"Can't find timezone for well {well.Caption} id: {well.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.Cluster 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 = Cache.FirstOrDefault(w => w.Id == 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);
        }
    }
}