using MemberCenter.Application.Abstractions;
using MemberCenter.Application.Models.Newsletter;
using MemberCenter.Application.Models.Profile;
using MemberCenter.Domain.Constants;
using MemberCenter.Domain.Entities;
using MemberCenter.Infrastructure.Configuration;
using MemberCenter.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

namespace MemberCenter.Infrastructure.Services;

public sealed class NewsletterService : INewsletterService
{
    private const string ConfirmTokenPurpose = "confirm";
    private const string UnsubscribeTokenPurpose = "unsubscribe";
    private const string OneClickUnsubscribeTokenPurpose = "one_click_unsubscribe";
    private readonly MemberCenterDbContext _dbContext;
    private readonly IEmailBlacklistService _emailBlacklist;
    private readonly ISendEngineWebhookPublisher _webhookPublisher;
    private readonly ILogger<NewsletterService> _logger;
    private readonly NewsletterTokenOptions _tokenOptions;

    public NewsletterService(
        MemberCenterDbContext dbContext,
        IEmailBlacklistService emailBlacklist,
        ISendEngineWebhookPublisher webhookPublisher,
        ILogger<NewsletterService> logger,
        IOptions<NewsletterTokenOptions> tokenOptions)
    {
        _dbContext = dbContext;
        _emailBlacklist = emailBlacklist;
        _webhookPublisher = webhookPublisher;
        _logger = logger;
        _tokenOptions = tokenOptions.Value;
    }

    public async Task<PendingSubscriptionResult?> SubscribeAsync(Guid listId, string email, Dictionary<string, object>? preferences)
    {
        var normalizedEmail = email.Trim().ToLowerInvariant();
        _logger.LogInformation("Newsletter subscribe requested. list_id={ListId}, email={Email}", listId, normalizedEmail);

        if (await _emailBlacklist.IsBlacklistedAsync(email))
        {
            _logger.LogWarning("Newsletter subscribe blocked by blacklist. list_id={ListId}, email={Email}", listId, normalizedEmail);
            return null;
        }

        var list = await _dbContext.NewsletterLists.FirstOrDefaultAsync(l => l.Id == listId);
        if (list is null)
        {
            _logger.LogWarning("Newsletter subscribe failed: list not found. list_id={ListId}, email={Email}", listId, normalizedEmail);
            return null;
        }

        var subscription = await _dbContext.NewsletterSubscriptions
            .FirstOrDefaultAsync(s => s.ListId == listId && s.Email == email);

        if (subscription is null)
        {
            subscription = new NewsletterSubscription
            {
                Id = Guid.NewGuid(),
                ListId = listId,
                Email = email,
                Status = SubscriptionStatus.Pending,
                Preferences = ToJsonDocument(preferences)
            };
            _dbContext.NewsletterSubscriptions.Add(subscription);
        }
        else
        {
            // Keep active subscriptions active; only pending/unsubscribed subscriptions need reconfirmation.
            if (!string.Equals(subscription.Status, SubscriptionStatus.Active, StringComparison.OrdinalIgnoreCase))
            {
                subscription.Status = SubscriptionStatus.Pending;
            }

            subscription.Preferences = ToJsonDocument(preferences);
        }

        var confirmToken = CreateToken();
        _dbContext.UnsubscribeTokens.Add(new UnsubscribeToken
        {
            Id = Guid.NewGuid(),
            SubscriptionId = subscription.Id,
            TokenHash = HashToken(confirmToken, ConfirmTokenPurpose),
            ExpiresAt = DateTimeOffset.UtcNow.AddDays(_tokenOptions.ConfirmTokenLifetimeDays)
        });

        await _dbContext.SaveChangesAsync();

        _logger.LogInformation(
            "Newsletter subscribe pending token issued. subscription_id={SubscriptionId}, list_id={ListId}, email={Email}, status={Status}",
            subscription.Id,
            subscription.ListId,
            normalizedEmail,
            subscription.Status);

        return new PendingSubscriptionResult(MapSubscription(subscription), confirmToken);
    }

