using ERPCore.Domain.Entities;
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common;
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>
/// Payroll run orchestration (FR-HR-PAY-05/06). Maps the business's 5-step flow onto
/// 3 stored states (docs/12-BACKEND-HRM.md A.4): Generate→Draft, Review is a human
/// action, Approve→Approved, Lock→Locked (the point loan installments and attendance
/// batches are stamped consumed — deliberately deferred from Generate so a
/// discarded/regenerated Draft never prematurely consumes them), Generate Payslips is
/// an action gated on Locked.
/// </summary>
public sealed class PayrollRunService : IPayrollRunService
{
    private readonly IRepository<PayrollRun> _runs;
    private readonly IRepository<Employee> _employees;
    private readonly IRepository<AttendanceUploadBatch> _attendanceBatches;
    private readonly IRepository<LoanInstallment> _installments;
    private readonly IRepository<EmployeeLoan> _loans;
    private readonly IRepository<Payslip> _payslips;
    private readonly IPayrollCalculationService _calculation;
    private readonly IEmployeeLoanService _loanService;
    private readonly INumberSequenceService _numberSequence;
    private readonly IUnitOfWork _uow;

    public PayrollRunService(
        IRepository<PayrollRun> runs, IRepository<Employee> employees, IRepository<AttendanceUploadBatch> attendanceBatches,
        IRepository<LoanInstallment> installments, IRepository<EmployeeLoan> loans, IRepository<Payslip> payslips,
        IPayrollCalculationService calculation, IEmployeeLoanService loanService, INumberSequenceService numberSequence, IUnitOfWork uow)
    {
        _runs = runs;
        _employees = employees;
        _attendanceBatches = attendanceBatches;
        _installments = installments;
        _loans = loans;
        _payslips = payslips;
        _calculation = calculation;
        _loanService = loanService;
        _numberSequence = numberSequence;
        _uow = uow;
    }

    public async Task<PagedResponse<PayrollRunDto>> ListAsync(
        PageQuery query, int? periodYear, int? periodMonth, PayrollRunStatus? status, CancellationToken ct = default)
    {
        var q = _runs.Query().AsNoTracking().Include(r => r.Lines).AsQueryable();
        if (periodYear is not null) q = q.Where(r => r.PeriodYear == periodYear);
        if (periodMonth is not null) q = q.Where(r => r.PeriodMonth == periodMonth);
        if (status is not null) q = q.Where(r => r.Status == status);

        var total = await q.CountAsync(ct);
        var rows = await q.OrderByDescending(r => r.GeneratedAt)
            .Skip(query.Skip).Take(query.PageSize)
            .ToListAsync(ct);

        return PagedResponse<PayrollRunDto>.Create(rows.Select(Map).ToList(), query.Page, query.PageSize, total);
    }

    public async Task<PayrollRunDto?> GetAsync(int payrollRunId, CancellationToken ct = default)
    {
        var run = await _runs.Query().AsNoTracking().Include(r => r.Lines)
            .FirstOrDefaultAsync(r => r.PayrollRunId == payrollRunId, ct);
        return run is null ? null : Map(run);
    }

    public async Task<List<PayrollLineDto>> ListLinesAsync(int payrollRunId, CancellationToken ct = default)
    {
        var lines = await _runs.Query().AsNoTracking()
            .Where(r => r.PayrollRunId == payrollRunId)
            .SelectMany(r => r.Lines)
            .Include(l => l.Employee)
            .OrderBy(l => l.Employee!.FullName)
            .ToListAsync(ct);
        return lines.Select(MapLine).ToList();
    }

    public async Task<PayrollLineDetailDto?> GetLineAsync(int payrollRunId, int lineId, CancellationToken ct = default)
    {
        var line = await _runs.Query().AsNoTracking()
            .Where(r => r.PayrollRunId == payrollRunId)
            .SelectMany(r => r.Lines)
            .Include(l => l.Employee)
            .Include(l => l.Components).ThenInclude(c => c.SalaryComponent)
            .FirstOrDefaultAsync(l => l.PayrollLineId == lineId, ct);
        return line is null ? null : MapLineDetail(line);
    }

