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

namespace AsbCloudInfrastructure.Repository;

public class GtrWitsRepository : IGtrRepository
{
    private static IDictionary<(int IdRecord, int IdItem), string> WitsParameters = new Dictionary<(int, int), string>
    {
        { (1, 8), nameof(GtrWitsDto.DEPTBITM) },
        { (1, 10), nameof(GtrWitsDto.DEPTMEAS) },
        { (1, 14), nameof(GtrWitsDto.HKLA) },
        { (1, 12), nameof(GtrWitsDto.BLKPOS) },
        { (1, 16), nameof(GtrWitsDto.WOBA) },
        { (1, 18), nameof(GtrWitsDto.TORQA) },
        { (1, 21), nameof(GtrWitsDto.SPPA) },
        { (2, 15), nameof(GtrWitsDto.RPMA) },
        { (1, 13), nameof(GtrWitsDto.ROPA) },
        { (3, 16), nameof(GtrWitsDto.RSUX) },
        { (3, 17), nameof(GtrWitsDto.RSDX) },
        { (1, 30), nameof(GtrWitsDto.MFIA) },
        { (1, 29), nameof(GtrWitsDto.MFOA)},
        { (1, 34), nameof(GtrWitsDto.MTIA) },
        { (1, 33), nameof(GtrWitsDto.MTOA) },
        { (1, 23), nameof(GtrWitsDto.SPM1) },
        { (1, 24), nameof(GtrWitsDto.SPM2) },
        { (1, 25), nameof(GtrWitsDto.SPM3) },
        { (1, 26), nameof(GtrWitsDto.TVOLACT) },
        { (11, 29), nameof(GtrWitsDto.TTVOL1) },
        { (11, 30), nameof(GtrWitsDto.TTVOL2) },
        { (11, 15), nameof(GtrWitsDto.TVOL01) },
        { (11, 16), nameof(GtrWitsDto.TVOL02) },
        { (11, 17), nameof(GtrWitsDto.TVOL03) },
        { (11, 18), nameof(GtrWitsDto.TVOL04) },
        { (11, 19), nameof(GtrWitsDto.TVOL05) },
        { (11, 20), nameof(GtrWitsDto.TVOL06) },
        { (11, 21), nameof(GtrWitsDto.TVOL07) },
        { (11, 22), nameof(GtrWitsDto.TVOL08) },
        { (11, 23), nameof(GtrWitsDto.TVOL09) },
        { (11, 24), nameof(GtrWitsDto.TVOL10) },
        { (11, 25), nameof(GtrWitsDto.TVOL11) },
        { (11, 26), nameof(GtrWitsDto.TVOL12) },
        { (11, 27), nameof(GtrWitsDto.TVOL13) },
        { (11, 28), nameof(GtrWitsDto.TVOL14) },
        { (1, 31), nameof(GtrWitsDto.MDOA) },
        { (1, 32), nameof(GtrWitsDto.MDIA) },
        { (12, 12), nameof(GtrWitsDto.METHANE) },
        { (12, 13), nameof(GtrWitsDto.ETHANE) },
        { (12, 14), nameof(GtrWitsDto.PROPANE) },
        { (12, 15), nameof(GtrWitsDto.IBUTANE) },
        { (12, 16), nameof(GtrWitsDto.NBUTANE) },
        { (1, 40), nameof(GtrWitsDto.GASA) },
    };

    private readonly IAsbCloudDbContext db;
    private readonly ITelemetryService telemetryService;
    private static ConcurrentDictionary<int, ConcurrentDictionary<(int, int), WitsItemRecordDto>> cache = new();

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

    public async Task<IEnumerable<GtrWitsDto>> GetAsync(int idWell, GtrRequest request, CancellationToken token) =>
        await GetAsync<WitsItemFloat, float>(idWell, request, token);

    public async Task<DatesRangeDto?> GetRangeAsync(int idWell, DateTimeOffset? geDate, DateTimeOffset? leDate, CancellationToken token)
    {
        var telemetry = telemetryService.GetOrDefaultTelemetryByIdWell(idWell);

        if (telemetry is null)
            return null;

        var rangeQuery = db
            .Set<WitsItemFloat>()
            .Where(e => e.IdTelemetry == telemetry.Id);

        if (geDate is not null)
            rangeQuery = rangeQuery.Where(e => e.DateTime >= geDate);

        if (leDate is not null)
            rangeQuery = rangeQuery.Where(e => e.DateTime <= leDate);

        var groupedQuery = rangeQuery.GroupBy(e => e.IdTelemetry)
            .Select(group => new
            {
                Min = group.Min(e => e.DateTime),
                Max = group.Max(e => e.DateTime)
            });
        db.Database.SetCommandTimeout(5*60);
        var range = await groupedQuery.FirstOrDefaultAsync(token);

        if (range is null)
            return null;

        var result = new DatesRangeDto
        {
            From = range.Min.ToOffset(telemetry.TimeZone!.Offset),
            To = range.Max.ToOffset(telemetry.TimeZone!.Offset),
        };
        return result;
    }

