using CSharpFunctionalExtensions;
using FluentValidation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Monitoring.Common;
using Monitoring.Contracts;
using Monitoring.Controllers;
using Monitoring.Core.Domain;
using Monitoring.Core.Domain.AssetGroupManagement;
using Monitoring.Core.Domain.AssetManagement;
using Monitoring.Core.Domain.EventManagement;
using Monitoring.Core.Domain.Primitives;
using Monitoring.Core.Domain.ServiceOrganizationManagement;
using Monitoring.Core.Domain.TokenManagement;
using Monitoring.Core.Domain.UserLogManagement;
using Monitoring.Core.Domain.UserPrimitives;
using Monitoring.DomainEvents.EventCreated;
using Monitoring.Extensions;
using Monitoring.Features.Areas;
using Monitoring.Features.Events.CreateDemoEvent;
using Monitoring.Features.Events.GetEventList;
using Monitoring.Features.Events.ResetAlarms;
using Monitoring.Infrastructure.Data;
using Monitoring.Models;
using Monitoring.Repositories;
using Monitoring.Services;
using Wolverine;
using EventId = Monitoring.Core.Domain.EventManagement.EventId;

namespace Monitoring.Features.Events;

[Route("api/events")]
[Authorize(Roles = $"{Roles.Watcher},{Roles.Manager}")]
public class EventsController(
	DataContext dataContext,
	INotificationService notificationService,
	UsersService usersService,
	AssetRepository assetRepository,
	EventCodeRepository eventCodeRepository,
	ILogger<EventsController> logger,
	IMessageBus messageBus
) : BaseController
{
	private readonly Dictionary<
		EventCodeGroup,
		Func<
			DataContext,
			Asset,
			Event,
			IEnumerable<UserId>,
			INotificationService,
			CancellationToken,
			ValueTask
		>
	> _handlers = new()
	{
		[EventCodeGroup.Alarm] = CreateAlarm,
		[EventCodeGroup.Fault] = CreateFault,
		[EventCodeGroup.Arming] = CreateArming,
	};

	[HttpPost(Name = "CreateEvent")]
	[AllowAnonymous]
	[ProducesResponseType(StatusCodes.Status401Unauthorized)]
	[ProducesResponseType<string>(StatusCodes.Status404NotFound)]
	[ProducesResponseType<string>(StatusCodes.Status400BadRequest)]
	[ProducesResponseType<EventModel>(StatusCodes.Status200OK)]
	public async Task<IActionResult> CreateEvent(
		CreateEventRequest request,
		CancellationToken cancellationToken
	)
	{
		logger.LogInformation("Try to create event");

		var tokenCode = TokenCode.Create(request.Token);
		var organization = await dataContext.Organizations.FirstOrDefaultAsync(
			x => x.Tokens!.Any(x => x.Code == tokenCode),
			cancellationToken
		);
		if (organization is null)
		{
			logger.LogInformation("Token is invalid");
			return Unauthorized();
		}

		var assetId = new AssetId(request.AssetId);
		var asset = await dataContext.Assets.FirstOrDefaultAsync(
			x => x.Id == assetId && x.OrganizationId == organization.Id,
			cancellationToken
		);
		if (asset is null)
		{
			if (request.AutoAssetCreating is null)
			{
				logger.LogInformation("Asset is null");

				return NotFound(nameof(Asset));
			}

			ServiceOrganizationId? serviceOrganizationId = null;

			asset = Asset.Create(
				new AssetId(request.AssetId),
				AssetName.Create(request.AutoAssetCreating.AssetName),
				AssetDescription.Create(request.AutoAssetCreating.AssetName),
				organization.Id,
				Address.Empty,
				Owner.Empty,
				Passwords.Empty,
				Coordinates.Empty,
				serviceOrganizationId,
				Imei.Empty
			);

			var result = await assetRepository.Add(asset, cancellationToken);

			if (result.IsFailure)
			{
				logger.LogInformation("Cannot to make auto create asset. {Error}", result.Error);

				return BadRequest(result.Error);
			}
		}

		logger.LogInformation("Asset found to create event with Id={Id}", asset.Id);

		return await CreateEvent(request, asset, cancellationToken);
	}

	private async Task<bool> IsInBlacklist(
		AssetId assetId,
		int code,
		CancellationToken cancellationToken
	)
	{
		var eventCodeValue = new EventCodeValue(code);
		var count = await dataContext
			.LockedEventCodes.Where(x =>
				x.AssetId == assetId && x.EventCode.Value == eventCodeValue
			)
			.CountAsync(cancellationToken);

		return count > 0;
	}

	private async Task<IActionResult> CreateEvent(
		CreateEventRequest request,
		Asset asset,
		CancellationToken cancellationToken
	)
	{
		logger.LogInformation("Try to create event");

		var codeIdResult = await eventCodeRepository.Get(new(request.Code), cancellationToken);
		if (codeIdResult.IsFailure)
		{
			logger.LogInformation("Cannot to find event code. {Error}", codeIdResult.Error);

			return BadRequest(codeIdResult.Error);
		}

		var areaIndex = new AreaIndex(request.AreaIndex);
		var zoneIndex = new ZoneIndex(request.ZoneIndex);

		try
		{
			var eventEntity = Event.Create(
				new EventId(Guid.CreateVersion7()),
				codeIdResult.Value.Id,
				areaIndex,
				zoneIndex,
				asset.Id,
				DateTime.UtcNow,
				request.CreatedAt ?? DateTime.UtcNow
			);

			_ = await dataContext.Events.AddAsync(eventEntity, cancellationToken);
			_ = await dataContext.SaveChangesAsync(cancellationToken);

			await messageBus.SendAsync(new EventCreatedDomainEvent(eventEntity.Id));

			var userIds = await usersService.GetWatcherIdListByAsset(asset.Id, cancellationToken);

			if (!await IsInBlacklist(asset.Id, request.Code, cancellationToken))
			{
				await TryChangeAssetState(
					asset,
					eventEntity,
					codeIdResult.Value,
					userIds,
					cancellationToken
				);
			}

			var model = await dataContext
				.Events.Where(x => x.Id == eventEntity.Id)
				.Select(EventModel.FromEntity)
				.FirstAsync(cancellationToken);

			return (IActionResult)Ok(model);
		}
		catch (Exception ex)
		{
			logger.LogError(ex, "Error occurred while creating new event");
			return (IActionResult)BadRequest(ex.Message);
		}
	}

	private async Task TryChangeAssetState(
		Asset asset,
		Event eventEntity,
		EventCode eventCode,
		IEnumerable<UserId> userIds,
		CancellationToken cancellationToken
	)
	{
		var handler = _handlers.GetValueOrDefault(eventCode.Group);
		if (handler is null)
		{
			return;
		}

		await handler(
			dataContext,
			asset,
			eventEntity,
			userIds,
			notificationService,
			cancellationToken
		);
	}

	private static async ValueTask CreateAlarm(
		DataContext dataContext,
		Asset asset,
		Event eventEntity,
		IEnumerable<UserId> userIds,
		INotificationService notificationService,
		CancellationToken cancellationToken
	)
	{
		var area = await GetOrCreateArea(
			dataContext,
			asset.Id,
			eventEntity.AreaIndex,
			cancellationToken
		);
		var zone = await GetOrCreateZone(
			dataContext,
			area.Id,
			eventEntity.ZoneIndex,
			cancellationToken
		);

		var alarmScope = await GetOrCreateAlarmScope(dataContext, asset, cancellationToken);

		var alarm = Alarm.Create(eventEntity.Id, zone.Id, alarmScope.Id);
		_ = await dataContext.Alarms.AddAsync(alarm, cancellationToken);
		_ = await dataContext.SaveChangesAsync(cancellationToken);

		await NotifyUsersAboutNewAlarm(
			dataContext,
			userIds,
			notificationService,
			alarm,
			cancellationToken
		);
	}

	private static async Task NotifyUsersAboutNewAlarm(
		DataContext dataContext,
		IEnumerable<UserId> userIds,
		INotificationService notificationService,
		Alarm alarm,
		CancellationToken cancellationToken
	)
	{
		var model = await GetModel(dataContext, alarm, cancellationToken);
		await notificationService.NotifyUsers(userIds, "ALARMS.CREATED", model, cancellationToken);
	}

	private static async Task<AlarmScope> GetOrCreateAlarmScope(
		DataContext dataContext,
		Asset asset,
		CancellationToken cancellationToken
	)
	{
		var alarmScope = await dataContext.AlarmScopes.FirstOrDefaultAsync(
			x =>
				x.AssetId == asset.Id
				&& x.StatusHistory.OrderByDescending(s => s.CreatedAt).First().Status
					!= AlarmScopeStatus.Resolved,
			cancellationToken
		);

		if (alarmScope is null)
		{
			alarmScope = new AlarmScope(asset.Id);
			_ = await dataContext.AlarmScopes.AddAsync(alarmScope, cancellationToken);
		}

		return alarmScope;
	}

	private static async ValueTask<AlarmModel> GetModel(
		DataContext dataContext,
		Alarm alarm,
		CancellationToken cancellationToken
	)
	{
		return await dataContext
			.Alarms.AsNoTracking()
			.Where(x => x.Id == alarm.Id)
			.Select(AlarmModel.FromEntity)
			.FirstAsync(cancellationToken);
	}

	private static async ValueTask CreateFault(
		DataContext dataContext,
		Asset asset,
		Event eventEntity,
		IEnumerable<UserId> userIds,
		INotificationService notificationService,
		CancellationToken cancellationToken
	)
	{
		var mainCode = new EventCodeValue(eventEntity.Code.Value.Value / 10 * 10 + 1);
		var fault = await dataContext.Faults.FirstOrDefaultAsync(
			x =>
				x.Event.AssetId == asset.Id
				&& x.Event.AreaIndex == eventEntity.AreaIndex
				&& x.Event.ZoneIndex == eventEntity.ZoneIndex
				&& x.Event.Code.Value == mainCode,
			cancellationToken
		);
		if (eventEntity.Code.IsActive)
		{
			if (fault is not null)
			{
				return;
			}

			var area = await GetOrCreateArea(
				dataContext,
				asset.Id,
				eventEntity.AreaIndex,
				cancellationToken
			);
			var zone = await GetOrCreateZone(
				dataContext,
				area.Id,
				eventEntity.ZoneIndex,
				cancellationToken
			);

			fault = Fault.Create(eventEntity.Id, zone.Id);
			_ = await dataContext.Faults.AddAsync(fault, cancellationToken);
			_ = await dataContext.SaveChangesAsync(cancellationToken);

			var model = await GetModel(dataContext, fault, cancellationToken);

			await notificationService.NotifyUsers(
				userIds,
				"FAULTS.CREATED",
				model,
				cancellationToken
			);
		}
		else
		{
			if (fault is null)
			{
				return;
			}

			_ = dataContext.Faults.Remove(fault);

			var model = await GetModel(dataContext, fault, cancellationToken);

			_ = await dataContext.SaveChangesAsync(cancellationToken);

			await notificationService.NotifyUsers(
				userIds,
				"FAULTS.DELETED",
				model,
				cancellationToken
			);
		}

		static async ValueTask<FaultModel> GetModel(
			DataContext dataContext,
			Fault fault,
			CancellationToken cancellationToken
		)
		{
			return await dataContext
				.Faults.AsNoTracking()
				.Where(x => x.Id == fault.Id)
				.Select(FaultModel.FromEntity)
				.FirstAsync(cancellationToken);
		}
	}

	private static async ValueTask CreateArming(
		DataContext dataContext,
		Asset asset,
		Event eventEntity,
		IEnumerable<UserId> userIds,
		INotificationService notificationService,
		CancellationToken cancellationToken
	)
	{
		var area = await GetOrCreateArea(
			dataContext,
			asset.Id,
			eventEntity.AreaIndex,
			cancellationToken
		);
		if (area.IsGuarded == eventEntity.Code.IsActive)
		{
			return;
		}

		area.IsGuarded = !eventEntity.Code.IsActive;
		_ = await dataContext.SaveChangesAsync(cancellationToken);

		var areaModel = await dataContext
			.Areas.Where(x => x.Id == area.Id)
			.Select(AreaModel.Projection)
			.FirstAsync(cancellationToken);

		await notificationService.NotifyUsers(
			userIds,
			"AREA.UPDATED",
			areaModel,
			cancellationToken
		);
	}

	[HttpGet(Name = "GetEventList")]
	[ProducesResponseType<TableDataResponseModel<EventModel>>(StatusCodes.Status200OK)]
	public async Task<IActionResult> GetList(
		[FromQuery] GetEventListRequest request,
		CancellationToken cancellationToken
	)
	{
		AssetId assetId = new(request.AssetId);
		var query = dataContext.Events.Where(x =>
			x.AssetId == assetId
			&& x.Asset.Groups.Any(x => x.Accesses.Any(x => x.Member.UserId == UserId))
		);

		if (request.AlarmsOnly == true)
		{
			query = query.Where(x => x.Code.Group == EventCodeGroup.Alarm);
		}

		var events = await query
			.AsNoTracking()
			.OrderByDescending(x => x.CreatedAt)
			.Take(1000)
			.GetTable(request, EventModel.FromEntity, cancellationToken);

		return Ok(events);
	}

	[HttpPost("reset-alarms", Name = "ResetAlarms")]
	[ProducesResponseType<string>(StatusCodes.Status404NotFound)]
	[ProducesResponseType(StatusCodes.Status200OK)]
	public async Task<IActionResult> ResetAlarms(
		[FromBody] ResetAlarmsRequest request,
		CancellationToken cancellationToken
	)
	{
		await using var transaction = await dataContext.Database.BeginTransactionAsync(
			cancellationToken
		);

		try
		{
			var assetIds = request.AssetIdList.Select(x => new AssetId(x));
			var assetList = await dataContext
				.Assets.Include(x => x.Organization.Members)
				.Where(x =>
					assetIds.Contains(x.Id)
					&& x.Organization!.Members!.Any(member => member.UserId == UserId)
				)
				.Where(x => x.Groups.Any(y => y.Accesses.Any(y => y.Member.UserId == UserId)))
				.ToListAsync(cancellationToken);
			if (assetList.Count == 0)
			{
				return NotFound(nameof(Asset));
			}

			foreach (var asset in assetList)
			{
				var faults = await dataContext
					.Faults.Where(x => x.Event.AssetId == asset.Id)
					.ToListAsync(cancellationToken);
				if (faults.Count != 0)
				{
					dataContext.Faults.RemoveRange(faults);
				}

				var alarms = await dataContext
					.Alarms.Include(x => x.Event.Code)
					.Where(x => x.Event.AssetId == asset.Id)
					.ToListAsync(cancellationToken);
				if (alarms.Count != 0)
				{
					// Create reset records instead of deleting alarms
					var resets = alarms
						.Select(a =>
							AlarmReset.Create(a.Id, UserId, request.Message ?? string.Empty)
						)
						.ToList();
					await dataContext.AlarmResets.AddRangeAsync(resets, cancellationToken);
				}

				var information = UserLogInformation.Create(request.Message);
				var log = UserLog.Create(UserId, asset.Id, information);
				_ = await dataContext.UserLogs.AddAsync(log, cancellationToken);

				await NotifyAlarmsReset(asset, alarms, cancellationToken);
			}

			_ = await dataContext.SaveChangesAsync(cancellationToken);

			await transaction.CommitAsync(cancellationToken);

			return Ok();
		}
		catch (Exception ex)
		{
			await transaction.RollbackAsync(cancellationToken);
			return BadRequest(ex.Message);
		}
	}

	private async Task NotifyAlarmsReset(
		Asset asset,
		List<Alarm> alarms,
		CancellationToken cancellationToken
	)
	{
		var taskList = new List<Task>();

		foreach (var alarm in alarms)
		{
			taskList.Add(
				Task.Run(
					async () =>
					{
						var model = AlarmModel.FromEntity.Compile()(alarm);
						var userIds = asset.Organization.Members.Select(x => x.UserId).ToList();
						await notificationService.NotifyUsers(
							userIds,
							"ALARMS.RESOLVED",
							model,
							cancellationToken
						);
					},
					cancellationToken
				)
			);
		}

		await Task.WhenAll(taskList);
	}

	private static async ValueTask<Area> GetOrCreateArea(
		DataContext dataContext,
		AssetId assetId,
		AreaIndex areaIndex,
		CancellationToken cancellationToken
	)
	{
		var area = await dataContext.Areas.FirstOrDefaultAsync(
			x => x.AssetId == assetId && x.Index == areaIndex,
			cancellationToken
		);
		if (area is null)
		{
			var name = AreaName.Create($"Area {areaIndex.Value}");
			var description = AreaDescription.Create($"Area {areaIndex.Value}");
			area = Area.Create(areaIndex, name, description, assetId);
			_ = await dataContext.Areas.AddAsync(area, cancellationToken);

			_ = await dataContext.SaveChangesAsync(cancellationToken);
		}

		return area;
	}

	private static async ValueTask<Zone> GetOrCreateZone(
		DataContext dataContext,
		AreaId areaId,
		ZoneIndex zoneIndex,
		CancellationToken cancellationToken
	)
	{
		var zone = await dataContext.Zones.FirstOrDefaultAsync(
			x => x.AreaId == areaId && x.Index == zoneIndex,
			cancellationToken
		);
		if (zone is null)
		{
			var name = ZoneName.Create($"Zone {zoneIndex.Value}");
			var description = ZoneDescription.Create($"Zone {zoneIndex.Value}");
			zone = Zone.Create(zoneIndex, name, description, areaId);
			_ = await dataContext.Zones.AddAsync(zone, cancellationToken);

			_ = await dataContext.SaveChangesAsync(cancellationToken);
		}

		return zone;
	}

	[HttpPost("demo", Name = "CreateDemoEvent")]
	[Authorize(Roles = Roles.Manager)]
	[ProducesResponseType(StatusCodes.Status401Unauthorized)]
	[ProducesResponseType<string>(StatusCodes.Status404NotFound)]
	[ProducesResponseType<string>(StatusCodes.Status400BadRequest)]
	[ProducesResponseType<EventModel>(StatusCodes.Status200OK)]
	public async Task<IActionResult> CreateDemoEvent(
		CreateDemoEventRequest request,
		IValidator<CreateDemoEventRequest> validator,
		CancellationToken cancellationToken
	)
	{
		logger.LogInformation("Try to create event");

		var validationResult = await validator.ValidateAsync(request, cancellationToken);
		if (!validationResult.IsValid)
		{
			return this.BadRequest(validationResult);
		}

		var assetGroupId = new AssetGroupId(request.AssetGroupId);
		var assetGroup = await dataContext
			.AssetGroups.Where(x => x.Organization!.Members.Any(x => x.UserId == UserId))
			.FirstOrDefaultAsync(x => x.Id == assetGroupId, cancellationToken);
		if (assetGroup is null)
		{
			logger.LogInformation("Asset group is not found");

			return NotFound(nameof(AssetGroup));
		}

		logger.LogInformation("Asset group found to create event with Id={Id}", assetGroup.Id);

		var assetId = new AssetId(request.AssetId);
		var asset = await dataContext.Assets.FirstOrDefaultAsync(
			x => x.Id == assetId && x.Groups.Any(x => x.Id == assetGroupId),
			cancellationToken
		);
		if (asset is null)
		{
			logger.LogInformation("Asset is not found");
			return NotFound(nameof(Asset));
		}

		var token = await dataContext.Tokens.FirstOrDefaultAsync(
			x => x.Organization.Members.Any(x => x.UserId == UserId),
			cancellationToken
		);
		if (token is null)
		{
			logger.LogInformation("Any token is not found");
			return NotFound(nameof(Token));
		}

		var createEventRequest = new CreateEventRequest(
			token.Code.Value,
			asset.Id.Value,
			GetRandomEventCode(),
			Random.Shared.Next(1, 16),
			Random.Shared.Next(1, 16),
			DateTime.UtcNow,
			null
		);

		return await CreateEvent(createEventRequest, asset, cancellationToken);
	}

	private static int GetRandomEventCode()
	{
		var codes = new[] { 1101, 1111, 1311, 1131 };
		return codes[Random.Shared.Next(codes.Length)];
	}
}