    public async Task<SubscriptionDto?> ConfirmAsync(string token)
    {
        _logger.LogInformation("Newsletter confirm requested.");
        var confirmToken = await FindTokenAsync(token, ConfirmTokenPurpose);
        if (confirmToken is null && token.Contains(' '))
        {
            // Backward compatibility: legacy Base64 tokens may decode '+' as space in query string.
            confirmToken = await FindTokenAsync(token.Replace(' ', '+'), ConfirmTokenPurpose);
        }

        if (confirmToken?.Subscription is null)
        {
            _logger.LogWarning("Newsletter confirm failed: token not found or already consumed.");
            return null;
        }

        if (await _emailBlacklist.IsBlacklistedAsync(confirmToken.Subscription.Email))
        {
            _logger.LogWarning(
                "Newsletter confirm blocked by blacklist. subscription_id={SubscriptionId}, list_id={ListId}, email={Email}",
                confirmToken.Subscription.Id,
                confirmToken.Subscription.ListId,
                confirmToken.Subscription.Email);
            return null;
        }

        if (confirmToken.ExpiresAt < DateTimeOffset.UtcNow)
        {
            _logger.LogWarning(
                "Newsletter confirm failed: token expired. subscription_id={SubscriptionId}, list_id={ListId}, email={Email}",
                confirmToken.Subscription.Id,
                confirmToken.Subscription.ListId,
                confirmToken.Subscription.Email);
            return null;
        }

        confirmToken.Subscription.Status = SubscriptionStatus.Active;
        confirmToken.ConsumedAt = DateTimeOffset.UtcNow;
        await _dbContext.SaveChangesAsync();

        var confirmed = MapSubscription(confirmToken.Subscription);
        _logger.LogInformation(
            "Newsletter confirm succeeded. subscription_id={SubscriptionId}, list_id={ListId}, email={Email}, status={Status}",
            confirmed.Id,
            confirmed.ListId,
            confirmed.Email,
            confirmed.Status);
        await PublishSubscriptionEventSafeAsync("subscription.activated", confirmed);
        return confirmed;
    }

    public async Task<SubscriptionDto?> UnsubscribeAsync(string token)
    {
        _logger.LogInformation("Newsletter unsubscribe requested.");
        var unsubscribeToken = await FindTokenAsync(token, UnsubscribeTokenPurpose)
            ?? await FindTokenAsync(token, OneClickUnsubscribeTokenPurpose);

        if (unsubscribeToken?.Subscription is null)
        {
            _logger.LogWarning("Newsletter unsubscribe failed: token not found or already consumed.");
            return null;
        }

        if (await _emailBlacklist.IsBlacklistedAsync(unsubscribeToken.Subscription.Email))
        {
            _logger.LogWarning(
                "Newsletter unsubscribe blocked by blacklist. subscription_id={SubscriptionId}, list_id={ListId}, email={Email}",
                unsubscribeToken.Subscription.Id,
                unsubscribeToken.Subscription.ListId,
                unsubscribeToken.Subscription.Email);
            return null;
        }

        unsubscribeToken.Subscription.Status = SubscriptionStatus.Unsubscribed;
        unsubscribeToken.ConsumedAt = DateTimeOffset.UtcNow;
        await _dbContext.SaveChangesAsync();

        var unsubscribed = MapSubscription(unsubscribeToken.Subscription);
        _logger.LogInformation(
            "Newsletter unsubscribe succeeded. subscription_id={SubscriptionId}, list_id={ListId}, email={Email}, status={Status}",
            unsubscribed.Id,
            unsubscribed.ListId,
            unsubscribed.Email,
            unsubscribed.Status);
        await PublishSubscriptionEventSafeAsync("subscription.unsubscribed", unsubscribed);
        return unsubscribed;
    }

    public async Task<string?> IssueUnsubscribeTokenAsync(Guid listId, string email)
    {
        var normalizedEmail = email.Trim().ToLowerInvariant();
        _logger.LogInformation("Newsletter unsubscribe-token requested. list_id={ListId}, email={Email}", listId, normalizedEmail);

        if (await _emailBlacklist.IsBlacklistedAsync(email))
        {
            _logger.LogWarning("Newsletter unsubscribe-token blocked by blacklist. list_id={ListId}, email={Email}", listId, normalizedEmail);
            return null;
        }

        var subscription = await _dbContext.NewsletterSubscriptions
            .Where(s => s.ListId == listId && s.Email == email)
            .OrderByDescending(s => s.CreatedAt)
            .FirstOrDefaultAsync();

        if (subscription is null)
        {
            _logger.LogWarning("Newsletter unsubscribe-token failed: subscription not found. list_id={ListId}, email={Email}", listId, normalizedEmail);
            return null;
        }

        var token = CreateToken();
        _dbContext.UnsubscribeTokens.Add(new UnsubscribeToken
        {
            Id = Guid.NewGuid(),
            SubscriptionId = subscription.Id,
            TokenHash = HashToken(token, UnsubscribeTokenPurpose),
            ExpiresAt = DateTimeOffset.UtcNow.AddDays(_tokenOptions.UnsubscribeTokenLifetimeDays)
        });

        await _dbContext.SaveChangesAsync();
        _logger.LogInformation(
            "Newsletter unsubscribe-token issued. subscription_id={SubscriptionId}, list_id={ListId}, email={Email}",
            subscription.Id,
            subscription.ListId,
            normalizedEmail);
        return token;
    }