    private async Task<IEnumerable<GtrWitsDto>> GetAsync<TEntity, TType>(int idWell, GtrRequest request, CancellationToken token)
        where TEntity : WitsItemBase<TType>
        where TType : notnull
    {
        var telemetry = telemetryService.GetOrDefaultTelemetryByIdWell(idWell);

        if (telemetry is null)
            return Enumerable.Empty<GtrWitsDto>();

        if (telemetry.TimeZone is null)
            throw new ArgumentInvalidException(nameof(idWell), $"Telemetry id: {telemetry.Id} can't find timezone");

        var query = BuildQuery<TEntity, TType>(telemetry.Id, request);

        var idsRecord = WitsParameters.Select(p => p.Key.IdRecord);

        var entities = await query
            .Where(e => idsRecord.Contains(e.IdRecord))
            .OrderBy(e => e.DateTime)
            .AsNoTracking()
            .ToArrayAsync(token);

        if (!entities.Any())
            return Enumerable.Empty<GtrWitsDto>();

        var interval = TimeSpan.FromSeconds(10);
        var timezoneOffset = TimeSpan.FromHours(telemetry.TimeZone.Hours);

        var dtos = entities
            .GroupBy(e => e.DateTime.Ticks / interval.Ticks)
            .Select(groupByInterval =>
            {
                var items = groupByInterval.Select(e => e);
                var values = items.GroupBy(e => (e.IdRecord, e.IdItem))
                    .Where(group => WitsParameters.ContainsKey(group.Key))
                    .ToDictionary(group => WitsParameters[group.Key], g => (object)g.Last().Value);

                var dto = values.Adapt<GtrWitsDto>();
                dto.DateTime = items.Last().DateTime.ToOffset(timezoneOffset);
                return dto;
            });

        return dtos;
    }

    private IQueryable<TEntity> BuildQuery<TEntity, TType>(int idTelemetry, GtrRequest request)
        where TEntity : WitsItemBase<TType>
        where TType : notnull
    {
        var query = db.Set<TEntity>()
            .Where(e => e.IdTelemetry == idTelemetry);

        if (request.Begin.HasValue)
        {
            var dateBegin = request.Begin.Value.ToUniversalTime();
            var dateEnd = dateBegin.AddSeconds(request.IntervalSec);
            query = query
                .Where(e => e.DateTime >= dateBegin)
                .Where(e => e.DateTime <= dateEnd);
        }
        else
        {
            var lastDate = query
                .OrderBy(e => e.DateTime)
                .LastOrDefault()
                ?.DateTime
                ?? DateTimeOffset.UtcNow;
            var dateBegin = lastDate.AddSeconds(-request.IntervalSec);
            var dateEnd = lastDate;
            query = query
                .Where(e => e.DateTime >= dateBegin)
                .Where(e => e.DateTime <= dateEnd);
        }

        return query;
    }

    [Obsolete]
    public async Task<IEnumerable<WitsRecordDto>> GetAsync(int idWell, DateTime? dateBegin, double intervalSec = 600, int approxPointsCount = 1024, CancellationToken token = default)
    {
        var telemetry = telemetryService.GetOrDefaultTelemetryByIdWell(idWell);
        if (telemetry is null)
            return Enumerable.Empty<WitsRecordDto>();

        var timezone = telemetryService.GetTimezone(telemetry.Id);

        DateTimeOffset? dateBeginUtc = dateBegin?.ToUtcDateTimeOffset(timezone.Hours);
        var dateEnd = dateBeginUtc?.AddSeconds(intervalSec);

        var witsRequest = new WitsRequest()
        {
            IdTelemetry = telemetry.Id,
            DateBeginUtc = dateBeginUtc,
            DateEnd = dateEnd,
            ApproxPointsCount = approxPointsCount,
            TimezoneHours = timezone.Hours
        };

        var recordAllInt = await GetItemsOrDefaultAsync<WitsItemInt, int>(witsRequest, token);
        var recordAllFloat = await GetItemsOrDefaultAsync<WitsItemFloat, float>(witsRequest, token);
        var recordAllString = await GetItemsOrDefaultAsync<WitsItemString, string>(witsRequest, token);

        var dtos = (recordAllFloat.Union(recordAllInt)).Union(recordAllString)
            .GroupBy(g => new
            {
                g.IdRecord,
                g.Date
            })
            .Select(g => new WitsRecordDto
            {
                Id = g.Key.IdRecord,
                Date = g.Key.Date,
                Items = g.Select(r => new
                {
                    Key = r.IdItem,
                    r.Value
                }).ToDictionary(x => x.Key, x => x.Value)
            });
        return dtos;
    }

