using System.Data;
using System.Text.Json;
using CSharpFunctionalExtensions;
using Microsoft.EntityFrameworkCore;
using Monitoring.Features.Reports.Contracts;
using Monitoring.Features.Reports.TranslationDictionaries;
using TimeZoneConverter;

namespace Monitoring.Features.Reports.ReportFactories;

/// <summary>
/// Фабрика отчета журнала событий.
/// </summary>
internal class JournalReportFactory : IReportFactory
{
	/// <summary>
	/// Начало диапазона кодов событий пожарных событий.
	/// </summary>
	const int FireEventCodeRangeStart = 1100;

	/// <summary>
	/// Конец диапазона кодов событий пожарных событий.
	/// </summary>
	const int FireEventCodeRangeEnd = 1199;

	/// <summary>
	/// Фильтры.
	/// </summary>
	private record Extra(
		/// <summary>Дата начала.</summary>
		DateTime From,
		/// <summary>Дата окончания</summary>
		DateTime To,
		/// <summary>Только действия пользователя.</summary>
		bool? OnlyUserLogs,
		/// <summary>Только пожарные тревоги.</summary>
		bool? OnlyFire
	);

	private record EventsReportData(
		DateTime DateFrom,
		DateTime DateTo,
		List<LogGroupModel> LogGroups
	);

	public Result<DataTable> Export(MakeExportContext context)
	{
		DataTable dataTable = new("Logs");
		dataTable.Columns.AddRange(
			[
				new(Translations.Translate(context.Language, "CreatedAt"), typeof(CellContainer)),
				new(Translations.Translate(context.Language, "IsAlarm"), typeof(CellContainer)),
				new(Translations.Translate(context.Language, "Code"), typeof(CellContainer)),
				new(Translations.Translate(context.Language, "Message"), typeof(CellContainer)),
				new(Translations.Translate(context.Language, "AreaIndex"), typeof(CellContainer)),
				new(Translations.Translate(context.Language, "ZoneIndex"), typeof(CellContainer)),
				new(Translations.Translate(context.Language, "User"), typeof(CellContainer)),
			]
		);

		var data = JsonSerializer.Deserialize<EventsReportData>(context.Report.Data);
		if (data is null)
		{
			return Result.Failure<DataTable>(
				"Report data is failed to deserialize to EventsReportData"
			);
		}

		foreach (var item in data.LogGroups)
		{
			_ = dataTable.Rows.Add(new CellContainer(item.AssetName, dataTable.Columns.Count));
			//_ = dataTable.Rows.Add($"{item.AssetName} ({item.AssetId})");

			foreach (var day in item.Days)
			{
				var localDay = ToTz(day.Date.ToDateTime(TimeOnly.MinValue), context.TimeZone).Date;
				_ = dataTable.Rows.Add(
					new CellContainer(localDay.ToString("dd.MM.yyyy"), dataTable.Columns.Count)
				);
				//_ = dataTable.Rows.Add(day.Date);

				foreach (var log in day.Logs)
				{
					_ = dataTable.Rows.Add(
						new CellContainer(
							ToTz(log.CreatedAt, context.TimeZone).TimeOfDay.ToString(@"hh\:mm\:ss"),
							1
						),
						new CellContainer(TranslateBool(context.Language, log.IsAlarm), 1),
						new CellContainer(GetCodeTitle(context, log), 1),
						new CellContainer(log.Message ?? "-", 1),
						new CellContainer(log.AreaIndex?.ToString() ?? "-", 1),
						new CellContainer(log.ZoneIndex?.ToString() ?? "-", 1),
						new CellContainer(log.Username ?? "-", 1)
					);
				}
			}
		}

		return dataTable;
	}

	private static DateTime ToTz(DateTime utc, string tzId)
	{
		try
		{
			var tz = TZConvert.GetTimeZoneInfo(tzId);
			var asUtc = DateTime.SpecifyKind(utc, DateTimeKind.Utc);
			return TimeZoneInfo.ConvertTimeFromUtc(asUtc, tz);
		}
		catch
		{
			return utc;
		}
	}

