using ERPCore.Domain.Entities;
using ERPCore.Dtos.Hrm;
using ERPCore.Infra.UoW;
using ERPCore.Repositories.Interfaces;
using ERPCore.Services.Interfaces;
using ERPCore.System.Errors;
using Microsoft.EntityFrameworkCore;

namespace ERPCore.Services.Hrm;

/// <summary>Leave balance service (FR-HR-LV-03, docs/13-BACKEND-HRM-API.md §5).</summary>
public sealed class LeaveBalanceService : ILeaveBalanceService
{
    private readonly IRepository<LeaveBalance> _balances;
    private readonly IRepository<LeaveType> _leaveTypes;
    private readonly IRepository<Employee> _employees;
    private readonly IUnitOfWork _uow;

    public LeaveBalanceService(
        IRepository<LeaveBalance> balances, IRepository<LeaveType> leaveTypes, IRepository<Employee> employees, IUnitOfWork uow)
    {
        _balances = balances;
        _leaveTypes = leaveTypes;
        _employees = employees;
        _uow = uow;
    }

    public async Task<List<LeaveBalanceDto>> ListAsync(int employeeId, int? year, CancellationToken ct = default)
    {
        var effectiveYear = year ?? DateTime.UtcNow.Year;
        var rows = await _balances.Query().AsNoTracking()
            .Include(b => b.LeaveType)
            .Where(b => b.EmployeeId == employeeId && b.Year == effectiveYear)
            .ToListAsync(ct);

        return rows.Select(Map).ToList();
    }

    public async Task<List<LeaveBalanceDto>> ApplyAdjustmentsAsync(int employeeId, UpdateLeaveBalancesRequest request, CancellationToken ct = default)
    {
        if (!await _employees.Query().AnyAsync(e => e.EmployeeId == employeeId, ct))
            throw new NotFoundException($"Employee {employeeId} was not found.");

        var result = new List<LeaveBalance>();
        foreach (var item in request.Items)
        {
            var balance = await GetOrCreateAsync(employeeId, item.LeaveTypeId, request.Year, ct);
            balance.AdjustmentDays = item.AdjustmentDays;
            balance.UpdatedAt = DateTime.UtcNow;
            result.Add(balance);
        }

        await _uow.SaveChangesAsync(ct);

        return result.Select(Map).ToList();
    }

    public async Task IncrementTakenDaysAsync(int employeeId, int leaveTypeId, int year, decimal days, CancellationToken ct = default)
    {
        var balance = await GetOrCreateAsync(employeeId, leaveTypeId, year, ct);
        balance.TakenDays += days;
        balance.UpdatedAt = DateTime.UtcNow;
        await _uow.SaveChangesAsync(ct);
    }

    private async Task<LeaveBalance> GetOrCreateAsync(int employeeId, int leaveTypeId, int year, CancellationToken ct)
    {
        var leaveType = await _leaveTypes.GetByIdAsync(leaveTypeId, ct)
            ?? throw new NotFoundException($"Leave type {leaveTypeId} was not found.");

        var balance = await _balances.Query()
            .FirstOrDefaultAsync(b => b.EmployeeId == employeeId && b.LeaveTypeId == leaveTypeId && b.Year == year, ct);
        if (balance is not null)
        {
            balance.LeaveType = leaveType;
            return balance;
        }

        balance = new LeaveBalance
        {
            EmployeeId = employeeId,
            LeaveTypeId = leaveTypeId,
            Year = year,
            EntitledDays = leaveType.AccrualPerYear,
            LeaveType = leaveType
        };
        await _balances.AddAsync(balance, ct);
        return balance;
    }

    private static LeaveBalanceDto Map(LeaveBalance b) => new(
        b.LeaveBalanceId, b.EmployeeId, b.LeaveTypeId, b.LeaveType?.Name, b.Year,
        b.EntitledDays, b.TakenDays, b.CarriedForwardDays, b.AdjustmentDays,
        b.EntitledDays + b.CarriedForwardDays + b.AdjustmentDays - b.TakenDays);
}
