📄 MatDenDagen/Services/DateService.cs
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using MatDenDagen.Infrastructure.Storage.Database;
using MatDenDagen.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;

namespace MatDenDagen.Services;

public sealed class DateService(ILogger<DateService> logger, Random random, QuestionnaireContext questionnaireContext)
{
    private const string DateSpanConfigKey = "DateSpan";
    private const string DateConfigKey = "DateConfig";

    public async Task<DateSpan?> GetDateSpanAsync()
    {
        var config = await questionnaireContext.Configs.FirstOrDefaultAsync(c => c.Key == DateSpanConfigKey);

        if (config?.Value == null)
        {
            return null;
        }

        try
        {
            return JsonSerializer.Deserialize<DateSpan>(config.Value);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to deserialize DateSpan from database");
            return null;
        }
    }

    public async Task SaveDateSpanAsync(DateSpan dateSpan)
    {
        if (!dateSpan.IsValid)
        {
            throw new ArgumentException("Invalid date span: start date must be before or equal to end date");
        }

        var config = await questionnaireContext.Configs.FirstOrDefaultAsync(c => c.Key == DateSpanConfigKey);

        var jsonValue = JsonSerializer.Serialize(dateSpan, ConfigJsonSerializerContext.Default.DateSpan);

        if (config == null)
        {
            questionnaireContext.Configs.Add(new Config { Key = DateSpanConfigKey, Value = jsonValue });
        }
        else
        {
            config.Value = jsonValue;
            questionnaireContext.Configs.Update(config);
        }

        await questionnaireContext.SaveChangesAsync();
    }

    public async Task<DateConfig?> GetDateConfigAsync()
    {
        var config = await questionnaireContext.Configs.FirstOrDefaultAsync(c => c.Key == DateConfigKey);

        if (config?.Value == null)
        {
            return null;
        }

        try
        {
            return JsonSerializer.Deserialize<DateConfig>(config.Value);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to deserialize DateConfig from database");
            return null;
        }
    }

    public async Task<DateOnly?> RandomizeDateAsync()
    {
        var dateSpan = await GetDateSpanAsync();

        if (dateSpan == null || !dateSpan.IsValid)
        {
            logger.LogWarning("Cannot randomize date: no valid date span configured");
            return null;
        }

        var randomDate = dateSpan.GetRandomDate(random);

        var dateConfig = new DateConfig { TheDay = randomDate };
        var jsonValue = JsonSerializer.Serialize(dateConfig, ConfigJsonSerializerContext.Default.DateConfig);

        var config = await questionnaireContext.Configs.FirstOrDefaultAsync(c => c.Key == DateConfigKey);

        if (config == null)
        {
            questionnaireContext.Configs.Add(new Config { Key = DateConfigKey, Value = jsonValue });
        }
        else
        {
            config.Value = jsonValue;
            questionnaireContext.Configs.Update(config);
        }

        await questionnaireContext.SaveChangesAsync();

        logger.LogInformation("Randomized date to {RandomDate}", randomDate);
        return randomDate;
    }
}

[JsonSerializable(typeof(DateSpan))]
[JsonSerializable(typeof(DateConfig))]
public sealed partial class ConfigJsonSerializerContext : JsonSerializerContext;

public static class DateServiceCollectionExtensions
{
    public static IServiceCollection AddDateService(this IServiceCollection services)
    {
        services.TryAddSingleton(Random.Shared);
        services.AddTransient<DateService>();
        return services;
    }
}