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.AssetGroupManagement;
using Monitoring.Core.Domain.AssetManagement;
using Monitoring.Core.Domain.OrganizationManagement;
using Monitoring.Core.Domain.ServiceOrganizationManagement;
using Monitoring.Core.Domain.TokenManagement;
using Monitoring.Extensions;
using Monitoring.Features.Assets.CreateAsset;
using Monitoring.Features.Assets.GetAssetList;
using Monitoring.Features.Assets.UpdateAsset;
using Monitoring.Infrastructure.Data;
using Monitoring.Models;
using Monitoring.Repositories;
using Monitoring.Services;

namespace Monitoring.Features.Assets;

[Route("api/assets")]
[Authorize(Roles = $"{Roles.Manager},{Roles.Watcher}")]
public class AssetsController(
	DataContext dataContext,
	INotificationService notificationService,
	UsersService usersService,
	AssetRepository assetRepository,
	ILogger<AssetsController> logger
) : BaseController
{
	[HttpGet(Name = "GetAssetList")]
	[ProducesResponseType<TableDataResponseModel<AssetModel>>(StatusCodes.Status200OK)]
	public async Task<IActionResult> GetList(
		[FromQuery] GetAssetListRequest request,
		CancellationToken cancellationToken
	)
	{
		IQueryable<Asset> query = dataContext.Assets;

		if (request.OrganizationId.HasValue)
		{
			OrganizationId organizationId = new(request.OrganizationId.Value);
			query = query.Where(x => x.OrganizationId == organizationId);
		}

		if (request.GroupId.HasValue)
		{
			AssetGroupId groupId = new(request.GroupId.Value);
			query = query.Where(x => x.Groups.Any(x => x.Id == groupId));
		}

		if (request.HasAlarms.HasValue)
		{
			query = query.Where(x => x.Events!.Any(x => x.Faults!.Count != 0));
		}

		if (Role == Roles.Watcher)
		{
			query = query.Where(x =>
				x.Groups.Any(x => x.Accesses.Any(x => x.Member.UserId == UserId))
			);
		}

		if (!string.IsNullOrWhiteSpace(request.Search))
		{
			var search = request.Search.Trim().ToLowerInvariant();
			query = query.Where(x =>
				((string)x.Name).ToLower().Contains(search)
				|| ((string)x.Description).ToLower().Contains(search)
			);
		}

		var result = await query
			.AsNoTracking()
			.Where(x => x.Organization.Members.Any(member => member.UserId == UserId))
			.Select(AssetModel.FromEntity)
			.GetTable(request, cancellationToken);

		return base.Ok(result);
	}

	[HttpGet("{id:guid}", Name = "GetAsset")]
	[ProducesResponseType<string>(StatusCodes.Status404NotFound)]
	[ProducesResponseType<AssetModel>(StatusCodes.Status200OK)]
	public async Task<IActionResult> Get(Guid id, CancellationToken cancellationToken)
	{
		AssetId assetId = new(id);
		var model = await assetRepository.Get(assetId, UserId, cancellationToken);
		return model is null ? NotFound(nameof(Asset)) : Ok(model);
	}

	[HttpPost(Name = "CreateAsset")]
	[Authorize(Roles = Roles.Manager)]
	[ProducesResponseType(StatusCodes.Status409Conflict)]
	[ProducesResponseType<string>(StatusCodes.Status404NotFound)]
	[ProducesResponseType<AssetModel>(StatusCodes.Status200OK)]
	public async Task<IActionResult> Create(
		CreateAssetRequest request,
		CancellationToken cancellationToken
	)
	{
		var name = AssetName.Create(request.Name);
		OrganizationId organizationId = new(request.OrganizationId);

		var asset = await dataContext.Assets.FirstOrDefaultAsync(
			x => x.Name == name && x.Organization!.Members!.Any(member => member.UserId == UserId),
			cancellationToken
		);
		if (asset is not null)
		{
			return Conflict();
		}

		var organization = await dataContext.Organizations.FirstOrDefaultAsync(
			x => x.Id == organizationId && x.Members!.Any(member => member.UserId == UserId),
			cancellationToken
		);
		if (organization is null)
		{
			return NotFound(nameof(Organization));
		}

		var groupIds = request.GroupIds.Select(x => new AssetGroupId(x)).ToList();
		var groups = await dataContext
			.AssetGroups.Where(x => groupIds.Contains(x.Id))
			.ToListAsync(cancellationToken);
		if (groups.Count != groupIds.Count)
		{
			var notFoundGroupIds = groupIds.Except(groups.Select(x => x.Id));
			return NotFound($"Asset groups not found: {string.Join(", ", notFoundGroupIds)}");
		}

		ServiceOrganizationId? serviceOrganizationId = null;
		if (request.ServiceOrganizationId.HasValue)
		{
			serviceOrganizationId = new ServiceOrganizationId(request.ServiceOrganizationId.Value);
			var svcOrg = await dataContext.ServiceOrganizations.FirstOrDefaultAsync(
				x => x.Id == serviceOrganizationId,
				cancellationToken
			);
			if (svcOrg == null)
			{
				return NotFound(
					$"ServiceOrganization с Id {request.ServiceOrganizationId} не найдена"
				);
			}
		}

		var description = AssetDescription.Create(request.Description);
		var comment = AssetComment.Create(request.Comment);

		asset = Asset.Create(
			name,
			description,
			organizationId,
			new(
				request.Country,
				request.Region,
				request.City,
				request.Street,
				request.House,
				request.Building,
				request.Apartment,
				request.GateCode,
				request.IntercomCode
			),
			new(request.OwnerName, request.OwnerPassword, request.OwnerPhone1, request.OwnerPhone2),
			new(request.IdentificationPassword, request.DuressPassword),
			new Coordinates(request.Longitude, request.Latitude),
			serviceOrganizationId,
			request.Imei is not null ? Imei.Create(request.Imei) : Imei.Empty,
			comment
		);

		asset.Groups.AddRange(groups);

		_ = await dataContext.Assets.AddAsync(asset, cancellationToken);
		_ = await dataContext.SaveChangesAsync(cancellationToken);

		var assetModel = await dataContext
			.Assets.Where(x => x.Id == asset.Id)
			.Select(AssetModel.FromEntity)
			.FirstAsync(cancellationToken);

		await notificationService.NotifyUsers(
			await usersService.GetUserIdListByOrganization(asset.OrganizationId, cancellationToken),
			"ASSETS.CREATED",
			assetModel,
			cancellationToken
		);

		return Ok(assetModel);
	}

	[HttpPut("{id:guid}", Name = "UpdateAsset")]
	[Authorize(Roles = Roles.Manager)]
	[ProducesResponseType<string>(StatusCodes.Status404NotFound)]
	[ProducesResponseType(StatusCodes.Status204NoContent)]
	public async Task<IActionResult> Update(
		Guid id,
		UpdateAssetRequest request,
		CancellationToken cancellationToken
	)
	{
		AssetId assetId = new(id);
		var asset = await dataContext
			.Assets.Include(x => x.Groups)
			.FirstOrDefaultAsync(
				x =>
					x.Id == assetId
					&& x.Organization!.Members!.Any(member => member.UserId == UserId),
				cancellationToken
			);
		if (asset is null)
		{
			return NotFound(nameof(Asset));
		}

		var groupIds = request.GroupIds.Select(x => new AssetGroupId(x)).ToList();
		var groups = await dataContext
			.AssetGroups.Where(x => groupIds.Contains(x.Id))
			.ToListAsync(cancellationToken);
		if (groups.Count != groupIds.Count)
		{
			var notFoundGroupIds = groupIds.Except(groups.Select(x => x.Id));
			return NotFound($"Asset groups not found: {string.Join(", ", notFoundGroupIds)}");
		}

		if (request.ServiceOrganizationId.HasValue)
		{
			var serviceOrganizationId = new ServiceOrganizationId(
				request.ServiceOrganizationId.Value
			);
			var svcOrg = await dataContext.ServiceOrganizations.FirstOrDefaultAsync(
				x => x.Id == serviceOrganizationId,
				cancellationToken
			);
			if (svcOrg is null)
			{
				return NotFound(
					$"ServiceOrganization с Id {request.ServiceOrganizationId} не найдена"
				);
			}
		}

		var name = AssetName.Create(request.Name);
		var description = AssetDescription.Create(request.Description);
		var comment = AssetComment.Create(request.Comment ?? "");

		asset.Name = name;
		asset.Description = description;
		asset.Comment = comment;

		asset.Groups.Clear();
		asset.Groups.AddRange(groups);

		asset.UpdateServiceOrganization(
			request.ServiceOrganizationId.HasValue
				? new ServiceOrganizationId(request.ServiceOrganizationId.Value)
				: null
		);

		asset.Address = new(
			request.Country,
			request.Region,
			request.City,
			request.Street,
			request.House,
			request.Building,
			request.Apartment,
			request.GateCode,
			request.IntercomCode
		);
		asset.Owner = new(
			request.OwnerName,
			request.OwnerPassword,
			request.OwnerPhone1,
			request.OwnerPhone2
		);
		asset.Passwords = new(request.IdentificationPassword, request.DuressPassword);
		asset.Coordinates = new Coordinates(request.Longitude, request.Latitude);
		if (request.Imei is not null)
		{
			asset.Imei = Imei.Create(request.Imei);
		}

		await assetRepository.Update(asset, request.RowVersion, cancellationToken);

		var assetModel = await dataContext
			.Assets.Where(x => x.Id == asset.Id)
			.Select(AssetModel.FromEntity)
			.FirstAsync(cancellationToken);

		await notificationService.NotifyUsers(
			await usersService.GetUserIdListByOrganization(asset.OrganizationId, cancellationToken),
			"ASSETS.UPDATED",
			assetModel,
			cancellationToken
		);

		return NoContent();
	}

	[HttpDelete("{id:guid}", Name = "DeleteAsset")]
	[Authorize(Roles = Roles.Manager)]
	[ProducesResponseType<string>(StatusCodes.Status404NotFound)]
	[ProducesResponseType(StatusCodes.Status204NoContent)]
	public async Task<IActionResult> Delete(Guid id, CancellationToken cancellationToken)
	{
		AssetId assetId = new(id);
		var asset = await dataContext.Assets.FirstOrDefaultAsync(
			x => x.Id == assetId && x.Organization!.Members!.Any(member => member.UserId == UserId),
			cancellationToken
		);
		if (asset is null)
		{
			return NotFound(nameof(Asset));
		}

		var assetModel = await dataContext
			.Assets.Where(x => x.Id == asset.Id)
			.Select(AssetModel.FromEntity)
			.FirstAsync(cancellationToken);

		_ = dataContext.Assets.Remove(asset);
		_ = await dataContext.SaveChangesAsync(cancellationToken);

		await notificationService.NotifyUsers(
			await usersService.GetUserIdListByOrganization(asset.OrganizationId, cancellationToken),
			"ASSETS.DELETED",
			assetModel,
			cancellationToken
		);

		return NoContent();
	}

	[HttpPost("online-status", Name = "UpdateAssetOnlineStatus")]
	[AllowAnonymous]
	[ProducesResponseType<string>(StatusCodes.Status404NotFound)]
	[ProducesResponseType<string>(StatusCodes.Status401Unauthorized)]
	[ProducesResponseType(StatusCodes.Status204NoContent)]
	public async Task<IActionResult> UpdateOnlineStatus(
		UpdateAssetOnlineStatusRequest request,
		CancellationToken cancellationToken
	)
	{
		logger.LogInformation("Try to update online status");

		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("Invalid token");
		}

		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)
		{
			logger.LogInformation("Asset is not found");
			return NotFound(nameof(Asset));
		}

		asset.IsOnline = request.IsOnline;
		asset.LastStatusChange = DateTime.UtcNow;

		var assetStatus = Monitoring.Core.Domain.AssetStatus.AssetStatus.Create(
			asset.Id,
			request.IsOnline,
			"Status updated via API"
		);

		_ = await dataContext.AssetStatuses.AddAsync(assetStatus, cancellationToken);
		_ = await dataContext.SaveChangesAsync(cancellationToken);

		logger.LogInformation("Asset online status updated");

		var assetModel = await dataContext
			.Assets.Where(x => x.Id == asset.Id)
			.Select(AssetModel.FromEntity)
			.FirstAsync(cancellationToken);

		await notificationService.NotifyUsers(
			await usersService.GetUserIdListByOrganization(asset.OrganizationId, cancellationToken),
			"ASSETS.UPDATED",
			assetModel,
			cancellationToken
		);

		logger.LogInformation("Asset updated notification sent");

		return NoContent();
	}
}
