using ERPCore.Domain.Entities;
using ERPCore.Domain.Enums;
using ERPCore.Repositories.Interfaces;
using ERPCore.Services.Interfaces;
using ERPCore.System.Errors;
using Microsoft.EntityFrameworkCore;

namespace ERPCore.Services.Hrm;

/// <inheritdoc cref="IPayrollCalculationService"/>
public sealed class PayrollCalculationService : IPayrollCalculationService
{
    private readonly IRepository<EmployeeSalaryStructure> _structures;
    private readonly IRepository<AttendanceRecord> _attendance;
    private readonly IRepository<WorkShift> _workShifts;
    private readonly IEmployeeLoanService _loans;
    private readonly IPayrollStatutorySettingService _statutory;
    private readonly ITaxSlabService _taxSlabs;

    public PayrollCalculationService(
        IRepository<EmployeeSalaryStructure> structures, IRepository<AttendanceRecord> attendance,
        IRepository<WorkShift> workShifts, IEmployeeLoanService loans,
        IPayrollStatutorySettingService statutory, ITaxSlabService taxSlabs)
    {
        _structures = structures;
        _attendance = attendance;
        _workShifts = workShifts;
        _loans = loans;
        _statutory = statutory;
        _taxSlabs = taxSlabs;
    }

    public async Task<PayrollLine> CalculateAsync(Employee employee, int periodYear, int periodMonth, CancellationToken ct = default)
    {
        var periodStart = new DateTime(periodYear, periodMonth, 1);
        var periodEnd = periodStart.AddMonths(1).AddDays(-1);

        var structure = await _structures.Query().AsNoTracking()
            .Include(s => s.Lines).ThenInclude(l => l.SalaryComponent)
            .Where(s => s.EmployeeId == employee.EmployeeId && s.EffectiveFrom <= periodEnd && (s.EffectiveTo == null || s.EffectiveTo >= periodStart))
            .OrderByDescending(s => s.EffectiveFrom)
            .FirstOrDefaultAsync(ct)
            ?? throw new NotFoundException($"Employee {employee.EmployeeId} has no salary structure effective for {periodYear}-{periodMonth:00}.");

        var shift = await _workShifts.GetByIdAsync(employee.WorkShiftId, ct)
            ?? throw new NotFoundException($"Work shift {employee.WorkShiftId} was not found.");

        var records = await _attendance.Query().AsNoTracking()
            .Where(r => r.EmployeeId == employee.EmployeeId && r.AttendanceDate >= periodStart && r.AttendanceDate <= periodEnd)
            .ToListAsync(ct);

        var presentDays = records.Count(r => r.AttendanceStatus is AttendanceStatus.Present or AttendanceStatus.HalfDay);
        var absentDays = records.Count(r => r.AttendanceStatus == AttendanceStatus.Absent);
        // Simplification: all OnLeave days are currently treated as paid — AttendanceRecord doesn't
        // carry which LeaveType covered it, so unpaid-leave No-Pay isn't distinguished here yet (docs §8).
        var leaveDays = records.Count(r => r.AttendanceStatus == AttendanceStatus.OnLeave);
        var otMinutesTotal = records.Sum(r => r.OvertimeMinutes);
        var lateMinutesTotal = records.Sum(r => r.LateMinutes);

        var daysInMonth = DateTime.DaysInMonth(periodYear, periodMonth);
        var dailyRate = structure.BasicSalary / daysInMonth;
        var perMinuteRate = shift.StandardWorkingMinutes > 0 ? dailyRate / shift.StandardWorkingMinutes : 0m;

        var earningLines = structure.Lines.Where(l => l.SalaryComponent!.ComponentType == SalaryComponentType.Earning).ToList();
        var deductionLines = structure.Lines.Where(l => l.SalaryComponent!.ComponentType == SalaryComponentType.Deduction).ToList();

        var totalAllowances = earningLines.Sum(l => l.Amount);
        var otMultiplier = shift.OtMultiplier;
        var overtimeAmount = Math.Round(perMinuteRate * otMultiplier * otMinutesTotal, 2);
        var grossSalary = structure.BasicSalary + totalAllowances + overtimeAmount;

        var lateDeduction = Math.Round(perMinuteRate * lateMinutesTotal, 2);
        var noPayAmount = Math.Round(dailyRate * absentDays, 2);
        var otherDeductionsAmount = deductionLines.Sum(l => l.Amount);

        var dueInstallments = await _loans.GetDueInstallmentsAsync(employee.EmployeeId, periodYear, periodMonth, ct);
        var loanDeduction = dueInstallments.Sum(i => i.ScheduledAmount);

        var statutory = await _statutory.GetEffectiveAsync(periodStart, ct);
        var epfEtfBase = structure.BasicSalary + earningLines.Where(l => l.SalaryComponent!.IsEpfEtfApplicable).Sum(l => l.Amount);
        var epfEmployeeAmount = statutory is null ? 0m : Math.Round(epfEtfBase * statutory.EpfEmployeeRate, 2);
        var epfEmployerAmount = statutory is null ? 0m : Math.Round(epfEtfBase * statutory.EpfEmployerRate, 2);
        var etfEmployerAmount = statutory is null ? 0m : Math.Round(epfEtfBase * statutory.EtfEmployerRate, 2);

        var taxableIncome = structure.BasicSalary + earningLines.Where(l => l.SalaryComponent!.IsTaxable).Sum(l => l.Amount) + overtimeAmount;
        var slabs = await _taxSlabs.GetEffectiveSlabsAsync(periodStart, ct);
        var taxAmount = Math.Round(ComputeMarginalTax(taxableIncome, slabs), 2);

        var netSalary = grossSalary - lateDeduction - noPayAmount - loanDeduction - epfEmployeeAmount - taxAmount - otherDeductionsAmount;

        var line = new PayrollLine
        {
            EmployeeId = employee.EmployeeId,
            BasicSalary = structure.BasicSalary,
            TotalAllowances = totalAllowances,
            OvertimeAmount = overtimeAmount,
            GrossSalary = grossSalary,
            LateDeductionAmount = lateDeduction,
            NoPayAmount = noPayAmount,
            LoanDeductionAmount = loanDeduction,
            EpfEmployeeAmount = epfEmployeeAmount,
            EpfEmployerAmount = epfEmployerAmount,
            EtfEmployerAmount = etfEmployerAmount,
            TaxAmount = taxAmount,
            OtherDeductionsAmount = otherDeductionsAmount,
            NetSalary = netSalary,
            WorkingDays = presentDays + absentDays + leaveDays,
            PresentDays = presentDays,
            AbsentDays = absentDays,
            LeaveDays = leaveDays,
            OtMinutesTotal = otMinutesTotal,
            LateMinutesTotal = lateMinutesTotal
        };

        var sort = 0;
        line.Components.Add(new PayrollLineComponent { ComponentCategory = PayrollLineComponentCategory.Earning, Label = "Basic Salary", Amount = structure.BasicSalary, SortOrder = sort++ });
        foreach (var l in earningLines)
            line.Components.Add(new PayrollLineComponent { ComponentCategory = PayrollLineComponentCategory.Earning, SalaryComponentId = l.SalaryComponentId, Label = l.SalaryComponent!.Name, Amount = l.Amount, SortOrder = sort++ });
        if (overtimeAmount > 0)
            line.Components.Add(new PayrollLineComponent { ComponentCategory = PayrollLineComponentCategory.Earning, Label = "Overtime", Amount = overtimeAmount, SortOrder = sort++ });

        if (lateDeduction > 0) line.Components.Add(new PayrollLineComponent { ComponentCategory = PayrollLineComponentCategory.Deduction, Label = "Late Deduction", Amount = lateDeduction, SortOrder = sort++ });
        if (noPayAmount > 0) line.Components.Add(new PayrollLineComponent { ComponentCategory = PayrollLineComponentCategory.Deduction, Label = "No Pay", Amount = noPayAmount, SortOrder = sort++ });
        if (loanDeduction > 0) line.Components.Add(new PayrollLineComponent { ComponentCategory = PayrollLineComponentCategory.Deduction, Label = "Loan", Amount = loanDeduction, SortOrder = sort++ });
        foreach (var l in deductionLines)
            line.Components.Add(new PayrollLineComponent { ComponentCategory = PayrollLineComponentCategory.Deduction, SalaryComponentId = l.SalaryComponentId, Label = l.SalaryComponent!.Name, Amount = l.Amount, SortOrder = sort++ });
        line.Components.Add(new PayrollLineComponent { ComponentCategory = PayrollLineComponentCategory.Deduction, Label = "EPF (Employee)", Amount = epfEmployeeAmount, SortOrder = sort++ });
        line.Components.Add(new PayrollLineComponent { ComponentCategory = PayrollLineComponentCategory.Deduction, Label = "Tax", Amount = taxAmount, SortOrder = sort++ });

        line.Components.Add(new PayrollLineComponent { ComponentCategory = PayrollLineComponentCategory.EmployerContribution, Label = "EPF (Employer)", Amount = epfEmployerAmount, SortOrder = sort++ });
        line.Components.Add(new PayrollLineComponent { ComponentCategory = PayrollLineComponentCategory.EmployerContribution, Label = "ETF (Company Contribution)", Amount = etfEmployerAmount, SortOrder = sort++ });

        return line;
    }

    /// <summary>Standard ascending marginal-slab computation over taxable income.</summary>
    private static decimal ComputeMarginalTax(decimal taxableIncome, List<TaxSlab> slabs)
    {
        if (taxableIncome <= 0 || slabs.Count == 0) return 0m;

        var tax = 0m;
        foreach (var slab in slabs.OrderBy(s => s.LowerBound))
        {
            if (taxableIncome <= slab.LowerBound) continue;
            var upper = slab.UpperBound ?? taxableIncome;
            var taxableInBand = Math.Min(taxableIncome, upper) - slab.LowerBound;
            if (taxableInBand <= 0) continue;
            tax += taxableInBand * slab.Rate;
        }
        return tax;
    }
}
