using Forwarder.Domain;
using Forwarder.Models;
using Forwarder.Repositories;

using Microsoft.Extensions.Options;

using Serilog;

namespace Forwarder.Services;

/// <summary>
/// Обновляет статусы объектов.
/// </summary>
/// <param name="options">Опции.</param>
/// <param name="geoRitmOptions">Опции подключения к GeoRitm.</param>
/// <param name="serviceProvider">Сервис-провайдер DI контейнер.</param>
internal class OnlineStatusJob(
	IOptions<ForwarderOptions> options,
	IOptions<GeoRitmConnectionOptions> geoRitmOptions,
	IServiceProvider serviceProvider
) : BackgroundService
{
	/// <inheritdoc />
	protected override async Task ExecuteAsync(CancellationToken stoppingToken)
	{
		if (string.IsNullOrEmpty(geoRitmOptions.Value.Url) ||
			string.IsNullOrEmpty(geoRitmOptions.Value.Username) ||
			string.IsNullOrEmpty(geoRitmOptions.Value.Password))
		{
			Log.Warning("GeoRitm connection options are not set. OnlineStatusJob will not run.");
			return;
		}

		while (!stoppingToken.IsCancellationRequested)
		{
			try
			{
				await ExecuteBatch(stoppingToken);
			}
			catch (Exception ex)
			{
				Log.Error(ex, "Error in ExecuteBatch");
			}

			await Task.Delay(options.Value.SendingIntervalInSeconds * 1000, stoppingToken);
		}
	}

	private async Task ExecuteBatch(CancellationToken stoppingToken)
	{
		using var scope = serviceProvider.CreateScope();
		var provider = scope.ServiceProvider;
		var sendService = provider.GetRequiredService<ISendService>();
		var assetMapRepository = provider.GetRequiredService<IAssetMapRepository>();
		var geoRitmClient = provider.GetRequiredService<IGeoRitmClient>();

		var assetMapList = await ReadAssetMaps(assetMapRepository, stoppingToken);

		var objectInformationList = await GetObjectInformationList(
			geoRitmClient,
			stoppingToken
		);

		foreach (var newObjectStatus in objectInformationList)
		{
			foreach (var oldObjectStatus in assetMapList)
			{
				if (!CompareAbonents(newObjectStatus.Imei, oldObjectStatus.Abonent))
				{
					continue;
				}

				if (CompareOnlineStatuses(newObjectStatus, oldObjectStatus))
				{
					continue;
				}

				await UpdateObjectStatus(
					assetMapRepository,
					sendService,
					oldObjectStatus,
					newObjectStatus,
					stoppingToken);
			}
		}
	}

	private static async Task UpdateObjectStatus(
		IAssetMapRepository assetMapRepository,
		ISendService sendService,
		AssetMap oldObjectStatus,
		GetObjectListResponse newObjectStatus,
		CancellationToken cancellationToken)
	{
		try
		{
			Log.Information(
				"Update object status: {Id}: {OldStatus} -> {NewStatus}",
				oldObjectStatus.Id,
				oldObjectStatus.IsOnline,
				newObjectStatus.IsOnline);

			await assetMapRepository.UpdateIsOnlineStatusAsync(
				oldObjectStatus.Id,
				newObjectStatus.IsOnline,
				cancellationToken);

			await sendService.SendOnlineStatus(
				newObjectStatus.IsOnline,
				oldObjectStatus.AssetId!.Value,
				cancellationToken);
		}
		catch (Exception ex)
		{
			Log.Error(ex, "Error in UpdateObjectStatus");
		}
	}

	private static async Task<IEnumerable<AssetMap>> ReadAssetMaps(
		IAssetMapRepository assetMapRepository,
		CancellationToken cancellationToken)
	{
		var result = await assetMapRepository.GetTable(
			new AssetTableStateRequest(null, null),
			cancellationToken);
		if (result.IsFail)
		{
			Log.Error("Get asset map table is failed: {Error}", result.Error);
			return [];
		}

		return result.Value.Items;
	}

	private static async Task<IEnumerable<GetObjectListResponse>> GetObjectInformationList(
		IGeoRitmClient geoRitmClient,
		CancellationToken cancellationToken)
	{
		try
		{
			var response = await geoRitmClient.GetObjectList(cancellationToken);
			if (response.IsFail)
			{
				Log.Error("Get object list is failed: {Error}", response.Error);
				return [];
			}

			return response.Value;
		}
		catch (Exception ex)
		{
			Log.Error(ex, "Get object list is failed");
			return [];
		}
	}

	private sealed record AssetTableStateRequest(int? Skip, int? Take) : ITableStateRequest;

	private static bool CompareOnlineStatuses(GetObjectListResponse newObjectStatus, AssetMap oldObjectStatus)
	{
		return newObjectStatus.IsOnline == (oldObjectStatus.IsOnline ?? false);
	}

	private static bool CompareAbonents(string abonent1, string abonent2)
	{
		return GetLastAbonentSymbols(abonent1) == GetLastAbonentSymbols(abonent2);
	}

	private static string GetLastAbonentSymbols(string abonent)
	{
		const int abonentLength = 8;
		return abonent.Length > abonentLength
			? abonent[^abonentLength..]
			: abonent;
	}
}
