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

namespace AsbCloudInfrastructure.Services.SAUB
{
#nullable enable
    public class TelemetryService : ITelemetryService
    {
        private const string CacheTag = "TelemetryCache";
        private static readonly TimeSpan telemetryCacheObsolescence = TimeSpan.FromMinutes(5);

        private readonly IAsbCloudDbContext db;
        private readonly ITelemetryTracker telemetryTracker;
        private readonly ITimezoneService timezoneService;

        public ITimezoneService TimeZoneService => timezoneService;
        public ITelemetryTracker TelemetryTracker => telemetryTracker;

        public TelemetryService(
            IAsbCloudDbContext db,
            ITelemetryTracker telemetryTracker,
            ITimezoneService timezoneService)
        {
            this.db = db;
            this.telemetryTracker = telemetryTracker;
            this.timezoneService = timezoneService;
        }

        private IEnumerable<Telemetry> GetTelemetryCache()
        {
            var cache = db.Set<Telemetry>()
                .Include(t => t.Well)
                .FromCache(CacheTag, telemetryCacheObsolescence);
            return cache;
        }

        private void DropTelemetryCache()
        {
            db.Telemetries.DropCache(CacheTag);
        }

        public DateTime GetLastTelemetryDate(int idTelemetry)
        {
            var telemetry = GetTelemetryCache().FirstOrDefault(t => t.Id == idTelemetry);

            if (telemetry is null)
                throw new Exception($"Telemetry id:{idTelemetry} does not exist");

            var uid = telemetry.RemoteUid;
            var timzone = GetTimezone(idTelemetry);
            var lastTelemetryDate = telemetryTracker.GetLastTelemetryDateByUid(uid);
            return lastTelemetryDate.ToRemoteDateTime(timzone.Hours);
        }

        public DatesRangeDto GetDatesRange(int idTelemetry)
        {
            var telemetry = GetTelemetryCache().FirstOrDefault(t => t.Id == idTelemetry);
            if (telemetry is null)
                throw new Exception($"Telemetry id:{idTelemetry} does not exist");

            var dto = TelemetryTracker.GetTelemetryDateRangeByUid(telemetry.RemoteUid);
            if (dto is null)
                throw new Exception($"Telemetry id:{idTelemetry} has no data");

            var timezone = GetTimezone(idTelemetry);
            dto.From = dto.From.ToTimeZoneOffsetHours(timezone.Hours);
            dto.To = dto.To.ToTimeZoneOffsetHours(timezone.Hours);

            return dto;
        }

        public int GetOrCreateTelemetryIdByUid(string uid)
            => GetOrCreateTelemetryByUid(uid).Id;

        public int? GetIdWellByTelemetryUid(string uid)
            => GetWellByTelemetryUid(uid)?.Id;

        public async Task UpdateInfoAsync(string uid, TelemetryInfoDto info,
            CancellationToken token)
        {
            var telemetry = GetOrCreateTelemetryByUid(uid);
            telemetry.Info = info.Adapt<TelemetryInfo>();

            if (!string.IsNullOrEmpty(info.TimeZoneId) &&
                telemetry.TimeZone?.IsOverride != true)
                telemetry.TimeZone = new SimpleTimezone()
                {
                    Hours = info.TimeZoneOffsetTotalHours,
                    TimezoneId = info.TimeZoneId
                };

            db.Telemetries.Upsert(telemetry);
            await db.SaveChangesAsync(token);
            DropTelemetryCache();
        }

        public SimpleTimezoneDto GetTimezone(int idTelemetry)
        {
            var telemetry = GetTelemetryCache().FirstOrDefault(t => t.Id == idTelemetry);

            if (telemetry is null)
                throw new Exception($"Telemetry id: {idTelemetry} does not exist.");

            if (telemetry.TimeZone is not null)
                return telemetry.TimeZone.Adapt<SimpleTimezoneDto>();

            if (telemetry.Info is not null)
            {
                telemetry.TimeZone = new SimpleTimezone
                {
                    Hours = telemetry.Info.TimeZoneOffsetTotalHours,
                    IsOverride = false,
                    TimezoneId = telemetry.Info.TimeZoneId,
                };
                db.Telemetries.Upsert(telemetry);
                db.SaveChanges();
                DropTelemetryCache();
                return telemetry.TimeZone.Adapt<SimpleTimezoneDto>();
            }

            if (telemetry.Well?.Timezone is not null)
            {
                telemetry.TimeZone = telemetry.Well.Timezone;
                db.Telemetries.Upsert(telemetry);
                db.SaveChanges();
                DropTelemetryCache();
                return telemetry.TimeZone.Adapt<SimpleTimezoneDto>();
            }

            throw new Exception($"Telemetry id: {idTelemetry} can't find timezone.");
        }

