using Couchbase.Lite;
using Couchbase.Lite.Query;
using WorkTracker.Domain;
using WorkTracker.Formatting;
using WorkTracker.Services.Festivities;
using WorkTracker.Services.Settings;
using WorkTracker.Services.Storage;

namespace WorkTracker.Services.WorkDays;

public sealed class CouchbaseLiteWorkDayService : IWorkDayService
{
    private readonly Collection workDaysCollection;
    private readonly IAppSettingsService appSettingsService;
    private readonly IItalianFestivitySource festivitySource;

    public CouchbaseLiteWorkDayService(
        CouchbaseLiteDatabaseProvider databaseProvider,
        IAppSettingsService appSettingsService,
        IItalianFestivitySource festivitySource)
    {
        workDaysCollection = databaseProvider.WorkDays;
        this.appSettingsService = appSettingsService;
        this.festivitySource = festivitySource;
    }

    public Task<WorkDayDocument?> GetAsync(DateOnly date, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();

        var id = date.ToString("yyyy-MM-dd");
        var doc = workDaysCollection.GetDocument(id);
        return Task.FromResult(doc is not null ? Map(doc) : null);
    }

    public async Task<WorkUnitDocument?> GetWorkUnitAsync(DateOnly date, string workUnitId, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();

        var day = await GetAsync(date, cancellationToken);
        return day?.WorkUnits.FirstOrDefault(unit => string.Equals(unit.Id, workUnitId, StringComparison.Ordinal));
    }

    public async Task<CalendarEventDocument?> GetCalendarEventAsync(DateOnly date, string calendarEventId, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();

        var day = await GetAsync(date, cancellationToken);
        var calendarEvent = day?.CalendarEvents.FirstOrDefault(entry => string.Equals(entry.Id, calendarEventId, StringComparison.Ordinal));
        if (calendarEvent is not null)
        {
            return calendarEvent;
        }

        return FindCalendarEventLocation(calendarEventId, cancellationToken)?.CalendarEvent;
    }

    public async Task<WorkUnitDocument> SaveWorkUnitAsync(DateOnly date, WorkUnitDocument workUnit, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();

        var settings = await appSettingsService.GetAsync(cancellationToken);
        var day = await GetOrCreateDayAsync(date, cancellationToken);
        var now = DateTimeOffset.UtcNow;
        var existingIndex = day.WorkUnits.FindIndex(unit => string.Equals(unit.Id, workUnit.Id, StringComparison.Ordinal));
        var existingCreatedAt = existingIndex >= 0 ? day.WorkUnits[existingIndex].CreatedAtUtc : now;

        workUnit.Id = string.IsNullOrWhiteSpace(workUnit.Id) ? Guid.NewGuid().ToString("N") : workUnit.Id;
        workUnit.Label = string.IsNullOrWhiteSpace(workUnit.Label) ? "Work unit" : workUnit.Label.Trim();
        workUnit.ManualWorkedHours = Math.Max(0m, workUnit.ManualWorkedHours);
        workUnit.CoeffSnapshot = new CoeffSnapshotDocument
        {
            StandardWorkHoursPerDay = settings.StandardWorkHoursPerDay,
            HourlyGrossRate = settings.HourlyGrossRate,
            ProfitabilityCoefficient = settings.ProfitabilityCoefficient,
            InpsRate = settings.InpsRate,
            SubstituteTaxRate = settings.SubstituteTaxRate
        };
        workUnit.CreatedAtUtc = existingCreatedAt;
        workUnit.UpdatedAtUtc = now;

        Compute(workUnit);

        if (existingIndex >= 0)
        {
            day.WorkUnits[existingIndex] = workUnit;
        }
        else
        {
            day.WorkUnits.Add(workUnit);
        }

        day.UpdatedAtUtc = now;
        SortEntries(day);
        SaveDocument(day);
        return workUnit;
    }

    public async Task<CalendarEventDocument> SaveCalendarEventAsync(DateOnly date, CalendarEventDocument calendarEvent, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();

        var now = DateTimeOffset.UtcNow;
        var existingLocation = string.IsNullOrWhiteSpace(calendarEvent.Id)
            ? null
            : FindCalendarEventLocation(calendarEvent.Id, cancellationToken);
        var targetDate = calendarEvent.StartDate == default ? date : calendarEvent.StartDate;
        var endDate = calendarEvent.EndDate;

        if (endDate.HasValue && endDate.Value < targetDate)
        {
            (targetDate, endDate) = (endDate.Value, targetDate);
        }

        calendarEvent.Id = string.IsNullOrWhiteSpace(calendarEvent.Id) ? Guid.NewGuid().ToString("N") : calendarEvent.Id;
        calendarEvent.StartDate = targetDate;
        calendarEvent.EndDate = endDate.HasValue && endDate.Value > targetDate
            ? endDate.Value
            : null;
        calendarEvent.Description = calendarEvent.Description?.Trim() ?? string.Empty;
        calendarEvent.CreatedAtUtc = existingLocation?.CalendarEvent.CreatedAtUtc ?? now;
        calendarEvent.UpdatedAtUtc = now;

        Compute(calendarEvent);

        if (existingLocation is not null)
        {
            var ownerDay = existingLocation.Day;
            if (ownerDay.Date == targetDate)
            {
                ownerDay.CalendarEvents[existingLocation.Index] = calendarEvent;
                ownerDay.UpdatedAtUtc = now;
                SortEntries(ownerDay);
                SaveDocument(ownerDay);
                return calendarEvent;
            }

            ownerDay.CalendarEvents.RemoveAt(existingLocation.Index);
            DeleteOrSaveDay(ownerDay);
        }

        var targetDay = await GetOrCreateDayAsync(targetDate, cancellationToken);
        var targetIndex = targetDay.CalendarEvents.FindIndex(entry => string.Equals(entry.Id, calendarEvent.Id, StringComparison.Ordinal));
        if (targetIndex >= 0)
        {
            targetDay.CalendarEvents[targetIndex] = calendarEvent;
        }
        else
        {
            targetDay.CalendarEvents.Add(calendarEvent);
        }

        targetDay.UpdatedAtUtc = now;
        SortEntries(targetDay);
        SaveDocument(targetDay);
        return calendarEvent;
    }

