using System.Text.Json;
using System.Text.Json.Serialization;

namespace TeleWave.Application.Programming.Templates;

/// <summary>Разовый диапазон дат.</summary>
public sealed record DateRange(DateOnly From, DateOnly To);

/// <summary>Ежегодно повторяющийся период — «перед Новым годом» задаётся один раз, а не каждый год.</summary>
public sealed record AnnualRange(int FromMonth, int FromDay, int ToMonth, int ToDay);

/// <summary>
/// Когда действует слой сетки. Пустая применимость — слой действует всегда. Заполненные разделы
/// объединяются по ИЛИ: слой применим, если дата подходит хотя бы под одно из условий.
/// </summary>
public sealed record LayerApplicability(
    /// <summary>Дни недели вещательных суток (0=Вс..6=Сб).</summary>
    IReadOnlyList<int>? Weekdays = null,
    IReadOnlyList<DateRange>? DateRanges = null,
    IReadOnlyList<AnnualRange>? AnnualRanges = null,
    IReadOnlyList<DateOnly>? SpecificDates = null
)
{
    private static readonly JsonSerializerOptions Options = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        Converters = { new JsonStringEnumConverter() },
    };

    public bool IsEmpty =>
        Weekdays is null or { Count: 0 }
        && DateRanges is null or { Count: 0 }
        && AnnualRanges is null or { Count: 0 }
        && SpecificDates is null or { Count: 0 };

    public string ToJson() => JsonSerializer.Serialize(this, Options);

    public static LayerApplicability? FromJson(string? json)
    {
        if (string.IsNullOrWhiteSpace(json))
            return null;

        try
        {
            return JsonSerializer.Deserialize<LayerApplicability>(json, Options);
        }
        catch (JsonException)
        {
            return null;
        }
    }

    /// <summary>
    /// Действует ли слой в эту дату (дата вещательных суток, не календарная — ночь после полуночи
    /// принадлежит предыдущему дню).
    /// </summary>
    public bool Covers(DateOnly date)
    {
        if (IsEmpty)
            return true;

        if (Weekdays is { Count: > 0 } weekdays && weekdays.Contains((int)date.DayOfWeek))
            return true;

        if (SpecificDates is { Count: > 0 } dates && dates.Contains(date))
            return true;

        if (DateRanges is { Count: > 0 } ranges && ranges.Any(r => date >= r.From && date <= r.To))
            return true;

        return AnnualRanges is { Count: > 0 } annual && annual.Any(r => CoversAnnual(r, date));
    }

    /// <summary>
    /// Попадает ли дата в ежегодный период. Период может пересекать Новый год («20 декабря —
    /// 8 января»), поэтому сравнение идёт по паре (месяц, день), а не по датам.
    /// </summary>
    private static bool CoversAnnual(AnnualRange range, DateOnly date)
    {
        var value = date.Month * 100 + date.Day;
        var from = range.FromMonth * 100 + range.FromDay;
        var to = range.ToMonth * 100 + range.ToDay;

        return from <= to ? value >= from && value <= to : value >= from || value <= to;
    }
}