	private static (DateTime fromUtc, DateTime toUtc) NormalizeRange(
		DateTime from,
		DateTime to,
		string tzId
	)
	{
		var tz = TZConvert.GetTimeZoneInfo(tzId);

		static DateTime ToUtc(DateTime dt, TimeZoneInfo tz) =>
			dt.Kind switch
			{
				DateTimeKind.Utc => dt,
				DateTimeKind.Local => dt.ToUniversalTime(),
				DateTimeKind.Unspecified => TimeZoneInfo.ConvertTimeToUtc(dt, tz),
				_ => dt,
			};

		return (ToUtc(from, tz), ToUtc(to, tz));
	}

	private static string GetCodeTitle(MakeExportContext context, LogModel log)
	{
		if (log.Code == 0)
			return "-";

		return Translations.Translate(
			context.Language,
			log.Code.ToString(),
			TranslationDictionaryCategories.ContactId
		);
	}

	public async ValueTask<Result<string>> Report(
		MakeReportContext context,
		CancellationToken cancellationToken
	)
	{
		if (string.IsNullOrEmpty(context.Extra))
			return Result.Failure<string>("Extra is required.");

		var request = JsonSerializer.Deserialize<Extra>(context.Extra);
		if (request is null)
			return Result.Failure<string>("Extra format error.");

		var (fromUtc, toUtc) = NormalizeRange(request.From, request.To, context.TimeZone);

		var events = await context
			.DataContext.Events.AsNoTracking()
			.Include(x => x.Asset)
			.Include(x => x.Code)
			.Where(x => context.AssetIdList.Any(y => y == x.AssetId))
			.Where(x =>
				x.Asset.Groups.Any(x => x.Accesses.Any(x => x.Member.UserId == context.UserId))
			)
			.Where(x => x.CreatedAt >= fromUtc && x.CreatedAt <= toUtc)
			.ToListAsync(cancellationToken);

		var logs = await context
			.DataContext.UserLogs.AsNoTracking()
			.Include(x => x.Asset)
			.Include(x => x.User)
			.Where(x => context.AssetIdList.Any(y => y == x.AssetId))
			.Where(x =>
				x.Asset.Groups.Any(x => x.Accesses.Any(x => x.Member.UserId == context.UserId))
			)
			.Where(x => x.CreatedAt >= fromUtc && x.CreatedAt <= toUtc)
			.ToListAsync(cancellationToken);

		var combinedList = events
			.Select(e => new LogGroupModel.Combined(e.AssetId, e.Asset, e.CreatedAt, e, null))
			.Union(
				logs.Select(l => new LogGroupModel.Combined(
					l.AssetId,
					l.Asset,
					l.CreatedAt,
					null,
					l
				))
			)
			.OrderBy(x => x.CreatedAt)
			.ToList();

		if (request.OnlyUserLogs == true)
			combinedList = [.. combinedList.Where(x => x.UserLog is not null)];

		if (request.OnlyFire == true)
		{
			combinedList =
			[
				.. combinedList.Where(x =>
					x.Event?.Code.Value.Value >= FireEventCodeRangeStart
					&& x.Event?.Code.Value.Value <= FireEventCodeRangeEnd
					&& x.Event?.Code.Value.Value % 10 == 1
				),
			];
		}

		var logGroups = combinedList
			.GroupBy(x => x.AssetId)
			.OrderBy(x => x.First().Asset.Name.Value)
			.Select(LogGroupModel.FromCombined)
			.ToList();

		var data = new EventsReportData(request.From, request.To, logGroups);

		var json = JsonSerializer.Serialize(data);
		return Result.Success(json);
	}

	private static string TranslateBool(string language, bool? value)
	{
		return Translations.Translate(language, value == true ? "True" : "False");
	}
}