    public async Task<bool> DeleteWorkUnitAsync(DateOnly date, string workUnitId, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();

        var day = await GetAsync(date, cancellationToken);
        if (day is null)
        {
            return false;
        }

        var removed = day.WorkUnits.RemoveAll(unit => string.Equals(unit.Id, workUnitId, StringComparison.Ordinal));
        if (removed == 0)
        {
            return false;
        }

        return DeleteOrSaveDay(day);
    }

    public async Task<bool> DeleteCalendarEventAsync(DateOnly date, string calendarEventId, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();

        var day = await GetAsync(date, cancellationToken);
        if (day is not null)
        {
            var removed = day.CalendarEvents.RemoveAll(calendarEvent => string.Equals(calendarEvent.Id, calendarEventId, StringComparison.Ordinal));
            if (removed > 0)
            {
                return DeleteOrSaveDay(day);
            }
        }

        var existingLocation = FindCalendarEventLocation(calendarEventId, cancellationToken);
        if (existingLocation is null)
        {
            return false;
        }

        existingLocation.Day.CalendarEvents.RemoveAt(existingLocation.Index);
        return DeleteOrSaveDay(existingLocation.Day);
    }

    public Task<IReadOnlyList<WorkDayDocument>> GetRangeAsync(DateOnly from, DateOnly to, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();

        var storedDays = GetAllDays(cancellationToken);
        var results = new Dictionary<DateOnly, WorkDayDocument>();

        foreach (var day in storedDays)
        {
            cancellationToken.ThrowIfCancellationRequested();

            if (day.Date >= from && day.Date <= to)
            {
                if (results.TryGetValue(day.Date, out var existingRangeDay))
                {
                    MergeStoredDayIntoRangeDay(existingRangeDay, day);
                }
                else
                {
                    results[day.Date] = CloneDayForRange(day.Date, day);
                }
            }

            foreach (var calendarEvent in day.CalendarEvents)
            {
                var startDate = calendarEvent.StartDate == default ? day.Date : calendarEvent.StartDate;
                var endDate = calendarEvent.EndDate ?? startDate;
                if (endDate < from || startDate > to)
                {
                    continue;
                }

                var overlapStart = startDate < from ? from : startDate;
                var overlapEnd = endDate > to ? to : endDate;
                for (var date = overlapStart; date <= overlapEnd; date = date.AddDays(1))
                {
                    if (!results.TryGetValue(date, out var rangeDay))
                    {
                        rangeDay = CloneDayForRange(date);
                        results.Add(date, rangeDay);
                    }

                    AddProjectedCalendarEvent(rangeDay, calendarEvent);
                }
            }
        }

        var orderedResults = results.Values.OrderBy(day => day.Date).ToList();
        foreach (var day in orderedResults)
        {
            SortEntries(day);
        }

        return Task.FromResult<IReadOnlyList<WorkDayDocument>>(orderedResults);
    }

    public async Task<MonthlySummaryModel> GetMonthlySummaryAsync(int year, int month, bool includePreview, CancellationToken cancellationToken = default)
    {
        var from = new DateOnly(year, month, 1);
        return await BuildMonthlySummaryAsync(from, includePreview, cancellationToken);
    }

    public async Task<IReadOnlyList<MonthlySummaryModel>> GetYearlySummaryAsync(int year, bool includePreview, CancellationToken cancellationToken = default)
    {
        var summaries = new List<MonthlySummaryModel>(12);

        for (var month = 1; month <= 12; month++)
        {
            cancellationToken.ThrowIfCancellationRequested();
            summaries.Add(await BuildMonthlySummaryAsync(new DateOnly(year, month, 1), includePreview, cancellationToken));
        }

        return summaries;
    }

    public async Task<MonthlyTimesheetModel> GetMonthlyTimesheetAsync(int year, int month, bool includePreview, CancellationToken cancellationToken = default)
    {
        var from = new DateOnly(year, month, 1);
        var to = from.AddMonths(1).AddDays(-1);
        var days = await GetRangeAsync(from, to, cancellationToken);
        var dayLookup = days.ToDictionary(day => day.Date);
        var settings = await appSettingsService.GetAsync(cancellationToken);

        var daySummaries = new List<MonthlyTimesheetDaySummary>();
        for (var date = from; date <= to; date = date.AddDays(1))
        {
            dayLookup.TryGetValue(date, out var day);
            daySummaries.Add(CreateTimesheetDaySummary(day, date, includePreview, settings.StandardWorkHoursPerDay));
        }

        return new MonthlyTimesheetModel
        {
            Year = year,
            Month = month,
            Days = daySummaries.Select(summary => new MonthlyTimesheetDayModel
            {
                Date = summary.Date,
                IsWeekend = summary.Date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday,
                IsHoliday = summary.HolidayDays > 0m || dayLookup.GetValueOrDefault(summary.Date)?.IsItalianFestivity == true,
                IsClosure = summary.VacationDays > 0m && HasEventType(dayLookup.GetValueOrDefault(summary.Date), CalendarEventType.Closure),
                WorkUnitSummaries = dayLookup.GetValueOrDefault(summary.Date)?.WorkUnits
                    .Where(unit => includePreview || !unit.IsPreview)
                    .Select(FormatTimesheetWorkUnitSummary)
                    .ToList() ?? [],
                EventSummaries = dayLookup.GetValueOrDefault(summary.Date)?.CalendarEvents
                    .Select(FormatTimesheetEventSummary)
                    .ToList() ?? []
            }).ToList(),
            Rows =
            [
                CreateTimesheetRow("office", "Ore lavorative in presenza", MonthlyTimesheetValueFormat.Hours, daySummaries.Select(summary => summary.OfficeHours)),
                CreateTimesheetRow("home", "Ore lavorative in smart working", MonthlyTimesheetValueFormat.Hours, daySummaries.Select(summary => summary.HomeHours)),
                CreateTimesheetRow("overtime", "Straordinari", MonthlyTimesheetValueFormat.Hours, daySummaries.Select(summary => summary.OvertimeHours)),
                CreateTimesheetRow("weekend", "Weekend", MonthlyTimesheetValueFormat.Hours, daySummaries.Select(summary => summary.WeekendHours)),
                CreateTimesheetRow("night", "Notturni (22-06)", MonthlyTimesheetValueFormat.Hours, daySummaries.Select(summary => summary.NightHours)),
                CreateTimesheetRow("vacation", "Giorni di ferie", MonthlyTimesheetValueFormat.Days, daySummaries.Select(summary => summary.VacationDays)),
                CreateTimesheetRow("permit", "Ore di permesso", MonthlyTimesheetValueFormat.Hours, daySummaries.Select(summary => summary.PermitHours)),
                CreateTimesheetRow("compensatory-rest", "Riposo compensativo", MonthlyTimesheetValueFormat.Days, daySummaries.Select(summary => summary.CompensatoryRestDays), includeZeroTotal: false),
                CreateTimesheetRow("sick", "Giorni di malattia", MonthlyTimesheetValueFormat.Days, daySummaries.Select(summary => summary.SickDays)),
                CreateTimesheetRow("holiday", "Festività", MonthlyTimesheetValueFormat.Days, daySummaries.Select(summary => summary.HolidayDays))
            ]
        };
    }

