using System.Collections.Concurrent;
using Email.Data.Repositories.Contracts;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Email.Data.Services;

/// <summary>
/// Singleton implementation of <see cref="IEmailTemplateService"/> that caches all email templates
/// from the database and refreshes them every 10 minutes.
/// Uses <see cref="IServiceScopeFactory"/> to resolve the scoped repository from a singleton lifetime.
/// </summary>
public sealed class EmailTemplateService : IEmailTemplateService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<EmailTemplateService> _logger;
    private ConcurrentDictionary<string, string> _valueCache = new(StringComparer.OrdinalIgnoreCase);
    private ConcurrentDictionary<string, string> _operatorCache = new(StringComparer.OrdinalIgnoreCase);
    private DateTime _loadedAt = DateTime.MinValue;
    private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(10);

    public EmailTemplateService(IServiceScopeFactory scopeFactory, ILogger<EmailTemplateService> logger)
    {
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    /// <inheritdoc />
    public string Get(string key, string language = "en")
    {
        EnsureCacheLoaded();

        if (_valueCache.TryGetValue(CacheKey(key, language), out var value))
            return value;

        if (!string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)
            && _valueCache.TryGetValue(CacheKey(key, "en"), out var fallback))
            return fallback;

        throw new InvalidOperationException(
            $"Email template not found: key='{key}', language='{language}'. " +
            $"This is a configuration error. Ensure the email.Templates table is properly seeded.");
    }

    /// <inheritdoc />
    public string Render(string key, string language, params (string Key, string Value)[] placeholders)
    {
        var template = Get(key, language);
        foreach (var (k, v) in placeholders)
            template = template.Replace($"{{{{{k}}}}}", v, StringComparison.OrdinalIgnoreCase);
        return template;
    }

    /// <inheritdoc />
    public string? GetOperatorCopy(string key, string language)
    {
        EnsureCacheLoaded();

        if (_operatorCache.TryGetValue(CacheKey(key, language), out var specific)
            && !string.IsNullOrWhiteSpace(specific))
            return specific;

        // Fall back to first non-empty OperatorCopy in the cache
        foreach (var val in _operatorCache.Values)
        {
            if (!string.IsNullOrWhiteSpace(val))
                return val;
        }

        return null;
    }

    /// <summary>
    /// Reloads all templates from the database when the cache TTL has expired.
    /// Swaps both caches atomically; logs an error and continues serving the stale cache on failure.
    /// </summary>
    private void EnsureCacheLoaded()
    {
        if (DateTime.UtcNow - _loadedAt < CacheTtl) return;

        try
        {
            using var scope = _scopeFactory.CreateScope();
            var repo = scope.ServiceProvider.GetRequiredService<IEmailTemplateRepository>();
            var rows = repo.GetAllAsync(CancellationToken.None).GetAwaiter().GetResult();

            var freshValues = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            var freshOperator = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);

            foreach (var row in rows)
            {
                freshValues[CacheKey(row.Key, row.Language)] = row.Value;
                freshOperator[CacheKey(row.Key, row.Language)] = row.OperatorCopy;
            }

            _valueCache = freshValues;
            _operatorCache = freshOperator;
            _loadedAt = DateTime.UtcNow;
            _logger.LogDebug("Email template cache refreshed. {Count} templates loaded.", rows.Count);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to refresh email template cache. Serving stale cache.");
        }
    }

    /// <summary>Builds the dictionary key used for both caches.</summary>
    private static string CacheKey(string key, string language) => $"{key}::{language}";
}