    public IEnumerable<WitsItemRecordDto> GetLastDataByRecordId(int idWell, int idRecord)
    {
        var result = GetLastData(idWell)
            .Where(item => item.IdRecord == idRecord);
        return result;
    }

    public IEnumerable<WitsItemRecordDto> GetLastData(int idWell)
    {
        var telemetry = telemetryService.GetOrDefaultTelemetryByIdWell(idWell);
        if (telemetry is null)
            return Enumerable.Empty<WitsItemRecordDto>();

        var lastData = cache.GetValueOrDefault(telemetry.Id);
        return lastData?.Values ?? Enumerable.Empty<WitsItemRecordDto>();
    }

    private async Task<IEnumerable<WitsItemRecordDto>> GetItemsOrDefaultAsync<TEntity, TValue>(
        WitsRequest request,
        CancellationToken token)
        where TEntity : WitsItemBase<TValue>
        where TValue : notnull
    {
        var query = BuildQuery<TEntity, TValue>(request);

        var fullDataCount = await query.CountAsync(token);
        if (fullDataCount == 0)
            return Enumerable.Empty<WitsItemRecordDto>();

        if (request.ApproxPointsCount is not null && fullDataCount > 1.75 * request.ApproxPointsCount)
        {
            var m = (int)Math.Round(1d * fullDataCount / request.ApproxPointsCount!.Value);
            if (m > 1)
                query = query.Where((d) => (((d.DateTime.DayOfYear * 24 + d.DateTime.Hour) * 60 + d.DateTime.Minute) * 60 + d.DateTime.Second) % m == 0);
        }

        var entities = await query
            .OrderBy(d => d.DateTime)
            .AsNoTracking()
            .ToListAsync(token)
            .ConfigureAwait(false);

        var items = entities.Select(e => new WitsItemRecordDto
        {
            IdRecord = e.IdRecord,
            Date = e.DateTime.ToRemoteDateTime(request.TimezoneHours),
            IdItem = e.IdItem,
            Value = new JsonValue(e.Value)
        });
        return items;
    }

    private IQueryable<TEntity> BuildQuery<TEntity, TValue>(WitsRequest request)
        where TEntity : WitsItemBase<TValue>
        where TValue : notnull
    {
        var query = db.Set<TEntity>().Where(i => i.IdTelemetry == request.IdTelemetry);

        if (request.IdRecord is not null)
            query = query
                .Where(d => d.IdRecord == request.IdRecord);

        if (request.DateBeginUtc is not null)
            query = query
                .Where(d => d.DateTime >= request.DateBeginUtc);

        if (request.DateEnd is not null)
            query = query
                .Where(d => d.DateTime <= request.DateEnd);

        return query;
    }

    public async Task SaveDataAsync(int idTelemetry, IEnumerable<WitsRecordDto> dtos, CancellationToken token)
    {
        var timezoneHours = telemetryService.GetTimezone(idTelemetry).Hours;

        var cacheTelemetryItems = cache.GetValueOrDefault(idTelemetry);

        var strings = new List<WitsItemString>(4);
        var floats = new List<WitsItemFloat>(4);
        var ints = new List<WitsItemInt>(4);

        foreach (var record in dtos)
        {
            var dateTime = record.Date.ToUtcDateTimeOffset(timezoneHours);
            foreach (var item in record.Items)
            {
                if (cacheTelemetryItems?.TryGetValue((record.Id, item.Key), out var cacheItem) == true)
                    if (Math.Abs((dateTime - cacheItem.Date).TotalSeconds) < 1)
                        continue;

                if (item.Value.Value is string valueString)
                {
                    var entity = MakeEntity<WitsItemString, string>(record.Id, item.Key, idTelemetry, dateTime, valueString);
                    strings.Add(entity);
                }
                if (item.Value.Value is float valueFloat)
                {
                    var entity = MakeEntity<WitsItemFloat, float>(record.Id, item.Key, idTelemetry, dateTime, valueFloat);
                    floats.Add(entity);
                }
                if (item.Value.Value is int valueInt)
                {
                    var entity = MakeEntity<WitsItemInt, int>(record.Id, item.Key, idTelemetry, dateTime, valueInt);
                    ints.Add(entity);
                }
            }
        }

        try
        {
            if (strings.Any())
                await db.Database.ExecInsertOrIgnoreAsync(db.Set<WitsItemString>(), strings, token);

            if (floats.Any())
                await db.Database.ExecInsertOrIgnoreAsync(db.Set<WitsItemFloat>(), floats, token);

            if (ints.Any())
                await db.Database.ExecInsertOrIgnoreAsync(db.Set<WitsItemInt>(), ints, token);
        }
        catch (Exception ex)
        {
            Trace.TraceError("Exception while saving GTR Wits data", ex);
        }

        cache.AddOrUpdate(idTelemetry,
            (_) => MakeNewCache(dtos),
            (_, oldItemsDictionary) =>
            {
                foreach (var record in dtos)
                    foreach (var item in record.Items)
                    {
                        oldItemsDictionary.AddOrUpdate(
                            (record.Id, item.Key),
                            (_) => new WitsItemRecordDto
                            {
                                IdRecord = record.Id,
                                IdItem = item.Key,
                                Date = record.Date,
                                Value = item.Value
                            },
                            (_, _) => new WitsItemRecordDto
                            {
                                IdRecord = record.Id,
                                IdItem = item.Key,
                                Date = record.Date,
                                Value = item.Value
                            });
                    }
                return oldItemsDictionary;
            });
    }