    public async Task<int> GenerateMonthlyPreviewWorkUnitsAsync(int year, int month, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();

        var settings = await appSettingsService.GetAsync(cancellationToken);
        var festivities = festivitySource.GetFestivities(year);
        var from = new DateOnly(year, month, 1);
        var to = from.AddMonths(1).AddDays(-1);
        var createdDays = 0;
        var projectedDays = await GetRangeAsync(from, to, cancellationToken);
        var projectedLookup = projectedDays.ToDictionary(day => day.Date);

        for (var date = from; date <= to; date = date.AddDays(1))
        {
            cancellationToken.ThrowIfCancellationRequested();

            if (date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday || festivities.Contains(date))
            {
                continue;
            }

            var day = await GetOrCreateDayAsync(date, cancellationToken);
            var projectedDay = projectedLookup.GetValueOrDefault(date);
            if (day.WorkUnits.Count > 0 || projectedDay?.CalendarEvents.Any(entry => IsNonWorkingEvent(entry.EventType)) == true)
            {
                continue;
            }

            day.WorkUnits.Add(CreatePreviewWorkUnit("Morning", new TimeOnly(8, 30), new TimeOnly(13, 0), settings));
            day.WorkUnits.Add(CreatePreviewWorkUnit("Afternoon", new TimeOnly(14, 0), new TimeOnly(17, 30), settings));
            day.UpdatedAtUtc = DateTimeOffset.UtcNow;

            SortEntries(day);
            SaveDocument(day);
            createdDays++;
        }

        return createdDays;
    }

    private void SaveDocument(WorkDayDocument day)
    {
        var doc = new MutableDocument(day.Id);
        doc.SetString("date", day.Date.ToString("yyyy-MM-dd"));
        doc.SetBoolean("isWeekend", day.IsWeekend);
        doc.SetBoolean("isItalianFestivity", day.IsItalianFestivity);

        var workUnits = new MutableArrayObject();
        foreach (var unit in day.WorkUnits)
        {
            var entry = new MutableDictionaryObject();
            entry.SetString("id", unit.Id);
            entry.SetString("label", unit.Label);
            entry.SetInt("location", (int)unit.Location);
            entry.SetString("startTime", unit.StartTime?.ToString("HH:mm"));
            entry.SetString("endTime", unit.EndTime?.ToString("HH:mm"));
            entry.SetBoolean("isPreview", unit.IsPreview);
            entry.SetDouble("manualWorkedHours", decimal.ToDouble(unit.ManualWorkedHours));
            entry.SetDouble("calculatedWorkedHours", decimal.ToDouble(unit.CalculatedWorkedHours));
            entry.SetDouble("workedHoursDelta", decimal.ToDouble(unit.WorkedHoursDelta));
            entry.SetDouble("grossIncome", decimal.ToDouble(unit.GrossIncome));
            entry.SetDouble("netIncome", decimal.ToDouble(unit.NetIncome));
            entry.SetString("notes", unit.Notes);
            entry.SetDouble("coeff_standardWorkHoursPerDay", decimal.ToDouble(unit.CoeffSnapshot.StandardWorkHoursPerDay));
            entry.SetDouble("coeff_hourlyGrossRate", decimal.ToDouble(unit.CoeffSnapshot.HourlyGrossRate));
            entry.SetDouble("coeff_profitabilityCoefficient", decimal.ToDouble(unit.CoeffSnapshot.ProfitabilityCoefficient));
            entry.SetDouble("coeff_inpsRate", decimal.ToDouble(unit.CoeffSnapshot.InpsRate));
            entry.SetDouble("coeff_substituteTaxRate", decimal.ToDouble(unit.CoeffSnapshot.SubstituteTaxRate));
            entry.SetString("createdAtUtc", unit.CreatedAtUtc.ToString("O"));
            entry.SetString("updatedAtUtc", unit.UpdatedAtUtc.ToString("O"));
            workUnits.AddDictionary(entry);
        }

        var calendarEvents = new MutableArrayObject();
        foreach (var calendarEvent in day.CalendarEvents)
        {
            var entry = new MutableDictionaryObject();
            entry.SetString("id", calendarEvent.Id);
            entry.SetString("startDate", (calendarEvent.StartDate == default ? day.Date : calendarEvent.StartDate).ToString("yyyy-MM-dd"));
            entry.SetInt("eventType", (int)calendarEvent.EventType);
            entry.SetString("description", calendarEvent.Description);
            entry.SetString("startTime", calendarEvent.StartTime?.ToString("HH:mm"));
            entry.SetString("endTime", calendarEvent.EndTime?.ToString("HH:mm"));
            if (calendarEvent.EndDate.HasValue)
            {
                entry.SetString("endDate", calendarEvent.EndDate.Value.ToString("yyyy-MM-dd"));
            }
            if (calendarEvent.DurationHours.HasValue)
            {
                entry.SetDouble("durationHours", decimal.ToDouble(calendarEvent.DurationHours.Value));
            }
            entry.SetString("createdAtUtc", calendarEvent.CreatedAtUtc.ToString("O"));
            entry.SetString("updatedAtUtc", calendarEvent.UpdatedAtUtc.ToString("O"));
            calendarEvents.AddDictionary(entry);
        }

        doc.SetArray("workUnits", workUnits);
        doc.SetArray("calendarEvents", calendarEvents);

        doc.SetString("createdAtUtc", day.CreatedAtUtc.ToString("O"));
        doc.SetString("updatedAtUtc", day.UpdatedAtUtc.ToString("O"));

        workDaysCollection.Save(doc);
    }