    public async Task<PayrollRunDto> GenerateAsync(GeneratePayrollRunRequest request, int actorUserId, CancellationToken ct = default)
    {
        var unconfirmed = await _attendanceBatches.Query().AnyAsync(b =>
            b.PeriodStart.Year == request.PeriodYear && b.PeriodStart.Month == request.PeriodMonth &&
            (b.Status == AttendanceBatchStatus.Draft || b.Status == AttendanceBatchStatus.Validated), ct);
        if (unconfirmed)
            throw new DomainException(ErrorCodes.AttendanceNotConfirmed,
                "One or more attendance batches for this period are not yet Confirmed.", 422);

        var employeesQuery = _employees.Query().Where(e => e.Status == EmployeeStatus.Active);
        if (request.BranchId is not null) employeesQuery = employeesQuery.Where(e => e.BranchId == request.BranchId);
        var employees = await employeesQuery.ToListAsync(ct);

        var docNo = await _numberSequence.NextAsync("PAY", ct);
        var run = new PayrollRun
        {
            DocNo = docNo,
            PeriodYear = request.PeriodYear,
            PeriodMonth = request.PeriodMonth,
            BranchId = request.BranchId,
            Status = PayrollRunStatus.Draft,
            GeneratedBy = actorUserId,
            GeneratedAt = DateTime.UtcNow
        };

        foreach (var employee in employees)
        {
            try
            {
                var line = await _calculation.CalculateAsync(employee, request.PeriodYear, request.PeriodMonth, ct);
                run.Lines.Add(line);
            }
            catch (NotFoundException)
            {
                // No effective salary structure for this employee this period — skip rather than fail the whole run.
            }
        }

        await _runs.AddAsync(run, ct);
        await _uow.SaveChangesAsync(ct);

        return Map(run);
    }

    public async Task<PayrollRunDto> ApproveAsync(int payrollRunId, int actorUserId, CancellationToken ct = default)
    {
        var run = await GetTrackedAsync(payrollRunId, ct);
        if (run.Status != PayrollRunStatus.Draft)
            throw new DomainException(ErrorCodes.Conflict, "Only a Draft payroll run can be approved.", 409);

        run.Status = PayrollRunStatus.Approved;
        run.ApprovedBy = actorUserId;
        run.ApprovedAt = DateTime.UtcNow;
        await _uow.SaveChangesAsync(ct);
        return Map(run);
    }

    public async Task<PayrollRunDto> LockAsync(int payrollRunId, int actorUserId, CancellationToken ct = default)
    {
        var run = await GetTrackedAsync(payrollRunId, ct);
        if (run.Status != PayrollRunStatus.Approved)
            throw new DomainException(ErrorCodes.Conflict, "Only an Approved payroll run can be locked.", 409);

        await _uow.ExecuteInTransactionAsync(async innerCt =>
        {
            // Stamp due loan installments as Deducted, decrementing outstanding balance.
            var employeeIds = run.Lines.Select(l => l.EmployeeId).ToList();
            var loans = await _loans.Query().Include(l => l.Installments)
                .Where(l => employeeIds.Contains(l.EmployeeId) && l.Status == LoanStatus.Active)
                .ToListAsync(innerCt);
            foreach (var loan in loans)
            {
                foreach (var installment in loan.Installments.Where(i =>
                    i.DueYear == run.PeriodYear && i.DueMonth == run.PeriodMonth && i.Status == LoanInstallmentStatus.Pending))
                {
                    installment.Status = LoanInstallmentStatus.Deducted;
                    installment.PaidAmount = installment.ScheduledAmount;
                    installment.PayrollRunId = run.PayrollRunId;
                    loan.OutstandingBalance = Math.Max(0, loan.OutstandingBalance - installment.ScheduledAmount);
                    if (loan.OutstandingBalance == 0) loan.Status = LoanStatus.Closed;
                }
            }

            // Flip consumed attendance batches Confirmed -> UsedInPayroll.
            var batches = await _attendanceBatches.Query()
                .Where(b => b.PeriodStart.Year == run.PeriodYear && b.PeriodStart.Month == run.PeriodMonth && b.Status == AttendanceBatchStatus.Confirmed)
                .ToListAsync(innerCt);
            foreach (var batch in batches) batch.Status = AttendanceBatchStatus.UsedInPayroll;

            run.Status = PayrollRunStatus.Locked;
            run.LockedBy = actorUserId;
            run.LockedAt = DateTime.UtcNow;

            await _uow.SaveChangesAsync(innerCt);
        }, ct);

        return Map(run);
    }