        public int? GetOrDefaultIdTelemetryByIdWell(int idWell)
        {
            var telemetry = GetTelemetryCache()
                .FirstOrDefault(t => t.Well?.Id == idWell);
            return telemetry?.Id;
        }

        private Well? GetWellByTelemetryUid(string uid)
        {
            var telemetry = GetOrDefaultTelemetryByUid(uid);
            return telemetry?.Well;
        }

        private Telemetry? GetOrDefaultTelemetryByUid(string uid)
        {
            var telemetry = GetTelemetryCache().FirstOrDefault(t => t.RemoteUid == uid);
            return telemetry;
        }

        private Telemetry GetOrCreateTelemetryByUid(string uid)
        {
            var telemetry = GetOrDefaultTelemetryByUid(uid);
            if (telemetry is null)
            {
                var newTelemetry = new Telemetry
                {
                    RemoteUid = uid,
                    TimeZone = new SimpleTimezone
                    {
                        Hours = 5,
                        IsOverride = false,
                        TimezoneId = "default",
                    }
                };
                var entry = db.Telemetries.Add(newTelemetry);
                db.SaveChanges();
                DropTelemetryCache();
                return entry.Entity;
            }            
            return telemetry;
        }

        public async Task<int> MergeAsync(int from, int to, CancellationToken token)
        {
            if (from == to)
                return -2;

            var stopwath = new System.Diagnostics.Stopwatch();
            stopwath.Start();

            var transaction = await db.Database.BeginTransactionAsync(token).ConfigureAwait(false);
            try
            {
                var affected = 0;

                var wellFrom = await db.Wells.FirstOrDefaultAsync(w => w.IdTelemetry == from, token)
                    .ConfigureAwait(false);
                var wellTo = await db.Wells.FirstOrDefaultAsync(w => w.IdTelemetry == to, token)
                    .ConfigureAwait(false);

                if (wellTo is not null && wellFrom is not null)
                    return -2;

                if (wellTo is null && wellFrom is not null)
                {
                    wellFrom.IdTelemetry = to;
                    affected += await db.SaveChangesAsync(token);
                }

                affected += await MergeEventsAndMessagesAndUsersAsync(from, to, token);
                affected += await MergeDataAsync<TelemetryDataSaub>(from, to, token);
                affected += await MergeDataAsync<TelemetryDataSpin>(from, to, token);

                affected += await db.Database.ExecuteSqlRawAsync($"DELETE FROM t_telemetry_analysis WHERE id_telemetry = {from} OR id_telemetry = {to};", token)
                    .ConfigureAwait(false);
                affected += await db.Database.ExecuteSqlRawAsync($"DELETE FROM t_telemetry WHERE id = {from};", token)
                    .ConfigureAwait(false);
                await transaction.CommitAsync(token).ConfigureAwait(false);

                stopwath.Stop();
                Console.WriteLine($"Successfully committed in {1d * stopwath.ElapsedMilliseconds / 1000d: #0.00} sec. Affected {affected} rows.");
                DropTelemetryCache();
                return affected;
            }
            catch (Exception ex)
            {
                System.Diagnostics.Trace.WriteLine($"Merge() Fail. Rollback. Reason is:{ex.Message}");
#pragma warning disable CA2016 // Перенаправьте параметр "CancellationToken" в методы
                await transaction.RollbackAsync();
#pragma warning restore CA2016 // Перенаправьте параметр "CancellationToken" в методы
                return -1;
            }
        }