    private bool DeleteOrSaveDay(WorkDayDocument day)
    {
        if (day.WorkUnits.Count == 0 && day.CalendarEvents.Count == 0)
        {
            var existing = workDaysCollection.GetDocument(day.Id);
            if (existing is null)
            {
                return false;
            }

            workDaysCollection.Delete(existing);
            return true;
        }

        day.UpdatedAtUtc = DateTimeOffset.UtcNow;
        SortEntries(day);
        SaveDocument(day);
        return true;
    }

    private static WorkDayDocument Map(Document doc)
    {
        var date = DateOnly.ParseExact(doc.GetString("date") ?? doc.Id, "yyyy-MM-dd");

        if (!doc.Contains("workUnits") && !doc.Contains("calendarEvents"))
        {
            return MapLegacy(doc);
        }

        var workUnits = new List<WorkUnitDocument>();
        var workUnitsArray = doc.GetArray("workUnits");
        if (workUnitsArray is not null)
        {
            for (var i = 0; i < workUnitsArray.Count; i++)
            {
                var unit = workUnitsArray.GetDictionary(i);
                if (unit is not null)
                {
                    workUnits.Add(MapWorkUnit(unit));
                }
            }
        }

        var calendarEvents = new List<CalendarEventDocument>();
        var calendarEventsArray = doc.GetArray("calendarEvents");
        if (calendarEventsArray is not null)
        {
            for (var i = 0; i < calendarEventsArray.Count; i++)
            {
                var calendarEvent = calendarEventsArray.GetDictionary(i);
                if (calendarEvent is not null)
                {
                    calendarEvents.Add(MapCalendarEvent(calendarEvent, date));
                }
            }
        }

        return new WorkDayDocument
        {
            Id = doc.Id,
            Date = date,
            IsWeekend = doc.GetBoolean("isWeekend"),
            IsItalianFestivity = doc.GetBoolean("isItalianFestivity"),
            WorkUnits = workUnits,
            CalendarEvents = calendarEvents,
            CreatedAtUtc = ReadDateTimeOffset(doc, "createdAtUtc"),
            UpdatedAtUtc = ReadDateTimeOffset(doc, "updatedAtUtc")
        };
    }

    private async Task<WorkDayDocument> GetOrCreateDayAsync(DateOnly date, CancellationToken cancellationToken)
    {
        var existing = await GetAsync(date, cancellationToken);
        if (existing is not null)
        {
            existing.IsWeekend = date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
            existing.IsItalianFestivity = festivitySource.GetFestivities(date.Year).Contains(date);
            existing.Id = date.ToString("yyyy-MM-dd");
            existing.Date = date;
            return existing;
        }

        return new WorkDayDocument
        {
            Id = date.ToString("yyyy-MM-dd"),
            Date = date,
            IsWeekend = date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday,
            IsItalianFestivity = festivitySource.GetFestivities(date.Year).Contains(date),
            CreatedAtUtc = DateTimeOffset.UtcNow,
            UpdatedAtUtc = DateTimeOffset.UtcNow
        };
    }

    private static void Compute(WorkUnitDocument unit)
    {
        unit.CalculatedWorkedHours = CalculateDuration(unit.StartTime, unit.EndTime) ?? 0m;
        unit.WorkedHoursDelta = unit.ManualWorkedHours - unit.CalculatedWorkedHours;

        var coeff = unit.CoeffSnapshot;
        unit.GrossIncome = unit.ManualWorkedHours * coeff.HourlyGrossRate;
        var taxableBase = unit.GrossIncome * coeff.ProfitabilityCoefficient;
        unit.NetIncome = unit.GrossIncome - (taxableBase * coeff.InpsRate) - (taxableBase * coeff.SubstituteTaxRate);
    }

    private static void Compute(CalendarEventDocument calendarEvent)
    {
        calendarEvent.DurationHours = CalculateDuration(calendarEvent.StartTime, calendarEvent.EndTime);
    }

    private static decimal? CalculateDuration(TimeOnly? startTime, TimeOnly? endTime)
    {
        if (!startTime.HasValue || !endTime.HasValue || endTime <= startTime)
        {
            return null;
        }

        return Math.Round((decimal)(endTime.Value - startTime.Value).TotalHours, 2, MidpointRounding.AwayFromZero);
    }

    private static int CountDaysWithEvent(IEnumerable<WorkDayDocument> days, CalendarEventType eventType)
    {
        return days.Count(day => day.CalendarEvents.Any(calendarEvent => calendarEvent.EventType == eventType));
    }