    private static ConcurrentDictionary<(int, int), WitsItemRecordDto> MakeNewCache(IEnumerable<WitsRecordDto> dtos)
    {
        var items = dtos.SelectMany(record =>
            record.Items.Select(
                item => new WitsItemRecordDto
                {
                    IdItem = item.Key,
                    IdRecord = record.Id,
                    Date = record.Date,
                    Value = item.Value,
                }));

        var groups = items
            .GroupBy(item => (item.IdRecord, item.IdItem));

        var pairs = groups.Select(group => new KeyValuePair<(int, int), WitsItemRecordDto>(
            group.Key,
            group.OrderByDescending(item => item.Date).First()));

        return new ConcurrentDictionary<(int, int), WitsItemRecordDto>(pairs);
    }

    private static TEntity MakeEntity<TEntity, TValue>(int idRecord, int idItem, int idTelemetry, DateTimeOffset dateTime, TValue value)
        where TEntity : WitsItemBase<TValue>, new()
        where TValue : notnull
    => new TEntity()
    {
        IdRecord = idRecord,
        IdItem = idItem,
        IdTelemetry = idTelemetry,
        DateTime = dateTime,
        Value = value,
    };

    private static TEntity MakeEntity<TEntity, TValue>(WitsItemRecordDto dto, int idTelemetry, DateTimeOffset dateTime)
        where TEntity : WitsItemBase<TValue>, new()
        where TValue : notnull
    => new TEntity()
    {
        IdRecord = dto.IdRecord,
        IdItem = dto.IdItem,
        IdTelemetry = idTelemetry,
        DateTime = dateTime,
        Value = (TValue)dto.Value.Value,
    };

    private IQueryable<TEntity> BuildQuery<TEntity, TValue>(TelemetryPartDeleteRequest request)
        where TEntity : WitsItemBase<TValue>
        where TValue : notnull
    {
        var query = db.Set<TEntity>().Where(i => i.IdTelemetry == request.IdTelemetry);

        if (request.LeDate is not null)
        {
            var leDate = request.LeDate.Value.ToUniversalTime();
            query = query.Where(o => o.DateTime <= leDate);
        }

        if (request.GeDate is not null)
        {
            var geDate = request.GeDate.Value.ToUniversalTime();
            query = query.Where(o => o.DateTime >= geDate);
        }

        return query;
    }

    public async Task<int> DeleteAsync(TelemetryPartDeleteRequest request, CancellationToken token)
    {
        var result = 0;
        result += await DeleteAsync<WitsItemFloat, float>(request, token);
        result += await DeleteAsync<WitsItemInt, int>(request, token);
        result += await DeleteAsync<WitsItemString, string>(request, token);

        return result;
    }

    private async Task<int> DeleteAsync<TEntity, TType>(TelemetryPartDeleteRequest request, CancellationToken token)
        where TEntity : WitsItemBase<TType>
        where TType : notnull
    {
        var query = BuildQuery<TEntity, TType>(request);
        db.Set<TEntity>().RemoveRange(query);
        return await db.SaveChangesAsync(token);
    }


    private class WitsRequest
    {
        public int IdTelemetry { get; set; }
        public DateTimeOffset? DateBeginUtc { get; set; }
        public DateTimeOffset? DateEnd { get; set; }
        public int? ApproxPointsCount { get; set; }
        public double TimezoneHours { get; set; }
        public int? IdRecord { get; set; }
    }
}