using MemberCenter.Application.Abstractions;
using MemberCenter.Application.Models.Newsletter;
using MemberCenter.Domain.Constants;
using MemberCenter.Domain.Entities;
using MemberCenter.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
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 int ConfirmTokenTtlDays = 7;
    private const int UnsubscribeTokenTtlDays = 7;

    private readonly MemberCenterDbContext _dbContext;

    public NewsletterService(MemberCenterDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<PendingSubscriptionResult?> SubscribeAsync(Guid listId, string email, Dictionary<string, object>? preferences)
    {
        var list = await _dbContext.NewsletterLists.FirstOrDefaultAsync(l => l.Id == listId);
        if (list is null)
        {
            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(ConfirmTokenTtlDays)
        });

        await _dbContext.SaveChangesAsync();

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

    public async Task<SubscriptionDto?> ConfirmAsync(string token)
    {
        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)
        {
            return null;
        }

        if (confirmToken.ExpiresAt < DateTimeOffset.UtcNow)
        {
            return null;
        }

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

        return MapSubscription(confirmToken.Subscription);
    }

    public async Task<SubscriptionDto?> UnsubscribeAsync(string token)
    {
        var tokenHash = HashToken(token, UnsubscribeTokenPurpose);
        var unsubscribeToken = await _dbContext.UnsubscribeTokens
            .Include(t => t.Subscription)
            .FirstOrDefaultAsync(t => t.TokenHash == tokenHash && t.ConsumedAt == null);

        if (unsubscribeToken?.Subscription is null)
        {
            return null;
        }

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

        return MapSubscription(unsubscribeToken.Subscription);
    }

    public async Task<string?> IssueUnsubscribeTokenAsync(Guid listId, string email)
    {
        var subscription = await _dbContext.NewsletterSubscriptions
            .Where(s => s.ListId == listId && s.Email == email)
            .OrderByDescending(s => s.CreatedAt)
            .FirstOrDefaultAsync();

        if (subscription is null)
        {
            return null;
        }

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

        await _dbContext.SaveChangesAsync();
        return token;
    }

    public async Task<SubscriptionDto?> GetPreferencesAsync(Guid listId, string email)
    {
        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)
    {
        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();

        return MapSubscription(subscription);
    }

    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);
    }
}