    private async Task<MonthlySummaryModel> BuildMonthlySummaryAsync(DateOnly from, bool includePreview, CancellationToken cancellationToken)
    {
        var to = from.AddMonths(1).AddDays(-1);
        var days = await GetRangeAsync(from, to, cancellationToken);

        var includedUnits = days
            .SelectMany(day => day.WorkUnits.Where(unit => includePreview || !unit.IsPreview).Select(unit => new { day.Date, Unit = unit }))
            .ToList();

        var previewUnits = days
            .SelectMany(day => day.WorkUnits.Where(unit => unit.IsPreview).Select(unit => new { day.Date, Unit = unit }))
            .ToList();

        return new MonthlySummaryModel
        {
            Year = from.Year,
            Month = from.Month,
            TotalWorkedHours = includedUnits.Sum(item => item.Unit.ManualWorkedHours),
            TotalPreviewWorkedHours = previewUnits.Sum(item => item.Unit.ManualWorkedHours),
            CountedWorkUnits = includedUnits.Count,
            PreviewWorkUnits = previewUnits.Count,
            OfficeDays = includedUnits.Where(item => item.Unit.Location == WorkUnitLocation.Office).Select(item => item.Date).Distinct().Count(),
            HomeDays = includedUnits.Where(item => item.Unit.Location == WorkUnitLocation.Home).Select(item => item.Date).Distinct().Count(),
            HolidayDays = CountDaysWithEvent(days, CalendarEventType.Holiday),
            SickDays = CountDaysWithEvent(days, CalendarEventType.Illness),
            DaysOff = CountDaysWithEvent(days, CalendarEventType.DayOff),
            ClosureDays = CountDaysWithEvent(days, CalendarEventType.Closure),
            TotalHoursOff = days.Sum(day => GetHoursOff(day, includePreview)),
            TotalGrossIncome = includedUnits.Sum(item => item.Unit.GrossIncome),
            TotalNetIncome = includedUnits.Sum(item => item.Unit.NetIncome),
            TotalWorkingDays = includedUnits.Select(item => item.Date).Distinct().Count()
        };
    }

    private static MonthlyTimesheetDaySummary CreateTimesheetDaySummary(WorkDayDocument? day, DateOnly date, bool includePreview, decimal defaultStandardHours)
    {
        var includedUnits = day?.WorkUnits.Where(unit => includePreview || !unit.IsPreview).ToList() ?? [];
        var totalHours = includedUnits.Sum(unit => unit.ManualWorkedHours);
        var explicitHoliday = HasEventType(day, CalendarEventType.Holiday);
        var illness = HasEventType(day, CalendarEventType.Illness);
        var dayOff = HasEventType(day, CalendarEventType.DayOff);
        var closure = HasEventType(day, CalendarEventType.Closure);
        var isWeekend = date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
        var isAutomaticHoliday = day?.IsItalianFestivity ?? false;
        var standardHours = includedUnits.FirstOrDefault()?.CoeffSnapshot.StandardWorkHoursPerDay ?? defaultStandardHours;
        var nightHours = includedUnits.Sum(GetNightHours);
        var weekdayDaytimeHours = isWeekend ? 0m : Math.Max(0m, totalHours - nightHours);
        var suppressVacation = isWeekend || explicitHoliday || isAutomaticHoliday || illness;
        var hasNonWorkingEvent = explicitHoliday || illness || dayOff || closure;
        var isFutureEmptyDay = date > DateOnly.FromDateTime(DateTime.Today) && includedUnits.Count == 0;
        var permitHours = !isWeekend && !isAutomaticHoliday && !hasNonWorkingEvent && !isFutureEmptyDay && totalHours < standardHours
            ? standardHours - totalHours
            : 0m;

        return new MonthlyTimesheetDaySummary
        {
            Date = date,
            OfficeHours = includedUnits.Where(unit => unit.Location == WorkUnitLocation.Office).Sum(unit => unit.ManualWorkedHours),
            HomeHours = includedUnits.Where(unit => unit.Location == WorkUnitLocation.Home).Sum(unit => unit.ManualWorkedHours),
            OvertimeHours = Math.Max(0m, weekdayDaytimeHours - standardHours),
            WeekendHours = isWeekend ? totalHours : 0m,
            NightHours = nightHours,
            VacationDays = (dayOff || closure) && !suppressVacation ? 1m : 0m,
            PermitHours = Math.Max(0m, permitHours),
            CompensatoryRestDays = 0m,
            SickDays = illness ? 1m : 0m,
            HolidayDays = explicitHoliday && !isWeekend ? 1m : 0m
        };
    }

    private static MonthlyTimesheetRowModel CreateTimesheetRow(
        string key,
        string label,
        MonthlyTimesheetValueFormat valueFormat,
        IEnumerable<decimal> values,
        bool includeZeroTotal = true)
    {
        var dailyValues = values
            .Select(value => value > 0m ? value : (decimal?)null)
            .ToList();

        var total = dailyValues.Where(value => value.HasValue).Sum(value => value ?? 0m);

        return new MonthlyTimesheetRowModel
        {
            Key = key,
            Label = label,
            ValueFormat = valueFormat,
            DailyValues = dailyValues,
            Total = includeZeroTotal || total > 0m ? total : null
        };
    }

    private static decimal GetHoursOff(WorkDayDocument day, bool includePreview)
    {
        var includedUnits = day.WorkUnits.Where(unit => includePreview || !unit.IsPreview).ToList();
        if (includedUnits.Count == 0)
        {
            return 0m;
        }

        var standardHours = includedUnits[0].CoeffSnapshot.StandardWorkHoursPerDay;
        var countedHours = includedUnits.Sum(unit => unit.ManualWorkedHours);
        return Math.Max(0m, standardHours - countedHours);
    }

    private static bool IsNonWorkingEvent(CalendarEventType eventType)
    {
        return eventType is CalendarEventType.DayOff or CalendarEventType.Closure or CalendarEventType.Holiday or CalendarEventType.Illness;
    }

    private static bool HasEventType(WorkDayDocument? day, CalendarEventType eventType)
    {
        return day?.CalendarEvents.Any(calendarEvent => calendarEvent.EventType == eventType) == true;
    }