        private async Task<int> MergeEventsAndMessagesAndUsersAsync(int from, int to, CancellationToken token)
        {
            var messagesFrom = await db.TelemetryMessages
                .Where(d => d.IdTelemetry == from)
                .ToListAsync(token)
                .ConfigureAwait(false);

            var usersFromQuery = db.TelemetryUsers
                .Where(d => d.IdTelemetry == from);
            var usersFrom = await usersFromQuery
                .ToListAsync(token)
                .ConfigureAwait(false);

            var usersTo = await db.TelemetryUsers
                .Where(d => d.IdTelemetry == to)
                .AsNoTracking()
                .ToListAsync(token)
                .ConfigureAwait(false);

            var usersToNextId = usersTo.Max(u => u.IdUser) + 100;

            messagesFrom.ForEach(m => m.IdTelemetry = to);

            foreach (var userFrom in usersFrom)
            {
                var userTo = usersTo
                    .FirstOrDefault(u => u.IdUser == userFrom.IdUser);

                if (userTo is null ||
                    userTo.Name != userFrom.Name ||
                    userTo.Surname != userFrom.Surname ||
                    userTo.Patronymic != userFrom.Patronymic)
                {
                    messagesFrom
                        .Where(m => m.IdTelemetryUser == userFrom.IdUser)
                        .ToList()
                        .ForEach(m => m.IdTelemetryUser = usersToNextId);
                    userFrom.IdUser = usersToNextId;
                    userFrom.IdTelemetry = to;
                    usersToNextId++;
                }
            }

            var eventsFromQuery = db.TelemetryEvents
                .Where(d => d.IdTelemetry == from);
            var eventsFrom = await eventsFromQuery
                .ToListAsync(token)
                .ConfigureAwait(false);

            var eventsTo = await db.TelemetryEvents
                .Where(d => d.IdTelemetry == to)
                .AsNoTracking()
                .ToListAsync(token)
                .ConfigureAwait(false);

            var eventsToNextId = eventsTo.Max(e => e.IdEvent) + 1;

            foreach (var eventFrom in eventsFrom)
            {
                var eventTo = eventsTo
                    .FirstOrDefault(e => e.IdEvent == eventFrom.IdEvent);

                if (eventTo is null ||
                    eventTo.IdCategory != eventFrom.IdCategory ||
                    eventTo.MessageTemplate != eventFrom.MessageTemplate)
                {
                    messagesFrom
                        .Where(m => m.IdEvent == eventFrom.IdEvent)
                        .ToList()
                        .ForEach(m => m.IdEvent = eventsToNextId);
                    eventFrom.IdEvent = eventsToNextId;
                    eventFrom.IdTelemetry = to;
                    eventsToNextId++;
                }
            }

            await db.Database.ExecuteSqlRawAsync($"ALTER TABLE t_telemetry_user DISABLE TRIGGER ALL;", token).ConfigureAwait(false);
            await db.Database.ExecuteSqlRawAsync($"ALTER TABLE t_telemetry_event DISABLE TRIGGER ALL;", token).ConfigureAwait(false);
            await db.Database.ExecuteSqlRawAsync($"ALTER TABLE t_telemetry_message DISABLE TRIGGER ALL;", token).ConfigureAwait(false);
            var affected = await db.SaveChangesAsync(token).ConfigureAwait(false);
            await db.Database.ExecuteSqlRawAsync($"ALTER TABLE t_telemetry_user ENABLE TRIGGER ALL;", token).ConfigureAwait(false);
            await db.Database.ExecuteSqlRawAsync($"ALTER TABLE t_telemetry_event ENABLE TRIGGER ALL;", token).ConfigureAwait(false);
            await db.Database.ExecuteSqlRawAsync($"ALTER TABLE t_telemetry_message ENABLE TRIGGER ALL;", token).ConfigureAwait(false);
            db.TelemetryUsers.RemoveRange(usersFromQuery);
            db.TelemetryEvents.RemoveRange(eventsFromQuery);
            affected += await db.SaveChangesAsync(token).ConfigureAwait(false);
            return affected;
        }

        private async Task<int> MergeDataAsync<TEntity>(int from, int to, CancellationToken token)
            where TEntity : class, AsbCloudDb.Model.ITelemetryData
        {
            const string IdTelemetryColumnName = "\"id_telemetry\"";
            var dbSet = db.Set<TEntity>();
            var tableName = dbSet.GetTableName();
            var columns = dbSet.GetColumnsNames().ToList();
            var index = columns.FindIndex(c => c.ToLower() == IdTelemetryColumnName.ToLower());
            if (index < 0)
                return -5;
            columns[index] = $"{to} as {IdTelemetryColumnName}";
            var columnsString = string.Join(',', columns);
            var sql = $"INSERT INTO {tableName} " +
                $"select {columnsString} " +
                $"from {tableName} " +
                $"where {IdTelemetryColumnName} = {from};" +
                $"delete from {tableName} where {IdTelemetryColumnName} = {from};";

            var affected = await db.Database.ExecuteSqlRawAsync(sql, token)
                .ConfigureAwait(false);

            return affected;
        }
    }
#nullable disable
}