```
using Microsoft.VisualBasic;
using PRPro.Data;
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;

namespace PRPro.Data
{
    public class LeaveCalculator
    {
        // --- Inner Classes ---
        public class LeaveBalance
        {
            public string LeaveTypeID { get; set; }
            public string LeaveType { get; set; }
            public decimal TotalAccrued { get; set; } = 0; // This is the calculated *current* balance
            public decimal Used { get; set; } = 0; // Can be populated externally if needed
            public decimal Pending { get; set; } = 0; // Can be populated externally if needed
            public decimal Available { get; set; } = 0; // Often same as TotalAccrued unless Pending is subtracted
            public decimal Lost { get; set; } = 0; // Will NOT be calculated here. Needs external logic.
            public decimal ProjectedEndOfYear { get; set; } = 0; // Needs separate calculation logic if required.
        }

        public class LeaveType
        {
            public string LeaveTypeID { get; set; }
            public string LeaveTypeName { get; set; }
            public bool ResetYearly { get; set; }
            public bool ResetAboveValue { get; set; } // Interpretation may vary by policy
            public bool IncreaseMonthly { get; set; }
            public bool HasMaximum { get; set; }
            public bool PaidLeave { get; set; }
            public bool ServiceRateIncrease { get; set; }
            public decimal ResetValue { get; set; } // Used for entitlement, reset target, or base accrual
            public decimal MonthlyIncreaseRate { get; set; }
            public decimal MaximumDays { get; set; }
        }

        public class ServiceRateIncrease
        {
            public string LeaveTypeID { get; set; }
            public int YearsService { get; set; }
            public decimal NewRate { get; set; }
        }

        // LeaveHistory class removed as it's implicitly handled by the DataTable/Dictionary input

        // --- Main Calculation Method ---

        /// <summary>
        /// Calculates leave balances for a specific employee based on provided data.
        /// </summary>
        /// <param name="employeeId">The Employee's UserID.</param>
        /// <param name="employeeStartDate">The date the employee started employment.</param>
        /// <param name="calculationDate">The date for which to calculate the balance (usually DateTime.Today).</param>
        /// <param name="workingDaysPerWeek">Number of days the employee normally works per week (e.g., 5 or 6).</param>
        /// <param name="leaveTypes">A list of all configured leave types for the company.</param>
        /// <param name="leaveHistory">DataTable containing approved leave records (Columns: StartDate, EndDate, LeaveTypeID, Days, Status).</param>
        /// <param name="serviceRateIncreases">A list of service-based rate increases (relevant for specific leave types).</param>
        /// <returns>A list of LeaveBalance objects.</returns>
        public async Task<List<LeaveBalance>> CalculateEmployeeLeaveBalance(
            String employeeId,
            DateTime employeeStartDate,
            DateTime calculationDate,
            int workingDaysPerWeek, // Added parameter
            List<LeaveType> leaveTypes,
            DataTable leaveHistory, // Keep DataTable or change to List<LeaveHistoryItem> if preferred
            List<ServiceRateIncrease> serviceRateIncreases)
        {
            // Pre-fetch Leave History into a lookup dictionary for efficiency
            // Key: LeaveTypeID, Value: List of (StartDate, Days) tuples for approved leave
            var leaveHistoryLookup = new Dictionary<string, List<(DateTime StartDate, decimal Days)>>();
            try
            {
                foreach (DataRow row in leaveHistory.Rows)
                {
                    // Only consider Approved leave for balance calculation
                    if (row["Status"]?.ToString().Equals("Approved", StringComparison.OrdinalIgnoreCase) == true)
                    {
                        var leaveTypeId = row["LeaveTypeID"].ToString();
                        var startDate = (DateTime)row["StartDate"];
                        var days = Convert.ToDecimal(row["Days"]); // Ensure correct conversion

                        if (!leaveHistoryLookup.ContainsKey(leaveTypeId))
                        {
                            leaveHistoryLookup[leaveTypeId] = new List<(DateTime StartDate, decimal Days)>();
                        }
                        // Store start date and days for potential cycle-based calculation
                        leaveHistoryLookup[leaveTypeId].Add((startDate, days));
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"Error pre-fetching leave history: {ex.Message}");
                // Consider re-throwing or returning an empty list/error indicator
                throw new ApplicationException("Error processing leave history data.", ex);
            }

            var balances = new List<LeaveBalance>();

            foreach (var leaveType in leaveTypes)
            {
                var balance = new LeaveBalance
                {
                    LeaveTypeID = leaveType.LeaveTypeID,
                    LeaveType = leaveType.LeaveTypeName
                };

                try
                {
                    // Calculate the raw accrued/available balance
                    balance.TotalAccrued = CalculateAccruedLeave(
                        leaveType,
                        employeeStartDate,
                        calculationDate,
                        workingDaysPerWeek,
                        serviceRateIncreases,
                        leaveHistoryLookup);

                    // Available is typically the same as accrued unless pending leave is considered (handled externally)
                    balance.Available = balance.TotalAccrued;

                    // --- Note: Calculation of Lost / ProjectedEndOfYear is complex ---
                    // It depends heavily on specific company policy regarding forfeiture timing (e.g., 6 months after cycle end for BCEA minimums).
                    // This basic calculator focuses on the *current* balance.
                    // These fields are left as 0 and should be calculated by separate, policy-specific logic if needed.
                    balance.Lost = 0;
                    balance.ProjectedEndOfYear = 0;

                }
                catch (Exception ex)
                {
                    Debug.WriteLine($"Error calculating leave for {leaveType.LeaveTypeName} (ID: {leaveType.LeaveTypeID}): {ex.Message}");
                    // Optionally add a placeholder balance indicating error, or skip it
                    balance.TotalAccrued = 0; // Set to 0 on error?
                    balance.Available = 0;
                    // Consider logging the error more formally
                }

                balances.Add(balance);
            }

            return balances;
        }

        // --- Leave Type Specific Calculation Dispatcher ---

        private decimal CalculateAccruedLeave(
           LeaveType leaveType,
           DateTime employeeStartDate,
           DateTime calculationDate,
           int workingDaysPerWeek,
           List<ServiceRateIncrease> serviceRateIncreases,
           Dictionary<string, List<(DateTime StartDate, decimal Days)>> leaveHistoryLookup)
        {
            // Use case-insensitive comparison for flexibility
            switch (leaveType.LeaveTypeName.ToLowerInvariant())
            {
                case "annual leave":
                    return CalculateAnnualLeave(leaveType, employeeStartDate, calculationDate, workingDaysPerWeek, serviceRateIncreases, leaveHistoryLookup);

                case "sick leave":
                    return CalculateSickLeave(leaveType, employeeStartDate, calculationDate, workingDaysPerWeek, leaveHistoryLookup);

                case "family responsibility leave":
                    return CalculateFamilyResponsibilityLeave(leaveType, employeeStartDate, calculationDate, workingDaysPerWeek, leaveHistoryLookup);

                case "maternity leave":
                    // Maternity leave is an entitlement, not typically an accrued balance.
                    // Return the configured entitlement amount (e.g., from ResetValue) or 0.
                    return leaveType.ResetValue > 0 ? leaveType.ResetValue : 0; // Or a fixed value like 120? Configurable is better.

                case "unpaid leave":
                    // Unpaid leave does not accrue a balance.
                    return 0;

                default:
                    // Handle custom or other leave types based on their configuration flags
                    return CalculateGenericLeave(leaveType, employeeStartDate, calculationDate, workingDaysPerWeek, serviceRateIncreases, leaveHistoryLookup);
            }
        }

        // --- Calculation Methods for Specific Leave Types ---

        private decimal CalculateAnnualLeave(
            LeaveType leaveType,
            DateTime employeeStartDate,
            DateTime calculationDate,
            int workingDaysPerWeek,
            List<ServiceRateIncrease> serviceRateIncreases,
            Dictionary<string, List<(DateTime StartDate, decimal Days)>> leaveHistoryLookup)
        {
            // --- BCEA Context ---
            // Min: 21 consecutive days / year (=> 15 working days for 5-day week, 18 for 6-day week).
            // Accrual: ~1 day per 17 days worked. Common practice: 1.25 days/month (5-day) or 1.5 days/month (6-day).
            // Cycle: Typically 12 months from start date or fixed company date.
            // Forfeiture: Minimum must be grantable up to 6 months after cycle end. Company policy dictates further carry-over/forfeiture.

            // --- Simplified Calculation (Current Cycle Focus) ---
            // This calculates the balance *within the current annual cycle*. It doesn't automatically handle complex multi-year carry-over rules dictated by Reset* flags without specific policy definition.

            DateTime cycleStartDate = GetCurrentCycleStartDate(employeeStartDate, calculationDate);
            decimal monthlyAccrualRate = GetEffectiveMonthlyRate(leaveType, workingDaysPerWeek, employeeStartDate, calculationDate, serviceRateIncreases);

            // Calculate full months passed since cycle start
            int monthsInCycle = CalculateMonthDifference(cycleStartDate, calculationDate);
            if (monthsInCycle < 0) monthsInCycle = 0; // Avoid negative months if calc date is before cycle start somehow

            // Calculate potential accrual within the current cycle up to the calculation date
            // Note: This assumes accrual happens at the start/end of the month consistently.
            decimal accruedInCycle = monthsInCycle * monthlyAccrualRate;

            // Add partial accrual for the very first month of employment if applicable
            // This logic might need refinement based on exact accrual timing (start/end of month)
            if (calculationDate.Year == employeeStartDate.Year && calculationDate.Month == employeeStartDate.Month)
            {
                // Simplified: Grant first month's accrual if within the first month
                accruedInCycle = monthlyAccrualRate;
            }
            else if (cycleStartDate == employeeStartDate && monthsInCycle == 0)
            {
                // Started this cycle, but less than a full month passed? Pro-rata or full month? Assume full month for simplicity here.
                accruedInCycle = monthlyAccrualRate * ((calculationDate - cycleStartDate).Days + 1) / (decimal)DateTime.DaysInMonth(cycleStartDate.Year, cycleStartDate.Month); // Pro-rata example
                                                                                                                                                                                // Or just accrue if calcDate >= startdate + some grace period? Needs policy.
                                                                                                                                                                                // Sticking to simpler monthly model: accruedInCycle = 0 if monthsInCycle is 0 initially.
                accruedInCycle = 0; // Resetting to 0 if first month isn't fully passed based on monthsInCycle logic.
            }


            // Initial Balance: Potentially add carry-over from previous cycle if ResetYearly=false? Needs policy.
            // For simplicity, assume ResetYearly=true clears balance or ResetValue applies, handled externally or via generic if needed.
            // This calculation focuses on *this cycle's* accrual.

            decimal leaveTakenThisCycle = SumLeaveTakenInPeriod(leaveType.LeaveTypeID, cycleStartDate, calculationDate, leaveHistoryLookup);

            decimal currentBalance = accruedInCycle - leaveTakenThisCycle;

            // Apply Maximum Cap if configured
            if (leaveType.HasMaximum)
            {
                currentBalance = Math.Min(currentBalance, leaveType.MaximumDays);
            }

            // Ensure balance is not negative
            currentBalance = Math.Max(0, currentBalance);

            return Math.Round(currentBalance, 2); // Round final balance
        }

        private decimal CalculateSickLeave(
             LeaveType leaveType,
             DateTime employeeStartDate,
             DateTime calculationDate,
             int workingDaysPerWeek,
             Dictionary<string, List<(DateTime StartDate, decimal Days)>> leaveHistoryLookup)
        {
            // --- BCEA Rules Implemented ---
            // 1. First 6 Months: 1 day per 26 days worked.
            // 2. After 6 Months: 3-year cycle granting entitlement = days normally worked in 6 weeks.

            int monthsDifference = CalculateMonthDifference(employeeStartDate, calculationDate);

            // --- Rule 1: First 6 Months ---
            if (monthsDifference < 6 && calculationDate >= employeeStartDate) // Ensure calc date is not before start date
            {
                int daysWorkedApproximation = (calculationDate - employeeStartDate).Days + 1;
                // Calculate potential accrual (floor is correct, complete day after 26 worked)
                decimal accruedDuringPeriod = Math.Floor((decimal)daysWorkedApproximation / 26m);

                // Sum sick leave taken from start date up to calculation date
                decimal leaveTakenDuringPeriod = SumLeaveTakenInPeriod(leaveType.LeaveTypeID, employeeStartDate, calculationDate, leaveHistoryLookup);

                decimal availableBalance = accruedDuringPeriod - leaveTakenDuringPeriod;
                availableBalance = Math.Max(0, availableBalance); // Cannot be negative
                return Math.Round(availableBalance, 2);
            }
            // --- Rule 2: After First 6 Months (3-Year Cycle) ---
            else if (calculationDate >= employeeStartDate)
            {
                // Determine cycle entitlement based on working days
                decimal cycleGrant = (workingDaysPerWeek == 6) ? 36 : 30;
                // Allow override from ResetValue if it's specifically configured for the cycle grant
                if (leaveType.ResetValue > 0 && leaveType.ResetValue != cycleGrant)
                {
                    // Use ResetValue if it seems intended as the cycle grant (e.g., company policy differs slightly)
                    // Be cautious with this override - ensure ResetValue is meant for this.
                    // cycleGrant = leaveType.ResetValue; // Uncomment if ResetValue DEFINITELY holds the cycle grant amount
                }

                // Calculate current 3-year cycle start/end dates
                TimeSpan totalElapsed = calculationDate - employeeStartDate;
                int cycleIndex = (int)Math.Floor(totalElapsed.TotalDays / (365.25 * 3)); // Approx cycle index
                DateTime cycleStartDate = employeeStartDate.AddYears(cycleIndex * 3);
                // DateTime cycleEndDate = cycleStartDate.AddYears(3).AddDays(-1); // End date isn't strictly needed for taken leave sum

                // Sum sick leave taken where the leave START DATE falls within the current cycle.
                decimal leaveTakenThisCycle = SumLeaveTakenInPeriod(leaveType.LeaveTypeID, cycleStartDate, calculationDate, leaveHistoryLookup);

                decimal availableBalance = cycleGrant - leaveTakenThisCycle;
                availableBalance = Math.Max(0, availableBalance); // Cannot be negative

                // Apply overall MaximumDays cap if it exists and is DIFFERENT from the cycle grant
                if (leaveType.HasMaximum && leaveType.MaximumDays < cycleGrant)
                {
                    availableBalance = Math.Min(availableBalance, leaveType.MaximumDays);
                }

                return Math.Round(availableBalance, 2);
            }
            else
            {
                // Calculation date is before employee start date
                return 0;
            }
        }

        private decimal CalculateFamilyResponsibilityLeave(
            LeaveType leaveType,
            DateTime employeeStartDate,
            DateTime calculationDate,
            int workingDaysPerWeek,
            Dictionary<string, List<(DateTime StartDate, decimal Days)>> leaveHistoryLookup)
        {
            // --- BCEA Rules ---
            // Eligibility: Worked > 4 months AND works >= 4 days/week.
            // Entitlement: 3 days per annual leave cycle.

            // 1. Check Eligibility
            int monthsWorked = CalculateMonthDifference(employeeStartDate, calculationDate);
            if (monthsWorked < 4 || workingDaysPerWeek < 4 || calculationDate < employeeStartDate)
            {
                return 0; // Not eligible or calculation date is before start date
            }

            // 2. Determine Current Annual Cycle (using employee start date anniversary)
            DateTime cycleStartDate = GetCurrentCycleStartDate(employeeStartDate, calculationDate);

            // 3. Base Entitlement for the cycle
            decimal cycleEntitlement = leaveType.ResetValue > 0 ? leaveType.ResetValue : 3; // Default to 3 days, allow override via ResetValue

            // 4. Sum leave taken within the current cycle
            decimal leaveTakenThisCycle = SumLeaveTakenInPeriod(leaveType.LeaveTypeID, cycleStartDate, calculationDate, leaveHistoryLookup);

            // 5. Calculate Balance
            decimal availableBalance = cycleEntitlement - leaveTakenThisCycle;
            availableBalance = Math.Max(0, availableBalance); // Cannot be negative

            // Apply maximum cap if specified (unlikely for FRL, but possible)
            if (leaveType.HasMaximum)
            {
                availableBalance = Math.Min(availableBalance, leaveType.MaximumDays);
            }

            return Math.Round(availableBalance, 2);
        }
        
        private decimal CalculateGenericLeave(
            LeaveType leaveType,
            DateTime employeeStartDate,
            DateTime calculationDate,
            int workingDaysPerWeek,
            List<ServiceRateIncrease> serviceRateIncreases,
            Dictionary<string, List<(DateTime StartDate, decimal Days)>> leaveHistoryLookup)
        {
            // Handles custom leave types based on flags. Assumes an 'annual' cycle unless flags imply otherwise.

            if (calculationDate < employeeStartDate) return 0;

            DateTime cycleStartDate = GetCurrentCycleStartDate(employeeStartDate, calculationDate); // Assume annual cycle for generic
            decimal currentBalance = 0;

            if (leaveType.IncreaseMonthly)
            {
                // --- Monthly Accrual Logic ---
                decimal monthlyAccrualRate = GetEffectiveMonthlyRate(leaveType, workingDaysPerWeek, employeeStartDate, calculationDate, serviceRateIncreases);
                int monthsInCycle = CalculateMonthDifference(cycleStartDate, calculationDate);
                if (monthsInCycle < 0) monthsInCycle = 0;

                // Potential accrual since cycle start
                currentBalance = monthsInCycle * monthlyAccrualRate;

                // TODO: Add handling for first month pro-rata/full accrual if needed, similar to Annual Leave calc.
            }
            else
            {
                // --- Fixed Grant Logic ---
                // Assumes ResetValue is the fixed amount granted per cycle (e.g., study leave grant)
                currentBalance = leaveType.ResetValue;
            }

            // Subtract leave taken within the current cycle
            decimal leaveTakenThisCycle = SumLeaveTakenInPeriod(leaveType.LeaveTypeID, cycleStartDate, calculationDate, leaveHistoryLookup);
            currentBalance -= leaveTakenThisCycle;

            // Apply Maximum Cap if configured
            if (leaveType.HasMaximum)
            {
                currentBalance = Math.Min(currentBalance, leaveType.MaximumDays);
            }

            // Apply Reset logic (Simplified: Assume reset happens *at cycle start* if ResetYearly is true)
            // Complex reset logic (e.g., reset *down to* ResetValue if balance > ResetValue at year end) needs more specific policy definition.
            // If ResetYearly is true, this calculation effectively shows balance accrued *since* the last reset (cycle start).

            // Ensure balance is not negative
            currentBalance = Math.Max(0, currentBalance);

            return Math.Round(currentBalance, 2);
        }
        // --- Helper Methods ---

        /// <summary>
        /// Sums the approved leave days taken for a specific leave type within a given date range.
        /// </summary>
        private decimal SumLeaveTakenInPeriod(
            string leaveTypeId,
            DateTime periodStartDate,
            DateTime periodEndDate,
            Dictionary<string, List<(DateTime StartDate, decimal Days)>> leaveHistoryLookup)
        {
            decimal totalDaysTaken = 0;

            if (leaveHistoryLookup.TryGetValue(leaveTypeId, out var history) && history != null)
            {
                foreach (var record in history)
                {
                    // Check if the leave record's START date falls within the specified period
                    if (record.StartDate >= periodStartDate && record.StartDate <= periodEndDate)
                    {
                        totalDaysTaken += record.Days;
                    }
                    // Note: This doesn't handle leave that *spans* the period start/end date precisely unless split externally.
                    // It sums based on the StartDate falling within the period. This is usually sufficient for balance calcs.
                }
            }
            return totalDaysTaken;
        }

        /// <summary>
        /// Gets the start date of the current annual cycle based on employee start date.
        /// </summary>
        private DateTime GetCurrentCycleStartDate(DateTime employeeStartDate, DateTime calculationDate)
        {
            int yearsPassed = calculationDate.Year - employeeStartDate.Year;

            // Check if the calculation date is before the anniversary in the current year
            if (calculationDate.Month < employeeStartDate.Month ||
                (calculationDate.Month == employeeStartDate.Month && calculationDate.Day < employeeStartDate.Day))
            {
                yearsPassed--; // Still in the previous cycle year
            }

            return employeeStartDate.AddYears(yearsPassed);
        }

        /// <summary>
        /// Calculates the effective monthly accrual rate, considering defaults, configured rate, and service increases.
        /// </summary>
        private decimal GetEffectiveMonthlyRate(
            LeaveType leaveType,
            int workingDaysPerWeek,
            DateTime employeeStartDate,
            DateTime calculationDate,
            List<ServiceRateIncrease> serviceRateIncreases)
        {
            // Determine base rate: Use configured rate, or default annual leave rates.
            decimal baseMonthlyRate = leaveType.MonthlyIncreaseRate;
            if (baseMonthlyRate <= 0) // If no specific rate configured, use standard annual defaults
            {
                baseMonthlyRate = (workingDaysPerWeek == 6) ? 1.5m : 1.25m;
            }

            // Apply Service Rate Increase if applicable
            if (leaveType.ServiceRateIncrease && serviceRateIncreases != null)
            {
                return GetCurrentMonthlyRateFromIncreases(employeeStartDate, calculationDate, baseMonthlyRate, serviceRateIncreases);
            }
            else
            {
                return baseMonthlyRate;
            }
        }

        /// <summary>
        /// Determines the current monthly accrual rate based on years of service.
        /// </summary>
        private decimal GetCurrentMonthlyRateFromIncreases(
         DateTime employeeStartDate,
         DateTime currentDate, // The date to check service years against
         decimal baseRate,
         List<ServiceRateIncrease> increases)
        {
            if (increases == null || !increases.Any())
            {
                return baseRate;
            }

            // Calculate years of service completed as of the current date
            double yearsOfService = (currentDate - employeeStartDate).TotalDays / 365.25;

            // Find the highest applicable rate based on completed years of service
            var applicableIncrease = increases
                .Where(i => i.YearsService <= yearsOfService) // Rate applies if service years are met or exceeded
                .OrderByDescending(i => i.YearsService)       // Get the one for the most years (highest tier met)
                .FirstOrDefault();

            return applicableIncrease?.NewRate ?? baseRate; // Use the increased rate or fallback to the base rate
        }

        /// <summary>
        /// Calculates the difference in *full* months between two dates.
        /// </summary>
        private int CalculateMonthDifference(DateTime startDate, DateTime endDate)
        {
            if (endDate < startDate) return -1; // Or 0, depending on how it should be handled

            // Original logic: ((endDate.Year - startDate.Year) * 12) + endDate.Month - startDate.Month + (endDate.Day >= startDate.Day ? 0 : -1);
            // Let's simplify for clarity, this calculates *full* months passed.
            int months = (endDate.Year - startDate.Year) * 12 + endDate.Month - startDate.Month;

            // Adjust if the end day is before the start day in the final month
            if (endDate.Day < startDate.Day)
            {
                months--;
            }
            return months;
        }

    }
}
```

I want you to look at the South African Labour Laws on each leave type. I then want you to see what properties are common between each leave type and what is unique. I then want you to check the code above and see if it will be able to support calculating leave for an employee. If the code can be enhanced/improved and updated, please give me the completed updated copy paste version without any comments.