    private static string FormatTimesheetWorkUnitSummary(WorkUnitDocument unit)
    {
        var prefix = unit.Location == WorkUnitLocation.Home ? "SW" : "Pres";
        var hours = FormatCompactHours(unit.ManualWorkedHours);
        if (unit.StartTime.HasValue && unit.EndTime.HasValue)
        {
            return $"{prefix}: {unit.Label} ({unit.StartTime:HH:mm}-{unit.EndTime:HH:mm}, {hours}h{(unit.IsPreview ? ", preview" : string.Empty)})";
        }

        return $"{prefix}: {unit.Label} ({hours}h{(unit.IsPreview ? ", preview" : string.Empty)})";
    }

    private static string FormatTimesheetEventSummary(CalendarEventDocument calendarEvent)
    {
        var description = CalendarEventFormatter.GetDisplayDescription(calendarEvent);
        if (calendarEvent.StartTime.HasValue)
        {
            return $"{CalendarEventFormatter.GetEventTypeName(calendarEvent.EventType)}: {description} ({calendarEvent.StartTime:HH:mm})";
        }

        return $"{CalendarEventFormatter.GetEventTypeName(calendarEvent.EventType)}: {description}";
    }

    private static string FormatCompactHours(decimal value)
    {
        return Formatting.DurationFormatter.FormatHours(value);
    }

    private static decimal GetNightHours(WorkUnitDocument unit)
    {
        if (!unit.StartTime.HasValue || !unit.EndTime.HasValue || unit.EndTime <= unit.StartTime)
        {
            return 0m;
        }

        return GetOverlapHours(unit.StartTime.Value, unit.EndTime.Value, new TimeOnly(0, 0), new TimeOnly(6, 0))
            + GetOverlapHours(unit.StartTime.Value, unit.EndTime.Value, new TimeOnly(22, 0), new TimeOnly(23, 59, 59));
    }

    private static decimal GetOverlapHours(TimeOnly rangeStart, TimeOnly rangeEnd, TimeOnly windowStart, TimeOnly windowEnd)
    {
        var overlapStart = rangeStart > windowStart ? rangeStart : windowStart;
        var overlapEnd = rangeEnd < windowEnd ? rangeEnd : windowEnd;
        if (overlapEnd <= overlapStart)
        {
            return 0m;
        }

        return Math.Round((decimal)(overlapEnd - overlapStart).TotalHours, 2, MidpointRounding.AwayFromZero);
    }

    private static WorkUnitDocument CreatePreviewWorkUnit(string label, TimeOnly startTime, TimeOnly endTime, AppSettingsDocument settings)
    {
        var workUnit = new WorkUnitDocument
        {
            Id = Guid.NewGuid().ToString("N"),
            Label = label,
            Location = WorkUnitLocation.Office,
            StartTime = startTime,
            EndTime = endTime,
            IsPreview = true,
            ManualWorkedHours = Math.Round((decimal)(endTime - startTime).TotalHours, 2, MidpointRounding.AwayFromZero),
            CoeffSnapshot = new CoeffSnapshotDocument
            {
                StandardWorkHoursPerDay = settings.StandardWorkHoursPerDay,
                HourlyGrossRate = settings.HourlyGrossRate,
                ProfitabilityCoefficient = settings.ProfitabilityCoefficient,
                InpsRate = settings.InpsRate,
                SubstituteTaxRate = settings.SubstituteTaxRate
            },
            CreatedAtUtc = DateTimeOffset.UtcNow,
            UpdatedAtUtc = DateTimeOffset.UtcNow
        };

        Compute(workUnit);
        return workUnit;
    }

    private static void SortEntries(WorkDayDocument day)
    {
        day.WorkUnits = day.WorkUnits
            .OrderBy(unit => unit.StartTime ?? TimeOnly.MaxValue)
            .ThenBy(unit => unit.Label, StringComparer.CurrentCultureIgnoreCase)
            .ToList();

        day.CalendarEvents = day.CalendarEvents
            .OrderBy(calendarEvent => calendarEvent.StartTime ?? TimeOnly.MaxValue)
            .ThenBy(calendarEvent => calendarEvent.Description, StringComparer.CurrentCultureIgnoreCase)
            .ToList();
    }

    private static WorkUnitDocument MapWorkUnit(DictionaryObject unit)
    {
        var workUnit = new WorkUnitDocument
        {
            Id = unit.GetString("id") ?? Guid.NewGuid().ToString("N"),
            Label = unit.GetString("label") ?? "Work unit",
            Location = unit.Contains("location") ? (WorkUnitLocation)unit.GetInt("location") : WorkUnitLocation.Office,
            StartTime = ReadTimeOnly(unit, "startTime"),
            EndTime = ReadTimeOnly(unit, "endTime"),
            IsPreview = unit.GetBoolean("isPreview"),
            ManualWorkedHours = ReadDecimal(unit, "manualWorkedHours", 0m),
            CalculatedWorkedHours = ReadDecimal(unit, "calculatedWorkedHours", 0m),
            WorkedHoursDelta = ReadDecimal(unit, "workedHoursDelta", 0m),
            GrossIncome = ReadDecimal(unit, "grossIncome", 0m),
            NetIncome = ReadDecimal(unit, "netIncome", 0m),
            Notes = unit.GetString("notes"),
            CoeffSnapshot = new CoeffSnapshotDocument
            {
                StandardWorkHoursPerDay = ReadDecimal(unit, "coeff_standardWorkHoursPerDay", 8m),
                HourlyGrossRate = ReadDecimal(unit, "coeff_hourlyGrossRate", 17.5m),
                ProfitabilityCoefficient = ReadDecimal(unit, "coeff_profitabilityCoefficient", 0.67m),
                InpsRate = ReadDecimal(unit, "coeff_inpsRate", 0.2607m),
                SubstituteTaxRate = ReadDecimal(unit, "coeff_substituteTaxRate", 0.15m)
            },
            CreatedAtUtc = ReadDateTimeOffset(unit, "createdAtUtc"),
            UpdatedAtUtc = ReadDateTimeOffset(unit, "updatedAtUtc")
        };

        Compute(workUnit);
        return workUnit;
    }