    public async Task<string?> IssueOneClickUnsubscribeTokenAsync(Guid tenantId, Guid listId, Guid subscriberId)
    {
        var items = await IssueOneClickUnsubscribeTokensAsync(tenantId, listId, new[] { subscriberId });
        return items.FirstOrDefault()?.UnsubscribeToken;
    }

    public async Task<IReadOnlyList<OneClickUnsubscribeTokenResult>> IssueOneClickUnsubscribeTokensAsync(
        Guid tenantId,
        Guid listId,
        IReadOnlyList<Guid> subscriberIds)
    {
        var requested = subscriberIds
            .Where(x => x != Guid.Empty)
            .Distinct()
            .ToList();

        if (requested.Count == 0)
        {
            return Array.Empty<OneClickUnsubscribeTokenResult>();
        }

        var subscriptions = await (
            from s in _dbContext.NewsletterSubscriptions
            join l in _dbContext.NewsletterLists on s.ListId equals l.Id
            where l.TenantId == tenantId && s.ListId == listId && requested.Contains(s.Id)
            select s)
            .ToListAsync();

        var byId = subscriptions.ToDictionary(x => x.Id, x => x);
        var normalizedEmails = subscriptions
            .Select(s => s.Email.ToLowerInvariant())
            .Distinct()
            .ToList();

        var blacklistedEmails = await _dbContext.EmailBlacklist
            .Where(x => normalizedEmails.Contains(x.Email))
            .Select(x => x.Email)
            .ToListAsync();
        var blacklistedSet = blacklistedEmails.ToHashSet(StringComparer.OrdinalIgnoreCase);

        var results = new List<OneClickUnsubscribeTokenResult>(requested.Count);
        foreach (var subscriberId in requested)
        {
            if (!byId.TryGetValue(subscriberId, out var subscription))
            {
                results.Add(new OneClickUnsubscribeTokenResult(subscriberId, null, "not_found"));
                continue;
            }

            if (blacklistedSet.Contains(subscription.Email))
            {
                results.Add(new OneClickUnsubscribeTokenResult(subscriberId, null, "blacklisted"));
                continue;
            }

            var token = CreateToken();
            _dbContext.UnsubscribeTokens.Add(new UnsubscribeToken
            {
                Id = Guid.NewGuid(),
                SubscriptionId = subscription.Id,
                TokenHash = HashToken(token, OneClickUnsubscribeTokenPurpose),
                ExpiresAt = DateTimeOffset.UtcNow.AddDays(_tokenOptions.OneClickTokenLifetimeDays)
            });

            results.Add(new OneClickUnsubscribeTokenResult(subscriberId, token, "issued"));
        }

        await _dbContext.SaveChangesAsync();
        _logger.LogInformation(
            "One-click token batch issued. tenant_id={TenantId}, list_id={ListId}, requested={RequestedCount}, issued={IssuedCount}",
            tenantId,
            listId,
            requested.Count,
            results.Count(x => string.Equals(x.Status, "issued", StringComparison.OrdinalIgnoreCase)));

        return results;
    }

    public async Task<SubscriptionDto?> GetPreferencesAsync(Guid listId, string email)
    {
        if (await _emailBlacklist.IsBlacklistedAsync(email))
        {
            return null;
        }

        var subscription = await _dbContext.NewsletterSubscriptions
            .Where(s => s.ListId == listId && s.Email == email)
            .OrderByDescending(s => s.CreatedAt)
            .FirstOrDefaultAsync();

        return subscription is null ? null : MapSubscription(subscription);
    }

    public async Task<SubscriptionDto?> UpdatePreferencesAsync(Guid listId, string email, Dictionary<string, object> preferences)
    {
        if (await _emailBlacklist.IsBlacklistedAsync(email))
        {
            return null;
        }

        var subscription = await _dbContext.NewsletterSubscriptions
            .Where(s => s.ListId == listId && s.Email == email)
            .OrderByDescending(s => s.CreatedAt)
            .FirstOrDefaultAsync();
        if (subscription is null)
        {
            return null;
        }

        subscription.Preferences = ToJsonDocument(preferences);
        await _dbContext.SaveChangesAsync();

        var updated = MapSubscription(subscription);
        await PublishSubscriptionEventSafeAsync("preferences.updated", updated);
        return updated;
    }

