using ERPCore.Domain.Entities;
using ERPCore.Domain.Enums;
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>
/// Loan/Advance service (FR-HR-PAY-03). Creating a loan generates its full
/// installment schedule up front; installments flip Pending→Deducted only when
/// their consuming PayrollRun reaches Locked (docs/12-BACKEND-HRM.md A.4), handled
/// by <see cref="Services.Hrm.PayrollRunService"/>, not here.
/// </summary>
public sealed class EmployeeLoanService : IEmployeeLoanService
{
    private readonly IRepository<EmployeeLoan> _loans;
    private readonly IRepository<Employee> _employees;
    private readonly INumberSequenceService _numberSequence;
    private readonly IUnitOfWork _uow;

    public EmployeeLoanService(
        IRepository<EmployeeLoan> loans, IRepository<Employee> employees, INumberSequenceService numberSequence, IUnitOfWork uow)
    {
        _loans = loans;
        _employees = employees;
        _numberSequence = numberSequence;
        _uow = uow;
    }

    public async Task<List<EmployeeLoanDto>> ListAsync(int employeeId, CancellationToken ct = default)
    {
        var rows = await _loans.Query().AsNoTracking()
            .Include(l => l.Installments)
            .Where(l => l.EmployeeId == employeeId)
            .OrderByDescending(l => l.CreatedAt)
            .ToListAsync(ct);
        return rows.Select(Map).ToList();
    }

    public async Task<EmployeeLoanDto?> GetAsync(int employeeId, int loanId, CancellationToken ct = default)
    {
        var loan = await _loans.Query().AsNoTracking()
            .Include(l => l.Installments)
            .FirstOrDefaultAsync(l => l.EmployeeLoanId == loanId && l.EmployeeId == employeeId, ct);
        return loan is null ? null : Map(loan);
    }

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

        var docNo = await _numberSequence.NextAsync("LOAN", ct);
        var loan = new EmployeeLoan
        {
            DocNo = docNo,
            EmployeeId = employeeId,
            LoanKind = request.LoanKind,
            PrincipalAmount = request.PrincipalAmount,
            InterestRate = request.InterestRate,
            InstallmentAmount = request.InstallmentAmount,
            NumberOfInstallments = request.NumberOfInstallments,
            StartYear = request.StartYear,
            StartMonth = request.StartMonth,
            OutstandingBalance = request.PrincipalAmount,
            Status = LoanStatus.Active,
            ApprovedBy = actorUserId,
            ApprovedAt = DateTime.UtcNow,
            CreatedBy = actorUserId,
            CreatedAt = DateTime.UtcNow
        };

        var year = request.StartYear;
        var month = request.StartMonth;
        for (var i = 1; i <= request.NumberOfInstallments; i++)
        {
            loan.Installments.Add(new LoanInstallment
            {
                InstallmentNumber = i,
                DueYear = year,
                DueMonth = month,
                ScheduledAmount = request.InstallmentAmount,
                Status = LoanInstallmentStatus.Pending
            });
            month++;
            if (month > 12) { month = 1; year++; }
        }

        await _loans.AddAsync(loan, ct);
        await _uow.SaveChangesAsync(ct);

        return Map(loan);
    }

    public async Task<List<LoanInstallment>> GetDueInstallmentsAsync(int employeeId, int periodYear, int periodMonth, CancellationToken ct = default)
    {
        return await _loans.Query()
            .Where(l => l.EmployeeId == employeeId && l.Status == LoanStatus.Active)
            .SelectMany(l => l.Installments)
            .Where(i => i.DueYear == periodYear && i.DueMonth == periodMonth && i.Status == LoanInstallmentStatus.Pending)
            .ToListAsync(ct);
    }

    private static EmployeeLoanDto Map(EmployeeLoan l) => new(
        l.EmployeeLoanId, l.DocNo, l.EmployeeId, l.LoanKind, l.PrincipalAmount, l.InterestRate, l.InstallmentAmount,
        l.NumberOfInstallments, l.StartYear, l.StartMonth, l.OutstandingBalance, l.Status,
        l.Installments.OrderBy(i => i.InstallmentNumber).Select(i => new LoanInstallmentDto(
            i.LoanInstallmentId, i.InstallmentNumber, i.DueYear, i.DueMonth, i.ScheduledAmount, i.PaidAmount, i.PayrollRunId, i.Status)).ToList(),
        l.CreatedAt);
}