    private static CalendarEventDocument MapCalendarEvent(DictionaryObject calendarEvent, DateOnly owningDate)
    {
        var entry = new CalendarEventDocument
        {
            Id = calendarEvent.GetString("id") ?? Guid.NewGuid().ToString("N"),
            StartDate = ReadDateOnly(calendarEvent, "startDate") ?? owningDate,
            EndDate = ReadDateOnly(calendarEvent, "endDate"),
            EventType = calendarEvent.Contains("eventType") ? (CalendarEventType)calendarEvent.GetInt("eventType") : CalendarEventType.Generic,
            Description = calendarEvent.GetString("description") ?? string.Empty,
            StartTime = ReadTimeOnly(calendarEvent, "startTime"),
            EndTime = ReadTimeOnly(calendarEvent, "endTime"),
            DurationHours = calendarEvent.Contains("durationHours") ? ReadDecimal(calendarEvent, "durationHours", 0m) : null,
            CreatedAtUtc = ReadDateTimeOffset(calendarEvent, "createdAtUtc"),
            UpdatedAtUtc = ReadDateTimeOffset(calendarEvent, "updatedAtUtc")
        };

        Compute(entry);
        return entry;
    }

    private static WorkDayDocument MapLegacy(Document doc)
    {
        var date = DateOnly.ParseExact(doc.GetString("date") ?? doc.Id, "yyyy-MM-dd");
        var dayType = doc.Contains("dayType") ? (DayType)doc.GetInt("dayType") : DayType.None;
        var day = new WorkDayDocument
        {
            Id = doc.Id,
            Date = date,
            IsWeekend = doc.GetBoolean("isWeekend"),
            IsItalianFestivity = doc.GetBoolean("isItalianFestivity"),
            CreatedAtUtc = ReadDateTimeOffset(doc, "createdAtUtc"),
            UpdatedAtUtc = ReadDateTimeOffset(doc, "updatedAtUtc")
        };

        var coeffSnapshot = new CoeffSnapshotDocument
        {
            StandardWorkHoursPerDay = ReadDecimal(doc, "coeff_standardWorkHoursPerDay", 8m),
            HourlyGrossRate = ReadDecimal(doc, "coeff_hourlyGrossRate", 17.5m),
            ProfitabilityCoefficient = ReadDecimal(doc, "coeff_profitabilityCoefficient", 0.67m),
            InpsRate = ReadDecimal(doc, "coeff_inpsRate", 0.2607m),
            SubstituteTaxRate = ReadDecimal(doc, "coeff_substituteTaxRate", 0.15m)
        };

        if (dayType is DayType.Work or DayType.Home)
        {
            var workUnit = new WorkUnitDocument
            {
                Id = "legacy",
                Label = "Legacy entry",
                Location = dayType == DayType.Home ? WorkUnitLocation.Home : WorkUnitLocation.Office,
                StartTime = ReadTimeOnly(doc, "startTime"),
                EndTime = ReadTimeOnly(doc, "actualExitTime") ?? ReadTimeOnly(doc, "projectedExitTime"),
                IsPreview = false,
                ManualWorkedHours = ReadDecimal(doc, "workedHoursFinal", ReadDecimal(doc, "workedHoursBase", 0m)),
                GrossIncome = ReadDecimal(doc, "grossIncome", 0m),
                NetIncome = ReadDecimal(doc, "netIncome", 0m),
                Notes = doc.GetString("notes"),
                CoeffSnapshot = coeffSnapshot,
                CreatedAtUtc = day.CreatedAtUtc,
                UpdatedAtUtc = day.UpdatedAtUtc
            };

            Compute(workUnit);
            day.WorkUnits.Add(workUnit);
        }
        else if (dayType != DayType.None)
        {
            var calendarEvent = new CalendarEventDocument
            {
                Id = "legacy",
                StartDate = date,
                EventType = MapLegacyEventType(dayType),
                Description = string.IsNullOrWhiteSpace(doc.GetString("notes")) ? $"Legacy {dayType}" : doc.GetString("notes")!,
                CreatedAtUtc = day.CreatedAtUtc,
                UpdatedAtUtc = day.UpdatedAtUtc
            };

            Compute(calendarEvent);
            day.CalendarEvents.Add(calendarEvent);
        }

        return day;
    }

    private static CalendarEventType MapLegacyEventType(DayType dayType)
    {
        return dayType switch
        {
            DayType.DayOff => CalendarEventType.DayOff,
            DayType.Closure => CalendarEventType.Closure,
            DayType.Holiday => CalendarEventType.Holiday,
            DayType.Illness => CalendarEventType.Illness,
            _ => CalendarEventType.Generic
        };
    }

    private static TimeOnly? ReadTimeOnly(Document doc, string key)
    {
        var value = doc.GetString(key);
        return !string.IsNullOrEmpty(value) && TimeOnly.TryParseExact(value, "HH:mm", out var time)
            ? time
            : null;
    }

    private static TimeOnly? ReadTimeOnly(DictionaryObject doc, string key)
    {
        var value = doc.GetString(key);
        return !string.IsNullOrEmpty(value) && TimeOnly.TryParseExact(value, "HH:mm", out var time)
            ? time
            : null;
    }

    private static DateOnly? ReadDateOnly(DictionaryObject doc, string key)
    {
        var value = doc.GetString(key);
        return !string.IsNullOrEmpty(value) && DateOnly.TryParseExact(value, "yyyy-MM-dd", out var date)
            ? date
            : null;
    }

    private static decimal ReadDecimal(Document doc, string key, decimal defaultValue)
    {
        return doc.Contains(key)
            ? Convert.ToDecimal(doc.GetDouble(key))
            : defaultValue;
    }

    private static decimal ReadDecimal(DictionaryObject doc, string key, decimal defaultValue)
    {
        return doc.Contains(key)
            ? Convert.ToDecimal(doc.GetDouble(key))
            : defaultValue;
    }