    public async Task<IReadOnlyList<SubscriptionDto>> ListSubscriptionsAsync(Guid listId)
    {
        var blacklisted = _dbContext.EmailBlacklist.Select(x => x.Email);
        var subscriptions = await _dbContext.NewsletterSubscriptions
            .Where(s => s.ListId == listId && !blacklisted.Contains(s.Email.ToLower()))
            .OrderByDescending(s => s.CreatedAt)
            .ToListAsync();

        return subscriptions.Select(MapSubscription).ToList();
    }

    public async Task<IReadOnlyList<UserSubscriptionSummaryDto>> ListSubscriptionsForUserAsync(Guid userId)
    {
        return await (
            from subscription in _dbContext.NewsletterSubscriptions
            join list in _dbContext.NewsletterLists on subscription.ListId equals list.Id
            join tenant in _dbContext.Tenants on list.TenantId equals tenant.Id
            where subscription.UserId == userId
            orderby tenant.Name, list.Name
            select new UserSubscriptionSummaryDto(
                subscription.Id,
                subscription.ListId,
                tenant.Id,
                tenant.Name,
                list.Name,
                subscription.Email,
                subscription.Status,
                subscription.CreatedAt))
            .ToListAsync();
    }

    public async Task<UserSubscriptionSummaryDto?> UnsubscribeForUserAsync(Guid userId, Guid subscriptionId)
    {
        var subscription = await _dbContext.NewsletterSubscriptions
            .FirstOrDefaultAsync(x => x.Id == subscriptionId && x.UserId == userId);
        if (subscription is null)
        {
            return null;
        }

        if (await _emailBlacklist.IsBlacklistedAsync(subscription.Email))
        {
            return null;
        }

        subscription.Status = SubscriptionStatus.Unsubscribed;
        await _dbContext.SaveChangesAsync();

        var updated = await (
            from saved in _dbContext.NewsletterSubscriptions
            join list in _dbContext.NewsletterLists on saved.ListId equals list.Id
            join tenant in _dbContext.Tenants on list.TenantId equals tenant.Id
            where saved.Id == subscriptionId
            select new UserSubscriptionSummaryDto(
                saved.Id,
                saved.ListId,
                tenant.Id,
                tenant.Name,
                list.Name,
                saved.Email,
                saved.Status,
                saved.CreatedAt))
            .FirstAsync();

        await PublishSubscriptionEventSafeAsync("subscription.unsubscribed", MapSubscription(subscription));
        return updated;
    }

    private static string CreateToken()
    {
        var bytes = RandomNumberGenerator.GetBytes(32);
        return Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_');
    }

    private static string HashToken(string token, string purpose)
    {
        using var sha = SHA256.Create();
        var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes($"{purpose}:{token}"));
        return Convert.ToHexString(bytes).ToLowerInvariant();
    }

    private static JsonDocument ToJsonDocument(Dictionary<string, object>? value)
    {
        if (value is null)
        {
            return JsonDocument.Parse("{}");
        }

        return JsonDocument.Parse(JsonSerializer.Serialize(value));
    }

    private static SubscriptionDto MapSubscription(NewsletterSubscription subscription)
    {
        return new SubscriptionDto(
            subscription.Id,
            subscription.ListId,
            subscription.Email,
            subscription.Status,
            subscription.Preferences.RootElement.Clone(),
            subscription.CreatedAt);
    }

    private Task<UnsubscribeToken?> FindTokenAsync(string token, string purpose)
    {
        var tokenHash = HashToken(token, purpose);
        return _dbContext.UnsubscribeTokens
            .Include(t => t.Subscription)
            .FirstOrDefaultAsync(t => t.TokenHash == tokenHash && t.ConsumedAt == null);
    }

    private async Task PublishSubscriptionEventSafeAsync(string eventType, SubscriptionDto subscription)
    {
        var tenantId = await _dbContext.NewsletterLists
            .Where(x => x.Id == subscription.ListId)
            .Select(x => x.TenantId)
            .FirstOrDefaultAsync();

        if (tenantId == Guid.Empty)
        {
            _logger.LogWarning(
                "Skip webhook publish because list {ListId} has no tenant mapping",
                subscription.ListId);
            return;
        }

        await _webhookPublisher.PublishSubscriptionEventAsync(eventType, tenantId, subscription);
    }
}
