using System.Text.Json;
using MemberCenter.Application.Abstractions;
using MemberCenter.Application.Models.Account;
using MemberCenter.Domain.Entities;
using MemberCenter.Infrastructure.Identity;
using MemberCenter.Infrastructure.Persistence;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using MemberCenter.Application.Models.Newsletter;

namespace MemberCenter.Infrastructure.Services;

public sealed class AccountProvisioningService : IAccountProvisioningService
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly MemberCenterDbContext _dbContext;
    private readonly ISendEngineWebhookPublisher _webhookPublisher;
    private readonly IAuditLogWriter _auditLogWriter;
    private readonly ILogger<AccountProvisioningService> _logger;

    public AccountProvisioningService(
        UserManager<ApplicationUser> userManager,
        MemberCenterDbContext dbContext,
        ISendEngineWebhookPublisher webhookPublisher,
        IAuditLogWriter auditLogWriter,
        ILogger<AccountProvisioningService> logger)
    {
        _userManager = userManager;
        _dbContext = dbContext;
        _webhookPublisher = webhookPublisher;
        _auditLogWriter = auditLogWriter;
        _logger = logger;
    }

    public async Task<AccountProvisioningResult> RegisterLocalAsync(string email, string password)
    {
        var normalizedEmail = NormalizeEmail(email);
        var user = new ApplicationUser
        {
            Id = Guid.NewGuid(),
            UserName = normalizedEmail,
            Email = normalizedEmail,
            EmailConfirmed = false
        };

        var result = await _userManager.CreateAsync(user, password);
        if (!result.Succeeded)
        {
            return Failed(result);
        }

        _dbContext.UserProfiles.Add(new UserProfile
        {
            UserId = user.Id,
            LastName = string.Empty,
            FirstName = string.Empty,
            Gender = "unspecified",
            UpdatedAt = DateTimeOffset.UtcNow
        });
        await _dbContext.SaveChangesAsync();

        var linkedSubscriptionsCount = await LinkSubscriptionsAndAuditAsync(user, "local_registration", new
        {
            email = normalizedEmail
        });

        await _auditLogWriter.WriteAsync("user", user.Id, "account.registered", new
        {
            user_id = user.Id,
            email = normalizedEmail,
            registration_type = "local"
        });

        return new AccountProvisioningResult(
            true,
            user.Id,
            user.Email,
            user.EmailConfirmed,
            true,
            false,
            linkedSubscriptionsCount,
            Array.Empty<string>());
    }

    public async Task<AccountProvisioningResult> ProvisionExternalLoginAsync(
        string loginProvider,
        string providerKey,
        string? email,
        bool emailVerified)
    {
        var existingByLogin = await _userManager.FindByLoginAsync(loginProvider, providerKey);
        if (existingByLogin is not null)
        {
            var linkedSubscriptionsCount = await LinkSubscriptionsAndAuditAsync(existingByLogin, "external_login_reuse", new
            {
                login_provider = loginProvider
            });

            return new AccountProvisioningResult(
                true,
                existingByLogin.Id,
                existingByLogin.Email,
                existingByLogin.EmailConfirmed,
                false,
                false,
                linkedSubscriptionsCount,
                Array.Empty<string>());
        }

        if (string.IsNullOrWhiteSpace(email))
        {
            return new AccountProvisioningResult(
                false,
                null,
                null,
                false,
                false,
                false,
                0,
                new[] { "External login did not provide an email address." });
        }

        var normalizedEmail = NormalizeEmail(email);
        var user = await _userManager.FindByEmailAsync(normalizedEmail);
        var createdUser = false;

        if (user is null)
        {
            user = new ApplicationUser
            {
                Id = Guid.NewGuid(),
                UserName = normalizedEmail,
                Email = normalizedEmail,
                EmailConfirmed = emailVerified
            };

            var createResult = await _userManager.CreateAsync(user);
            if (!createResult.Succeeded)
            {
                return Failed(createResult);
            }

            _dbContext.UserProfiles.Add(new UserProfile
            {
                UserId = user.Id,
                LastName = string.Empty,
                FirstName = string.Empty,
                Gender = "unspecified",
                UpdatedAt = DateTimeOffset.UtcNow
            });
            await _dbContext.SaveChangesAsync();

            createdUser = true;
        }
        else if (emailVerified && !user.EmailConfirmed)
        {
            user.EmailConfirmed = true;
            await _userManager.UpdateAsync(user);
        }

        var addLoginResult = await _userManager.AddLoginAsync(user, new UserLoginInfo(loginProvider, providerKey, loginProvider));
        if (!addLoginResult.Succeeded
            && addLoginResult.Errors.All(x => x.Code != nameof(IdentityErrorDescriber.LoginAlreadyAssociated)))
        {
            return Failed(addLoginResult);
        }

        var linkedSubscriptions = await LinkSubscriptionsAndAuditAsync(user, "external_login_linked", new
        {
            login_provider = loginProvider,
            created_user = createdUser
        });

        await _auditLogWriter.WriteAsync("user", user.Id, "account.external_login_linked", new
        {
            user_id = user.Id,
            email = user.Email,
            login_provider = loginProvider,
            created_user = createdUser
        });

        return new AccountProvisioningResult(
            true,
            user.Id,
            user.Email,
            user.EmailConfirmed,
            createdUser,
            true,
            linkedSubscriptions,
            Array.Empty<string>());
    }

    private async Task<int> LinkSubscriptionsAndAuditAsync(ApplicationUser user, string source, object payload)
    {
        var email = NormalizeEmail(user.Email ?? user.UserName ?? string.Empty);
        var subscriptions = await _dbContext.NewsletterSubscriptions
            .Where(x => x.UserId == null && x.Email.ToLower() == email)
            .ToListAsync();

        if (subscriptions.Count == 0)
        {
            return 0;
        }

        foreach (var subscription in subscriptions)
        {
            subscription.UserId = user.Id;
        }

        var linkedSubscriptions = subscriptions
            .Select(subscription => new SubscriptionDto(
                subscription.Id,
                subscription.ListId,
                subscription.Email,
                subscription.Status,
                subscription.Preferences.RootElement.Clone(),
                subscription.CreatedAt))
            .ToList();

        _dbContext.AuditLogs.Add(new AuditLog
        {
            Id = Guid.NewGuid(),
            ActorType = "system",
            ActorId = user.Id,
            Action = "subscription.linked_to_user",
            Payload = JsonDocument.Parse(JsonSerializer.Serialize(new
            {
                user_id = user.Id,
                email,
                linked_subscriptions = subscriptions.Count,
                source,
                metadata = payload
            }))
        });

        await _dbContext.SaveChangesAsync();

        await PublishLinkedSubscriptionEventsAsync(linkedSubscriptions);
        return subscriptions.Count;
    }

    private async Task PublishLinkedSubscriptionEventsAsync(IReadOnlyList<SubscriptionDto> subscriptions)
    {
        if (subscriptions.Count == 0)
        {
            return;
        }

        var listIds = subscriptions.Select(x => x.ListId).Distinct().ToList();
        var tenantMap = await _dbContext.NewsletterLists
            .Where(x => listIds.Contains(x.Id))
            .ToDictionaryAsync(x => x.Id, x => x.TenantId);

        foreach (var subscription in subscriptions)
        {
            if (!tenantMap.TryGetValue(subscription.ListId, out var tenantId) || tenantId == Guid.Empty)
            {
                _logger.LogWarning(
                    "Skip linked subscription event because list {ListId} has no tenant mapping",
                    subscription.ListId);
                continue;
            }

            await _webhookPublisher.PublishSubscriptionEventAsync("subscription.linked_to_user", tenantId, subscription);
        }
    }

    private static AccountProvisioningResult Failed(IdentityResult result) =>
        new(
            false,
            null,
            null,
            false,
            false,
            false,
            0,
            result.Errors.Select(x => x.Description).ToArray());

    private static string NormalizeEmail(string email) =>
        email.Trim().ToLowerInvariant();
}