    private IReadOnlyList<WorkDayDocument> GetAllDays(CancellationToken cancellationToken)
    {
        var query = QueryBuilder
            .Select(SelectResult.Expression(Meta.ID))
            .From(DataSource.Collection(workDaysCollection))
            .OrderBy(Ordering.Expression(Meta.ID));

        var results = new List<WorkDayDocument>();
        foreach (var result in query.Execute())
        {
            cancellationToken.ThrowIfCancellationRequested();

            var id = result.GetString(0);
            if (string.IsNullOrWhiteSpace(id))
            {
                continue;
            }

            var doc = workDaysCollection.GetDocument(id);
            if (doc is not null)
            {
                results.Add(Map(doc));
            }
        }

        return results;
    }

    private WorkDayDocument CloneDayForRange(DateOnly date, WorkDayDocument? sourceDay = null)
    {
        return new WorkDayDocument
        {
            Id = date.ToString("yyyy-MM-dd"),
            Date = date,
            IsWeekend = date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday,
            IsItalianFestivity = festivitySource.GetFestivities(date.Year).Contains(date),
            WorkUnits = sourceDay?.WorkUnits.Select(CloneWorkUnit).ToList() ?? [],
            CalendarEvents = [],
            CreatedAtUtc = sourceDay?.CreatedAtUtc ?? DateTimeOffset.UtcNow,
            UpdatedAtUtc = sourceDay?.UpdatedAtUtc ?? DateTimeOffset.UtcNow
        };
    }

    private static WorkUnitDocument CloneWorkUnit(WorkUnitDocument workUnit)
    {
        return new WorkUnitDocument
        {
            Id = workUnit.Id,
            Label = workUnit.Label,
            Location = workUnit.Location,
            StartTime = workUnit.StartTime,
            EndTime = workUnit.EndTime,
            IsPreview = workUnit.IsPreview,
            ManualWorkedHours = workUnit.ManualWorkedHours,
            CalculatedWorkedHours = workUnit.CalculatedWorkedHours,
            WorkedHoursDelta = workUnit.WorkedHoursDelta,
            GrossIncome = workUnit.GrossIncome,
            NetIncome = workUnit.NetIncome,
            Notes = workUnit.Notes,
            CoeffSnapshot = new CoeffSnapshotDocument
            {
                StandardWorkHoursPerDay = workUnit.CoeffSnapshot.StandardWorkHoursPerDay,
                HourlyGrossRate = workUnit.CoeffSnapshot.HourlyGrossRate,
                ProfitabilityCoefficient = workUnit.CoeffSnapshot.ProfitabilityCoefficient,
                InpsRate = workUnit.CoeffSnapshot.InpsRate,
                SubstituteTaxRate = workUnit.CoeffSnapshot.SubstituteTaxRate
            },
            CreatedAtUtc = workUnit.CreatedAtUtc,
            UpdatedAtUtc = workUnit.UpdatedAtUtc
        };
    }

    private static CalendarEventDocument CloneCalendarEvent(CalendarEventDocument calendarEvent)
    {
        return new CalendarEventDocument
        {
            Id = calendarEvent.Id,
            StartDate = calendarEvent.StartDate,
            EndDate = calendarEvent.EndDate,
            EventType = calendarEvent.EventType,
            Description = calendarEvent.Description,
            StartTime = calendarEvent.StartTime,
            EndTime = calendarEvent.EndTime,
            DurationHours = calendarEvent.DurationHours,
            CreatedAtUtc = calendarEvent.CreatedAtUtc,
            UpdatedAtUtc = calendarEvent.UpdatedAtUtc
        };
    }

    private static void AddProjectedCalendarEvent(WorkDayDocument rangeDay, CalendarEventDocument calendarEvent)
    {
        if (rangeDay.CalendarEvents.Any(existing => string.Equals(existing.Id, calendarEvent.Id, StringComparison.Ordinal)))
        {
            return;
        }

        rangeDay.CalendarEvents.Add(CloneCalendarEvent(calendarEvent));
    }

    private static void MergeStoredDayIntoRangeDay(WorkDayDocument rangeDay, WorkDayDocument storedDay)
    {
        rangeDay.IsWeekend = storedDay.IsWeekend;
        rangeDay.IsItalianFestivity = storedDay.IsItalianFestivity;
        rangeDay.CreatedAtUtc = storedDay.CreatedAtUtc;
        rangeDay.UpdatedAtUtc = storedDay.UpdatedAtUtc;
        rangeDay.WorkUnits = storedDay.WorkUnits.Select(CloneWorkUnit).ToList();

        foreach (var calendarEvent in storedDay.CalendarEvents)
        {
            var existingIndex = rangeDay.CalendarEvents.FindIndex(existing => string.Equals(existing.Id, calendarEvent.Id, StringComparison.Ordinal));
            if (existingIndex >= 0)
            {
                rangeDay.CalendarEvents[existingIndex] = CloneCalendarEvent(calendarEvent);
            }
            else
            {
                rangeDay.CalendarEvents.Add(CloneCalendarEvent(calendarEvent));
            }
        }
    }

    private CalendarEventLocation? FindCalendarEventLocation(string calendarEventId, CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(calendarEventId))
        {
            return null;
        }

        foreach (var day in GetAllDays(cancellationToken))
        {
            var index = day.CalendarEvents.FindIndex(calendarEvent => string.Equals(calendarEvent.Id, calendarEventId, StringComparison.Ordinal));
            if (index >= 0)
            {
                return new CalendarEventLocation(day, index, day.CalendarEvents[index]);
            }
        }

        return null;
    }

    private sealed record CalendarEventLocation(WorkDayDocument Day, int Index, CalendarEventDocument CalendarEvent);

    private static DateTimeOffset ReadDateTimeOffset(Document doc, string key)
    {
        var value = doc.GetString(key);
        return !string.IsNullOrEmpty(value) && DateTimeOffset.TryParse(value, out var dt)
            ? dt
            : DateTimeOffset.UtcNow;
    }

    private static DateTimeOffset ReadDateTimeOffset(DictionaryObject doc, string key)
    {
        var value = doc.GetString(key);
        return !string.IsNullOrEmpty(value) && DateTimeOffset.TryParse(value, out var dt)
            ? dt
            : DateTimeOffset.UtcNow;
    }
}
