using Microsoft.Extensions.Caching.Hybrid;
using StackExchange.Redis;

namespace Jiaowu.Api.Infrastructure.Caching;

public enum AppCacheProfile
{
    ReferenceData,
    PublishedTimetable,
    Analytics
}

public interface IAppCache
{
    Task<T> GetOrCreateAsync<T>(
        string key,
        Func<CancellationToken, Task<T>> factory,
        AppCacheProfile profile,
        IReadOnlyCollection<string> tags,
        CancellationToken cancellationToken);

    ValueTask RemoveByTagAsync(
        string tag,
        CancellationToken cancellationToken = default);
}

public sealed class HybridAppCache(
    HybridCache cache,
    AppCacheOptions options,
    IHostEnvironment environment,
    ILogger<HybridAppCache> logger) : IAppCache
{
    private readonly string prefix = BuildPrefix(options.KeyPrefix, environment.EnvironmentName);

    public async Task<T> GetOrCreateAsync<T>(
        string key,
        Func<CancellationToken, Task<T>> factory,
        AppCacheProfile profile,
        IReadOnlyCollection<string> tags,
        CancellationToken cancellationToken)
    {
        if (!options.Enabled)
            return await factory(cancellationToken);

        var sourceCompleted = false;
        T? sourceValue = default;
        try
        {
            return await cache.GetOrCreateAsync(
                $"{prefix}:{key}",
                async token =>
                {
                    sourceValue = await factory(token);
                    sourceCompleted = true;
                    return sourceValue;
                },
                GetEntryOptions(profile),
                tags.Select(tag => $"{prefix}:tag:{tag}"),
                cancellationToken);
        }
        catch (RedisException exception)
        {
            logger.LogWarning(
                exception,
                "Redis cache operation failed for {CacheKey}; using the source directly.",
                key);
            if (sourceCompleted)
                return sourceValue!;
            return await factory(cancellationToken);
        }
    }

    public async ValueTask RemoveByTagAsync(
        string tag,
        CancellationToken cancellationToken = default)
    {
        if (!options.Enabled)
            return;

        try
        {
            await cache.RemoveByTagAsync(
                $"{prefix}:tag:{tag}",
                cancellationToken);
        }
        catch (RedisException exception)
        {
            logger.LogWarning(
                exception,
                "Redis cache invalidation failed for tag {CacheTag}; TTL will bound staleness.",
                tag);
        }
    }

    private HybridCacheEntryOptions GetEntryOptions(AppCacheProfile profile) =>
        profile switch
        {
            AppCacheProfile.ReferenceData => new HybridCacheEntryOptions
            {
                Expiration = TimeSpan.FromMinutes(options.ReferenceExpirationMinutes),
                LocalCacheExpiration =
                    TimeSpan.FromSeconds(options.ReferenceLocalExpirationSeconds)
            },
            AppCacheProfile.PublishedTimetable => new HybridCacheEntryOptions
            {
                Expiration = TimeSpan.FromMinutes(options.TimetableExpirationMinutes),
                LocalCacheExpiration =
                    TimeSpan.FromSeconds(options.TimetableLocalExpirationSeconds)
            },
            AppCacheProfile.Analytics => new HybridCacheEntryOptions
            {
                Expiration = TimeSpan.FromMinutes(options.AnalyticsExpirationMinutes),
                LocalCacheExpiration =
                    TimeSpan.FromSeconds(options.AnalyticsLocalExpirationSeconds)
            },
            _ => throw new ArgumentOutOfRangeException(nameof(profile), profile, null)
        };

    private static string BuildPrefix(string configuredPrefix, string environmentName)
    {
        var prefixValue = string.IsNullOrWhiteSpace(configuredPrefix)
            ? "jiaowu:v1"
            : configuredPrefix.Trim().Trim(':');
        var environmentValue = string.Concat(
            environmentName.Trim().ToLowerInvariant()
                .Select(character => char.IsLetterOrDigit(character) ? character : '-'));
        return $"{prefixValue}:{environmentValue}";
    }
}

public sealed class NoOpAppCache : IAppCache
{
    public static NoOpAppCache Instance { get; } = new();

    private NoOpAppCache()
    {
    }

    public Task<T> GetOrCreateAsync<T>(
        string key,
        Func<CancellationToken, Task<T>> factory,
        AppCacheProfile profile,
        IReadOnlyCollection<string> tags,
        CancellationToken cancellationToken) =>
        factory(cancellationToken);

    public ValueTask RemoveByTagAsync(
        string tag,
        CancellationToken cancellationToken = default) =>
        ValueTask.CompletedTask;
}

public static class AppCacheKeys
{
    public const string ActivationOptions = "auth:activation-options";
    public const string Dashboard = "dashboard:summary";
    public const string TimetableOptions = "timetable:options";

    public static string BaseData(string kind) => $"base-data:{kind}";

    public static string PublishedTimetable(
        string resourceType,
        Guid resourceId,
        Guid? academicTermId) =>
        $"timetable:published:{academicTermId?.ToString("N") ?? "current"}:" +
        $"{resourceType}:{resourceId:N}";

    public static string Statistics(
        string area,
        string dataScope,
        Guid? effectiveCollegeId,
        params string?[] filters)
    {
        static string Normalize(string? value) =>
            string.IsNullOrWhiteSpace(value)
                ? "-"
                : value.Trim().ToLowerInvariant();

        var filterPart = filters.Length == 0
            ? "all"
            : string.Join(':', filters.Select(Normalize));
        return $"statistics:v2:{Normalize(area)}:scope:{Normalize(dataScope)}:" +
               $"college:{effectiveCollegeId?.ToString("N") ?? "all"}:{filterPart}";
    }

    public static string CourseGradeStatistics(Guid gradeSheetId) =>
        $"grade-statistics:sheet:{gradeSheetId:N}";

    public static string TeachingTaskGradeAnalytics(Guid gradeSheetId) =>
        $"grade-analytics:sheet:{gradeSheetId:N}:v1";
}

public static class AppCacheTags
{
    public const string BaseData = "base-data";
    public const string Analytics = "analytics";
    public const string CourseGradeStatistics = "grade-statistics";
    public const string Timetables = "timetables";
    public const string TimetableOptions = "timetable:options";

    public static string Timetable(Guid academicTermId) =>
        $"timetable:term:{academicTermId:N}";
}
