namespace MyOffice.Web.Auth;

using Core.Extensions;
using Core.Identity;
using Data.Models.Users;
using Data.Repositories.Users;
using Identity.Domain;
using Identity.ExternalProviders;
using Identity.Repositories;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using OpenIddict.Server;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Server.OpenIddictServerEvents;

public sealed class ExternalGrantHandler : IOpenIddictServerHandler<HandleTokenRequestContext>
{
	private readonly AppUserManager _userManager;
	private readonly IUserExternalRepository _userExternalRepository;
	private readonly IEnumerable<IExternalProviderValidator> _externalProviderValidators;
	private readonly IHttpContextAccessor _httpContextAccessor;

	public ExternalGrantHandler(
		AppUserManager userManager,
		IUserExternalRepository userExternalRepository,
		IEnumerable<IExternalProviderValidator> externalProviderValidators,
		IHttpContextAccessor httpContextAccessor
	)
	{
		_userManager = userManager;
		_userExternalRepository = userExternalRepository;
		_externalProviderValidators = externalProviderValidators;
		_httpContextAccessor = httpContextAccessor;
	}

	public async ValueTask HandleAsync(HandleTokenRequestContext context)
	{
		if (!string.Equals(context.Request.GrantType, OpenIddictAuthConstants.ExternalGrantType, StringComparison.Ordinal))
			return;

		var provider = context.Request.GetParameter("provider")?.ToString();
		if (provider.IsMissing())
		{
			context.Reject(Errors.InvalidRequest, "The provider parameter is required.");
			return;
		}

		var validator = _externalProviderValidators.FirstOrDefault(x => x.Provider.EqualsIgnoreCase(provider));
		if (validator is null || !validator.IsConfigured)
		{
			context.Reject(Errors.InvalidRequest, $"Provider not supported: {provider}");
			return;
		}

		var token = context.Request.GetParameter("token")?.ToString();
		if (token.IsMissing())
		{
			context.Reject(Errors.InvalidRequest, "The token parameter is required.");
			return;
		}

		var validationResult = await validator.ValidateAsync(token);
		if (!validationResult.IsSuccessed)
		{
			context.Reject(Errors.InvalidGrant, "Token not valid.");
			return;
		}

		if (_httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated == true)
		{
			context.Reject(Errors.InvalidGrant, "Authentication failed.");
			return;
		}

		var user = await _userManager.FindByEmailAsync(validationResult.Email!);
		if (user is null)
		{
			if (!validationResult.EmailVerified)
			{
				context.Reject(Errors.InvalidGrant, "Email not confirmed.");
				return;
			}

			user = await CreateUserAsync(validationResult.Email!, validationResult.EmailVerified, validationResult.FullName);
			if (user is null)
			{
				context.Reject(Errors.InvalidGrant, "Unable to create user.");
				return;
			}
		}

		var externalLogin = await _userExternalRepository.GetByUserIdAsync(user.Id, provider)
			?? AddExternalLogin(user, validationResult.Email!, validationResult.ExternalId!, provider);

		context.SignIn(OpenIddictClaimsHelper.CreatePrincipal(user, context.Request.GetScopes()));
	}

	private async Task<ApplicationUser<Guid>?> CreateUserAsync(string email, bool isEmailConfirmed, string? fullName)
	{
		var user = new ApplicationUser<Guid>
		{
			Id = Guid.NewGuid(),
			UserName = email,
			Email = email,
			IsEmailConfirmed = isEmailConfirmed,
			FullName = fullName
		};

		var password = "Qq1!_" + Guid.NewGuid();
		var result = await _userManager.CreateAsync(user, password);
		return result.Succeeded ? user : null;
	}

	private UserExternal AddExternalLogin(
		ApplicationUser<Guid> user,
		string email,
		string externalId,
		string provider
	)
	{
		var existing = _userExternalRepository.GetByUserId(user.Id, provider);
		if (existing is not null)
			return existing;

		var claim = new UserExternal
		{
			UserId = user.Id,
			CreatedOn = DateTime.UtcNow,
			Provider = provider.ToLower(),
			ExternalId = externalId,
			Email = email
		};
		_userExternalRepository.AddUserExternal(claim);

		return claim;
	}
}
