#nullable enable
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Holidays;
using Baya.Application.Models.Common;
using Baya.Application.Models.Holidays;
using Baya.Domain.Entities.Holidays;
using Microsoft.EntityFrameworkCore;

namespace Baya.Infrastructure.Persistence.Services.Holidays;

/// <summary>
/// Reads the seeded <c>iranian_holidays</c> table (lookups cached) to answer holiday/bank-closure
/// questions and shift a date to the next open bank day. The Iranian banking weekend is Friday.
/// </summary>
internal sealed class HolidayCalendarService(ApplicationDbContext db, ICacheService cache) : IHolidayCalendar
{
    private static readonly TimeSpan CacheTtl = TimeSpan.FromHours(6);

    // Payouts must not schedule past a bounded horizon even if the calendar is misconfigured.
    private const int MaxLookaheadDays = 60;

    private static string HolidayKey(DateOnly date) => $"holiday:is_holiday:{date:yyyy-MM-dd}";
    private static string BankClosedKey(DateOnly date) => $"holiday:bank_closed:{date:yyyy-MM-dd}";

    public ValueTask<bool> IsHoliday(DateOnly date, CancellationToken cancellationToken = default) =>
        cache.GetOrCreateAsync(
            HolidayKey(date),
            async ct => await db.Set<IranianHoliday>().AsNoTracking().AnyAsync(h => h.HolidayDate == date, ct),
            CacheTtl,
            cancellationToken);

    public ValueTask<bool> IsBankClosed(DateOnly date, CancellationToken cancellationToken = default) =>
        cache.GetOrCreateAsync(
            BankClosedKey(date),
            async ct => await db.Set<IranianHoliday>().AsNoTracking().AnyAsync(h => h.HolidayDate == date && h.IsBankClosed, ct),
            CacheTtl,
            cancellationToken);

    public async ValueTask<DateOnly> NextBusinessDay(DateOnly date, CancellationToken cancellationToken = default)
    {
        var candidate = date;
        for (var i = 0; i <= MaxLookaheadDays; i++)
        {
            if (!IsBankWeekend(candidate) && !await IsBankClosed(candidate, cancellationToken))
                return candidate;

            candidate = candidate.AddDays(1);
        }

        throw new InvalidOperationException(
            $"No open bank day found within {MaxLookaheadDays} days of {date:yyyy-MM-dd} — the holiday calendar is likely misconfigured.");
    }

    public async ValueTask<PagedResult<HolidayDto>> ListAsync(DateOnly? from, DateOnly? to, int page, int pageSize, CancellationToken cancellationToken = default)
    {
        var query = db.Set<IranianHoliday>().AsNoTracking().AsQueryable();

        if (from is { } f)
            query = query.Where(h => h.HolidayDate >= f);
        if (to is { } t)
            query = query.Where(h => h.HolidayDate <= t);

        query = query.OrderBy(h => h.HolidayDate);

        var total = await query.CountAsync(cancellationToken);
        var items = await query
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(h => new HolidayDto(h.Id, h.HolidayDate, h.NameFa, h.Type, h.IsBankClosed))
            .ToListAsync(cancellationToken);

        return new PagedResult<HolidayDto>(items, total, page, pageSize);
    }

    public async ValueTask UpsertAsync(DateOnly date, string nameFa, string type, bool isBankClosed, CancellationToken cancellationToken = default)
    {
        var existing = await db.Set<IranianHoliday>().FirstOrDefaultAsync(h => h.HolidayDate == date, cancellationToken);
        if (existing is null)
        {
            db.Set<IranianHoliday>().Add(new IranianHoliday
            {
                HolidayDate = date,
                NameFa = nameFa,
                Type = type,
                IsBankClosed = isBankClosed
            });
        }
        else
        {
            existing.NameFa = nameFa;
            existing.Type = type;
            existing.IsBankClosed = isBankClosed;
        }

        await db.SaveChangesAsync(cancellationToken);
        await Evict(date, cancellationToken);
    }

    public async ValueTask<bool> DeleteAsync(DateOnly date, CancellationToken cancellationToken = default)
    {
        var existing = await db.Set<IranianHoliday>().FirstOrDefaultAsync(h => h.HolidayDate == date, cancellationToken);
        if (existing is null)
            return false;

        db.Set<IranianHoliday>().Remove(existing);
        await db.SaveChangesAsync(cancellationToken);
        await Evict(date, cancellationToken);
        return true;
    }

    // Iranian banks are closed on Fridays; Thursday is treated as a business day.
    private static bool IsBankWeekend(DateOnly date) => date.DayOfWeek == DayOfWeek.Friday;

    private async ValueTask Evict(DateOnly date, CancellationToken cancellationToken)
    {
        await cache.RemoveAsync(HolidayKey(date), cancellationToken);
        await cache.RemoveAsync(BankClosedKey(date), cancellationToken);
    }
}
