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,
                       IWellOperationCategoryRepository wellOperationCategoryRepository)
        : base(db, memoryCache, MakeQueryWell)
    {
        this.telemetryService = telemetryService;
        this.timezoneService = timezoneService;
        this.wellInfoService = wellInfoService;
        wellOperationRepository = new WellOperationRepository(db, memoryCache, wellOperationCategoryRepository, 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 DateTimeOffset GetLastTelemetryDate(int idWell)
    {
        var well = GetOrDefault(idWell);

        if (well?.IdTelemetry is null)
            return DateTimeOffset.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;
                        dto.Companies = well.RelationCompaniesWells.Select(r => Convert(r.Company));
                        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
            .GetFirstAndLastFact(entity.Id)?.First?.DateStart;
        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.ToOffset(dto.Timezone.Offset);
        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 cache = GetCache();
        var cacheItem = cache.FirstOrDefault(d => d.Id == idWell)
            ?? throw new ArgumentInvalidException(nameof(idWell), $"idWell: {idWell} does not exist.");
        return cacheItem.Timezone.Adapt<SimpleTimezoneDto>();
    }

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