    public async Task<PayrollRunDto> UnlockAsync(int payrollRunId, string reason, int actorUserId, CancellationToken ct = default)
    {
        var run = await GetTrackedAsync(payrollRunId, ct);
        if (run.Status != PayrollRunStatus.Locked)
            throw new DomainException(ErrorCodes.PayrollPeriodLocked, "Only a Locked payroll run can be unlocked.", 409);

        await _uow.ExecuteInTransactionAsync(async innerCt =>
        {
            var installments = await _installments.Query()
                .Where(i => i.PayrollRunId == run.PayrollRunId)
                .Include(i => i.EmployeeLoan)
                .ToListAsync(innerCt);
            foreach (var installment in installments)
            {
                installment.Status = LoanInstallmentStatus.Pending;
                installment.PaidAmount = null;
                installment.PayrollRunId = null;
                if (installment.EmployeeLoan is not null)
                {
                    installment.EmployeeLoan.OutstandingBalance += installment.ScheduledAmount;
                    installment.EmployeeLoan.Status = LoanStatus.Active;
                }
            }

            var batches = await _attendanceBatches.Query()
                .Where(b => b.PeriodStart.Year == run.PeriodYear && b.PeriodStart.Month == run.PeriodMonth && b.Status == AttendanceBatchStatus.UsedInPayroll)
                .ToListAsync(innerCt);
            foreach (var batch in batches) batch.Status = AttendanceBatchStatus.Confirmed;

            run.Status = PayrollRunStatus.Approved;
            run.UnlockedBy = actorUserId;
            run.UnlockedAt = DateTime.UtcNow;
            run.UnlockReason = reason.Trim();

            await _uow.SaveChangesAsync(innerCt);
        }, ct);

        return Map(run);
    }

    public async Task<List<PayslipDto>> GeneratePayslipsAsync(int payrollRunId, CancellationToken ct = default)
    {
        var run = await _runs.Query().Include(r => r.Lines).FirstOrDefaultAsync(r => r.PayrollRunId == payrollRunId, ct)
            ?? throw new NotFoundException($"Payroll run {payrollRunId} was not found.");
        if (run.Status != PayrollRunStatus.Locked)
            throw new DomainException(ErrorCodes.Conflict, "Payslips can only be generated for a Locked payroll run.", 409);

        var existingLineIds = await _payslips.Query()
            .Where(p => run.Lines.Select(l => l.PayrollLineId).Contains(p.PayrollLineId))
            .Select(p => p.PayrollLineId)
            .ToListAsync(ct);

        var created = new List<Payslip>();
        foreach (var line in run.Lines.Where(l => !existingLineIds.Contains(l.PayrollLineId)))
        {
            var payslip = new Payslip { PayrollLineId = line.PayrollLineId, GeneratedAt = DateTime.UtcNow };
            created.Add(payslip);
            await _payslips.AddAsync(payslip, ct);
        }
        await _uow.SaveChangesAsync(ct);

        var all = await _payslips.Query().AsNoTracking()
            .Where(p => run.Lines.Select(l => l.PayrollLineId).Contains(p.PayrollLineId))
            .ToListAsync(ct);
        return all.Select(p => new PayslipDto(p.PayslipId, p.PayrollLineId, p.GeneratedAt, p.ReleasedAt, p.ReleasedBy)).ToList();
    }

    private async Task<PayrollRun> GetTrackedAsync(int payrollRunId, CancellationToken ct)
        => await _runs.Query().Include(r => r.Lines).FirstOrDefaultAsync(r => r.PayrollRunId == payrollRunId, ct)
            ?? throw new NotFoundException($"Payroll run {payrollRunId} was not found.");

    private static PayrollRunDto Map(PayrollRun r) => new(
        r.PayrollRunId, r.DocNo, r.PeriodYear, r.PeriodMonth, r.BranchId, r.Status,
        r.GeneratedBy, r.GeneratedAt, r.ApprovedBy, r.ApprovedAt, r.LockedBy, r.LockedAt,
        r.UnlockedBy, r.UnlockedAt, r.UnlockReason,
        r.Lines.Sum(l => l.GrossSalary), r.Lines.Sum(l => l.NetSalary), r.Lines.Count);

    private static PayrollLineDto MapLine(PayrollLine l) => new(
        l.PayrollLineId, l.PayrollRunId, l.EmployeeId, l.Employee?.EmployeeCode, l.Employee?.FullName,
        l.BasicSalary, l.TotalAllowances, l.OvertimeAmount, l.GrossSalary,
        l.LateDeductionAmount, l.NoPayAmount, l.LoanDeductionAmount,
        l.EpfEmployeeAmount, l.EpfEmployerAmount, l.EtfEmployerAmount, l.TaxAmount, l.OtherDeductionsAmount, l.NetSalary,
        l.WorkingDays, l.PresentDays, l.AbsentDays, l.LeaveDays, l.OtMinutesTotal, l.LateMinutesTotal);

    private static PayrollLineDetailDto MapLineDetail(PayrollLine l) => new(
        MapLine(l),
        l.Components.OrderBy(c => c.SortOrder).Select(c => new PayrollLineComponentDto(
            c.ComponentCategory, c.SalaryComponentId, c.Label, c.Amount, c.SortOrder)).ToList());
}
