package br.gov.sigefp.rh.service;

import br.gov.sigefp.budget.integration.BudgetIntegrationService;
import br.gov.sigefp.rh.api.dto.*;
import br.gov.sigefp.rh.domain.*;
import br.gov.sigefp.rh.repository.*;
import br.gov.sigefp.common.exception.BusinessException;
import br.gov.sigefp.common.exception.ResourceNotFoundException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * Serviço de aplicação para gestão de folha de pagamento.
 */
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class PayrollService {

        private final PayrollPeriodRepository payrollPeriodRepository;
        private final PayrollRunRepository payrollRunRepository;
        private final PayrollItemRepository payrollItemRepository;
        private final br.gov.sigefp.budget.integration.BudgetIntegrationService budgetIntegrationService;
        private final AgentRepository agentRepository;
        private final SalaryGridRepository salaryGridRepository;
        private final EarningTypeRepository earningTypeRepository;
        private final GlobalDeductionRuleRepository globalDeductionRuleRepository;
        private final TaxBracketRepository taxBracketRepository;
        private final AbsenceRepository absenceRepository;
        private final DeductionTypeRepository deductionTypeRepository;
        private final br.gov.sigefp.budget.repository.BudgetLineRepository budgetLineRepository;
        private final br.gov.sigefp.budget.repository.FiscalYearRepository fiscalYearRepository;
        private final AttendanceRecordRepository attendanceRecordRepository;
        private final br.gov.sigefp.common.service.PaymentGenerator paymentGenerator;

        private final br.gov.sigefp.rh.repository.FamilyAllowanceTableRepository familyAllowanceTableRepository;
        private final AgentContractRepository agentContractRepository;
        private final br.gov.sigefp.org.repository.MinistryRepository ministryRepository;
        private final br.gov.sigefp.org.repository.OrgUnitRepository orgUnitRepository;

        public PayrollPeriodDTO createPeriod(CreatePayrollPeriodDTO dto) {
                // Validação: mês entre 1 e 12
                if (dto.getMonth() < 1 || dto.getMonth() > 12) {
                        throw new BusinessException("Mês deve estar entre 1 e 12", "INVALID_MONTH",
                                        org.springframework.http.HttpStatus.BAD_REQUEST);
                }

                // Validação: não duplicar (fiscalYear, month)
                if (payrollPeriodRepository.existsByFiscalYearAndMonth(dto.getFiscalYear(), dto.getMonth())) {
                        throw new BusinessException(
                                        String.format("Período já existe para ano fiscal %d e mês %d",
                                                        dto.getFiscalYear(),
                                                        dto.getMonth()),
                                        "DUPLICATE_PERIOD", org.springframework.http.HttpStatus.CONFLICT);
                }

                PayrollPeriod period = PayrollPeriod.builder()
                                .fiscalYear(dto.getFiscalYear())
                                .month(dto.getMonth())
                                .startDate(dto.getStartDate())
                                .endDate(dto.getEndDate())
                                .status("OPEN")
                                .build();

                PayrollPeriod saved = payrollPeriodRepository.save(period);
                return toPeriodDTO(saved);
        }

        @Transactional(readOnly = true)
        public List<PayrollPeriodDTO> findAllPeriods() {
                return payrollPeriodRepository.findAll().stream()
                                .map(this::toPeriodDTO)
                                .collect(Collectors.toList());
        }

        public PayrollRunDTO createPayrollRun(CreatePayrollRunDTO dto) {
                PayrollPeriod period = payrollPeriodRepository.findById(dto.getPeriodId())
                                .orElseThrow(() -> new ResourceNotFoundException(
                                                "Período não encontrado: " + dto.getPeriodId()));

                if (!"OPEN".equals(period.getStatus())) {
                        throw new BusinessException(
                                        "Não é possível criar execução de folha para período fechado",
                                        "PERIOD_CLOSED",
                                        org.springframework.http.HttpStatus.BAD_REQUEST);
                }

                // Validation: Prevent duplicate run for same Period + Ministry + OrgUnit
                if (dto.getMinistryId() != null) {
                        List<PayrollRun> existingRuns = payrollRunRepository
                                        .findByPeriodIdAndMinistry(dto.getPeriodId(), dto.getMinistryId());
                        boolean duplicate = existingRuns.stream()
                                        .anyMatch(run -> java.util.Objects.equals(run.getOrgUnit(), dto.getOrgUnitId())
                                                        &&
                                                        java.util.Objects.equals(run.getRunType(), dto.getRunType()) &&
                                                        !"CLOSED".equals(run.getStatus())
                                                        && !"FAILED".equals(run.getStatus()));

                        if (duplicate) {
                                throw new BusinessException(
                                                "Já existe uma execução de folha ativa (não fechada/falha) para este período, ministério e unidade orgânica.",
                                                "DUPLICATE_PAYROLL_RUN",
                                                org.springframework.http.HttpStatus.CONFLICT);
                        }
                }

                PayrollRun payrollRun = PayrollRun.builder()
                                .period(period)
                                .ministry(dto.getMinistryId())
                                .orgUnit(dto.getOrgUnitId())
                                .runType(dto.getRunType())
                                .status("PENDING")
                                .createdAt(LocalDateTime.now())
                                .build();

                PayrollRun saved = payrollRunRepository.save(payrollRun);
                return toRunDTO(saved);
        }

        /**
         * Exclui uma execução de folha (apenas status PENDING ou GENERATED).
         */
        @Transactional
        public void deletePayrollRun(UUID id) {
                PayrollRun payrollRun = payrollRunRepository.findById(id)
                                .orElseThrow(() -> new IllegalArgumentException("Execução não encontrada: " + id));

                // Apenas PENDING ou GENERATED podem ser excluídos
                if (!"PENDING".equals(payrollRun.getStatus()) && !"GENERATED".equals(payrollRun.getStatus())) {
                        throw new BusinessException(
                                        "Apenas execuções PENDING ou GENERATED podem ser excluídas. Status atual: "
                                                        + payrollRun.getStatus(),
                                        "INVALID_STATUS",
                                        org.springframework.http.HttpStatus.BAD_REQUEST);
                }

                // Excluir itens primeiro
                List<PayrollItem> items = payrollItemRepository.findByPayrollRunId(id);
                if (!items.isEmpty()) {
                        payrollItemRepository.deleteAll(items);
                        log.info("Excluídos {} itens da execução {}", items.size(), id);
                }

                // Excluir a execução
                payrollRunRepository.delete(payrollRun);
                log.info("Execução de folha excluída: {}", id);
        }

        /**
         * Diagnóstico: Verifica todos os requisitos para geração de itens.
         */
        @Transactional(readOnly = true)
        public Map<String, Object> diagnosePayrollRun(UUID payrollRunId) {
                Map<String, Object> result = new LinkedHashMap<>();

                // 1. Verificar Payroll Run
                PayrollRun payrollRun = payrollRunRepository.findById(payrollRunId)
                                .orElseThrow(() -> new IllegalArgumentException(
                                                "Execução não encontrada: " + payrollRunId));

                result.put("payrollRunId", payrollRunId);
                result.put("payrollRunStatus", payrollRun.getStatus());
                result.put("periodId", payrollRun.getPeriod() != null ? payrollRun.getPeriod().getId() : null);
                result.put("ministryId", payrollRun.getMinistry());
                result.put("orgUnitId", payrollRun.getOrgUnit());

                // 2. Verificar Tipos de Provento
                Map<String, Boolean> earningTypes = new LinkedHashMap<>();
                earningTypes.put("SALARIO_BASE", earningTypeRepository.findByCode("SALARIO_BASE").isPresent());
                earningTypes.put("SUBSIDIO", earningTypeRepository.findByCode("SUBSIDIO").isPresent());
                earningTypes.put("ABONO_FAMILIA", earningTypeRepository.findByCode("ABONO_FAMILIA").isPresent());
                result.put("earningTypesConfigured", earningTypes);

                // 3. Verificar Todos os Agentes (não só ACTIVE)
                List<Agent> allAgents = agentRepository.findAll();
                result.put("totalAgentsInDatabase", allAgents.size());

                // Contadores por status
                Map<String, Long> agentsByStatus = allAgents.stream()
                                .collect(Collectors.groupingBy(
                                                a -> a.getStatus() != null ? a.getStatus() : "NULL",
                                                Collectors.counting()));
                result.put("agentsByStatus", agentsByStatus);

                // 4. Buscar apenas ACTIVE
                List<Agent> activeAgents = agentRepository.findByStatus("ACTIVE");
                result.put("activeAgentsCount", activeAgents.size());

                // 5. Diagnóstico detalhado por agente
                List<Map<String, Object>> agentDiagnosis = new ArrayList<>();

                // Buscar dados necessários
                List<SalaryGrid> allGrids = salaryGridRepository.findAll();
                Map<UUID, SalaryGrid> salaryGridMap = allGrids.stream()
                                .filter(g -> g.getStep() != null && g.getValidFrom().isBefore(LocalDate.now()) &&
                                                (g.getValidTo() == null || g.getValidTo().isAfter(LocalDate.now())))
                                .collect(Collectors.toMap(
                                                g -> g.getStep().getId(),
                                                g -> g,
                                                (existing, replacement) -> existing));
                result.put("salaryGridsCount", salaryGridMap.size());

                LocalDate pStart = payrollRun.getPeriod() != null ? payrollRun.getPeriod().getStartDate() : null;
                LocalDate pEnd = payrollRun.getPeriod() != null ? payrollRun.getPeriod().getEndDate() : null;
                result.put("periodStartDate", pStart);
                result.put("periodEndDate", pEnd);

                List<UUID> agentIds = activeAgents.stream().map(Agent::getId).collect(Collectors.toList());
                List<br.gov.sigefp.rh.domain.AgentContract> activeContracts = agentIds.isEmpty()
                                ? new ArrayList<>()
                                : agentContractRepository.findByAgentIdIn(agentIds);
                Map<UUID, List<br.gov.sigefp.rh.domain.AgentContract>> contractMap = activeContracts.stream()
                                .filter(c -> c.getIsActive() &&
                                                (c.getEndDate() == null || pEnd == null
                                                                || !c.getEndDate().isBefore(pStart))
                                                &&
                                                (pEnd == null || !c.getStartDate().isAfter(pEnd)))
                                .collect(Collectors.groupingBy(c -> c.getAgent().getId()));

                for (Agent agent : activeAgents) {
                        Map<String, Object> diag = new LinkedHashMap<>();
                        diag.put("matricula", agent.getMatricula());
                        diag.put("fullName", agent.getFullName());
                        diag.put("status", agent.getStatus());

                        // Verificar requisitos
                        List<String> issues = new ArrayList<>();

                        // Escalão salarial
                        if (agent.getSalaryStep() == null) {
                                issues.add("Sem escalão salarial (salaryStep)");
                        } else {
                                diag.put("salaryStep", agent.getSalaryStep());
                                SalaryGrid grid = salaryGridMap.get(agent.getSalaryStep());
                                if (grid == null) {
                                        issues.add("Tabela salarial não encontrada para o escalão "
                                                        + agent.getSalaryStep());
                                } else {
                                        diag.put("salaryGridFound", true);
                                        diag.put("baseAmount", grid.getBaseAmount());
                                }
                        }

                        // Data de posse
                        if (agent.getPosseDate() == null) {
                                issues.add("Sem data de posse (posseDate)");
                        } else {
                                diag.put("posseDate", agent.getPosseDate());
                        }

                        // Idade
                        if (agent.getBirthDate() != null) {
                                int age = java.time.Period.between(agent.getBirthDate(), LocalDate.now()).getYears();
                                diag.put("age", age);
                                if (age < 18) {
                                        issues.add("Menor de 18 anos (" + age + " anos)");
                                }
                        }

                        // Contrato ativo
                        List<br.gov.sigefp.rh.domain.AgentContract> agentContracts = contractMap.get(agent.getId());
                        if (agentContracts == null || agentContracts.isEmpty()) {
                                issues.add("Sem contrato ativo válido para o período");
                        } else {
                                diag.put("activeContractsCount", agentContracts.size());
                        }

                        diag.put("issues", issues);
                        diag.put("canGenerateItems", issues.isEmpty());

                        agentDiagnosis.add(diag);
                }

                result.put("agentDiagnosis", agentDiagnosis);

                // Resumo
                long eligibleCount = agentDiagnosis.stream()
                                .filter(d -> (Boolean) d.get("canGenerateItems"))
                                .count();
                result.put("eligibleAgentsCount", eligibleCount);

                return result;
        }

        /**
         * Gera automaticamente os itens de folha para todos os agentes ativos.
         * 
         * @param payrollRunId ID da execução de folha
         */
        public void generatePayrollItems(UUID payrollRunId) {
                PayrollRun payrollRun = payrollRunRepository.findById(payrollRunId)
                                .orElseThrow(() -> new IllegalArgumentException(
                                                "Execução de folha não encontrada: " + payrollRunId));

                if (!"PENDING".equals(payrollRun.getStatus())) {
                        throw new BusinessException("Apenas execuções PENDING podem gerar itens", "INVALID_RUN_STATUS",
                                        org.springframework.http.HttpStatus.PRECONDITION_FAILED);
                }

                UUID fiscalYearId = payrollRun.getPeriod().getFiscalYear() != null
                                ? fiscalYearRepository.findByYear(payrollRun.getPeriod().getFiscalYear())
                                                .map(br.gov.sigefp.budget.domain.FiscalYear::getId).orElse(null)
                                : null;

                // 1. Limpar itens anteriores
                List<PayrollItem> existingItems = payrollItemRepository.findByPayrollRunId(payrollRunId);
                if (!existingItems.isEmpty()) {
                        payrollItemRepository.deleteAll(existingItems);
                }

                // 2. Buscar tipos base
                EarningType baseSalaryType = earningTypeRepository.findByCode("SALARIO_BASE")
                                .orElseThrow(() -> new BusinessException(
                                                "Tipo de provento SALARIO_BASE não configurado",
                                                "CONFIG_MISSING",
                                                org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR));
                EarningType subsidyType = earningTypeRepository.findByCode("SUBSIDIO")
                                .orElseThrow(() -> new BusinessException(
                                                "Tipo de provento SUBSIDIO não configurado",
                                                "CONFIG_MISSING",
                                                org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR));
                EarningType familyAllowanceType = earningTypeRepository.findByCode("ABONO_FAMILIA")
                                .orElseThrow(() -> new BusinessException(
                                                "Tipo de provento ABONO_FAMILIA não configurado",
                                                "CONFIG_MISSING",
                                                org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR));

                List<Agent> activeAgents = agentRepository.findByStatus("ACTIVE");
                log.info("=== GERANDO ITENS DE FOLHA === PayrollRunId: {}", payrollRunId);
                log.info("Total de agentes ACTIVE encontrados: {}", activeAgents.size());
                if (activeAgents.isEmpty()) {
                        log.warn("Nenhum agente com status ACTIVE encontrado. Verifique os agentes cadastrados.");
                }

                // --- PERFORMANCE OPTIMIZATION: BATCH FETCHING ---
                // 1. Fetch relevant Salary Grids (Bulk)
                // Filter agents that have salarySteps
                List<UUID> steps = activeAgents.stream()
                                .map(Agent::getSalaryStep)
                                .filter(java.util.Objects::nonNull)
                                .distinct()
                                .collect(Collectors.toList());

                // Fetch ALL relevant Salary Grids in one go
                List<SalaryGrid> allGrids = salaryGridRepository.findAll(); // Optimization: Create
                                                                            // findByStepIdIn(steps) if possible, but
                                                                            // findAll is better than N queries
                Map<UUID, SalaryGrid> salaryGridMap = allGrids.stream()
                                .filter(g -> g.getStep() != null && g.getValidFrom().isBefore(LocalDate.now()) &&
                                                (g.getValidTo() == null || g.getValidTo().isAfter(LocalDate.now())))
                                .collect(Collectors.toMap(
                                                g -> g.getStep().getId(),
                                                g -> g,
                                                (existing, replacement) -> existing));

                // 2. Fetch Budget Lines (Bulk by OrgUnit + FiscalYear)
                // Assuming fiscalYearId is same for all
                List<br.gov.sigefp.budget.domain.BudgetLine> budgetLines = budgetLineRepository
                                .findByFiscalYearId(fiscalYearId);
                // Map: OrgUnitId + EconCode -> BudgetLine
                Map<String, br.gov.sigefp.budget.domain.BudgetLine> budgetLineMap = budgetLines.stream()
                                .filter(bl -> bl.getOrgUnit() != null)
                                .collect(Collectors.toMap(
                                                bl -> bl.getOrgUnit() + "_" + bl.getEconomicClass(),
                                                bl -> bl,
                                                (existing, replacement) -> existing));

                // 3. Fetch Attendance & Absences (Bulk by AgentIds + DateRange)
                LocalDate pStart = payrollRun.getPeriod().getStartDate();
                LocalDate pEnd = payrollRun.getPeriod().getEndDate();
                List<UUID> agentIds = activeAgents.stream().map(Agent::getId).collect(Collectors.toList());

                Map<UUID, List<AttendanceRecord>> attendanceMap = attendanceRecordRepository
                                .findByAgentIdInAndDateRange(agentIds, pStart, pEnd)
                                .stream()
                                .collect(Collectors.groupingBy(r -> r.getAgent().getId()));

                Map<UUID, List<Absence>> absenceMap = absenceRepository
                                .findByAgentIdInAndDateRange(agentIds, pStart, pEnd)
                                .stream()
                                .collect(Collectors.groupingBy(a -> a.getAgent().getId()));

                // 4. Fetch Active Contracts (Bulk)
                // Assuming we want any contract that is active AND overlaps with period
                List<br.gov.sigefp.rh.domain.AgentContract> activeContracts = agentContractRepository
                                .findByAgentIdIn(agentIds);
                Map<UUID, List<br.gov.sigefp.rh.domain.AgentContract>> contractMap = activeContracts.stream()
                                .filter(c -> c.getIsActive() &&
                                                (c.getEndDate() == null || !c.getEndDate().isBefore(pStart)) &&
                                                !c.getStartDate().isAfter(pEnd))
                                .collect(Collectors.groupingBy(c -> c.getAgent().getId()));

                for (Agent agent : activeAgents) {
                        // Regra: Deve ter posse e escalão salarial
                        if (agent.getSalaryStep() == null || agent.getPosseDate() == null) {
                                log.warn("Agente {} ignorado: Sem escalão ({}) ou data de posse ({}).",
                                                agent.getMatricula(), agent.getSalaryStep(), agent.getPosseDate());
                                continue;
                        }

                        // Regra: Idade Mínima 18 Anos
                        if (agent.getBirthDate() != null) {
                                int age = java.time.Period.between(agent.getBirthDate(), LocalDate.now()).getYears();
                                if (age < 18) {
                                        log.warn("Agente {} ignorado: Menor de idade ({} anos).", agent.getMatricula(),
                                                        age);
                                        continue;
                                }
                        }

                        // Regra: Deve ter Contrato Ativo Válido
                        List<br.gov.sigefp.rh.domain.AgentContract> agentContracts = contractMap.get(agent.getId());
                        if (agentContracts == null || agentContracts.isEmpty()) {
                                log.warn("Agente {} ignorado: Sem contrato ativo válido para o período.",
                                                agent.getMatricula());
                                continue;
                        }

                        // A. Salário Base (Fetch from Map)
                        SalaryGrid salary = salaryGridMap.get(agent.getSalaryStep());
                        if (salary == null) {
                                log.warn("Agente {} ignorado: Tabela salarial não encontrada para step {}.",
                                                agent.getMatricula(), agent.getSalaryStep());
                                continue;
                        }

                        // Calculate Proportional Fraction (0.0 to 1.0)
                        BigDecimal workingFraction = calculateWorkingDaysFraction(agent, pStart, pEnd);

                        // If fraction is 0, skip agent implies no working days in period
                        if (workingFraction.compareTo(BigDecimal.ZERO) == 0) {
                                log.info("Agente {} ignorado: 0 dias trabalhados no período.", agent.getMatricula());
                                continue;
                        }

                        BigDecimal baseAmount = salary.getBaseAmount().multiply(workingFraction).setScale(2,
                                        java.math.RoundingMode.HALF_UP);

                        // Budget Line Lookup (With Fallback)
                        UUID baseBudgetLineId = findBudgetLineWithFallback(budgetLineMap, agent.getOrgUnit(),
                                        baseSalaryType.getEconomicClassCode());

                        PayrollItem baseItem = PayrollItem.builder()
                                        .payrollRun(payrollRun)
                                        .agent(agent.getId())
                                        .lineType("EARNING")
                                        .earningType(baseSalaryType)
                                        .description("Vencimento Base" + (workingFraction.compareTo(BigDecimal.ONE) < 0
                                                        ? " (Proporcional)"
                                                        : ""))
                                        .quantity(workingFraction)
                                        .unitAmount(salary.getBaseAmount()) // Unit remains full rate
                                        .totalAmount(baseAmount) // Total is proportional
                                        .budgetLine(baseBudgetLineId)
                                        .build();
                        payrollItemRepository.save(baseItem);

                        BigDecimal grossSalary = baseAmount;
                        BigDecimal socialSecurityDeduction = BigDecimal.ZERO;

                        // A.1 Subsídio (Novo)
                        if (salary.getSubsidyAmount() != null
                                        && salary.getSubsidyAmount().compareTo(BigDecimal.ZERO) > 0) {
                                BigDecimal subsidyFull = salary.getSubsidyAmount();
                                // Apply same fraction to subsidy
                                BigDecimal subsidyAmount = subsidyFull.multiply(workingFraction).setScale(2,
                                                java.math.RoundingMode.HALF_UP);

                                grossSalary = grossSalary.add(subsidyAmount);

                                UUID subsidyBudgetLineId = findBudgetLineWithFallback(budgetLineMap, agent.getOrgUnit(),
                                                subsidyType.getEconomicClassCode());

                                PayrollItem subsidyItem = PayrollItem.builder()
                                                .payrollRun(payrollRun)
                                                .agent(agent.getId())
                                                .lineType("EARNING")
                                                .earningType(subsidyType)
                                                .description("Subsídio de Representação/Outros"
                                                                + (workingFraction.compareTo(BigDecimal.ONE) < 0
                                                                                ? " (Proporcional)"
                                                                                : ""))
                                                .quantity(workingFraction)
                                                .unitAmount(subsidyFull)
                                                .totalAmount(subsidyAmount)
                                                .budgetLine(subsidyBudgetLineId)
                                                .build();
                                payrollItemRepository.save(subsidyItem);
                        }

                        // B. Abono de Família
                        if (agent.getEligibleDependentsCount() != null && agent.getEligibleDependentsCount() > 0) {
                                BigDecimal familyAmount;
                                BigDecimal unitAmount;

                                java.util.Optional<br.gov.sigefp.rh.domain.FamilyAllowanceTable> rule = familyAllowanceTableRepository
                                                .findByDependentsAndDate(agent.getEligibleDependentsCount(),
                                                                LocalDate.now());

                                if (rule.isPresent()) {
                                        familyAmount = rule.get().getAmount();
                                        unitAmount = familyAmount.divide(
                                                        new BigDecimal(agent.getEligibleDependentsCount()), 2,
                                                        java.math.RoundingMode.HALF_UP);
                                } else {
                                        // Fallback padrão: 1000 por dependente
                                        unitAmount = new BigDecimal("1000");
                                        familyAmount = unitAmount
                                                        .multiply(new BigDecimal(agent.getEligibleDependentsCount()));
                                }

                                grossSalary = grossSalary.add(familyAmount);

                                UUID familyBudgetLineId = findBudgetLineWithFallback(budgetLineMap, agent.getOrgUnit(),
                                                familyAllowanceType.getEconomicClassCode());

                                PayrollItem familyItem = PayrollItem.builder()
                                                .payrollRun(payrollRun)
                                                .agent(agent.getId())
                                                .lineType("EARNING")
                                                .earningType(familyAllowanceType)
                                                .description("Abono de Família (" + agent.getEligibleDependentsCount()
                                                                + " dep.)")
                                                .quantity(new BigDecimal(agent.getEligibleDependentsCount()))
                                                .unitAmount(unitAmount)
                                                .totalAmount(familyAmount)
                                                .budgetLine(familyBudgetLineId)
                                                .build();
                                payrollItemRepository.save(familyItem);
                        }

                        // C. Faltas Injustificadas e Descontos por Exceção
                        // 1. Busca por Folha de Ponto Aprovada (FROM MAP)
                        List<AttendanceRecord> attendanceRecords = attendanceMap.getOrDefault(agent.getId(),
                                        java.util.Collections.emptyList());

                        // 2. Busca por Faltas Individuais (Modelo Legado) - FROM MAP
                        List<Absence> legacyAbsences = absenceMap.getOrDefault(agent.getId(),
                                        java.util.Collections.emptyList());

                        BigDecimal totalAbsenceDays = BigDecimal.ZERO;
                        List<Absence> deductedAbsences = new java.util.ArrayList<>();
                        // LocalDate pStart = payrollRun.getPeriod().getStartDate(); // Already defined
                        // above
                        // LocalDate pEnd = payrollRun.getPeriod().getEndDate(); // Already defined
                        // above

                        // Processar Registros da Folha de Ponto
                        for (AttendanceRecord rec : attendanceRecords) {
                                if (rec.getType() == AttendanceType.ABSENCE_UNJUSTIFIED) {
                                        totalAbsenceDays = totalAbsenceDays.add(BigDecimal.ONE);
                                }
                        }

                        // Processar legado (evitando contagem dupla se necessário, aqui assumimos soma
                        // simples por enquanto)
                        for (Absence abs : legacyAbsences) {
                                if (!abs.isJustified()) {
                                        LocalDate start = abs.getStartDate().isBefore(pStart) ? pStart
                                                        : abs.getStartDate();
                                        LocalDate end = abs.getEndDate().isAfter(pEnd) ? pEnd : abs.getEndDate();

                                        if (!start.isAfter(end)) {
                                                long days = java.time.temporal.ChronoUnit.DAYS.between(start, end) + 1;
                                                totalAbsenceDays = totalAbsenceDays.add(new BigDecimal(days));
                                                deductedAbsences.add(abs);
                                        }
                                }
                        }

                        if (totalAbsenceDays.compareTo(BigDecimal.ZERO) > 0) {
                                DeductionType absenceType = deductionTypeRepository.findByCode("FALTA_INJUSTIFICADA") // Ensure
                                                                                                                      // this
                                                                                                                      // code
                                                                                                                      // matches
                                                                                                                      // Seed
                                                .orElseThrow(() -> new BusinessException(
                                                                "Tipo de desconto FALTA_INJUSTIFICADA não configurado",
                                                                "CONFIG_MISSING",
                                                                org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR));

                                // Correção: Dias exatos do período
                                long daysInPeriod = java.time.temporal.ChronoUnit.DAYS.between(pStart, pEnd) + 1;
                                if (daysInPeriod == 0)
                                        daysInPeriod = 30; // Fallback

                                BigDecimal dailySalary = baseAmount.divide(new BigDecimal(daysInPeriod), 2,
                                                java.math.RoundingMode.HALF_UP);
                                BigDecimal deductionAmount = dailySalary.multiply(totalAbsenceDays);

                                UUID absenceBudgetLineId = findBudgetLineWithFallback(budgetLineMap, agent.getOrgUnit(),
                                                absenceType.getEconomicClassCode());

                                PayrollItem absenceItem = PayrollItem.builder()
                                                .payrollRun(payrollRun)
                                                .agent(agent.getId())
                                                .lineType("DEDUCTION")
                                                .deductionType(absenceType)
                                                .description("Faltas Injustificadas (" + totalAbsenceDays + " dias)")
                                                .quantity(totalAbsenceDays)
                                                .unitAmount(dailySalary)
                                                .totalAmount(deductionAmount)
                                                .budgetLine(absenceBudgetLineId)
                                                .build();
                                payrollItemRepository.save(absenceItem);

                                // P10: Sincronizar deductedInPayrollRunId nas ausências processadas
                                for (Absence abs : deductedAbsences) {
                                        abs.setDeductedInPayrollRunId(payrollRun.getId());
                                        absenceRepository.save(abs);
                                }
                        }

                        // 1. Regras Globais (INPS, Selo)
                        List<GlobalDeductionRule> globalRules = globalDeductionRuleRepository
                                        .findActiveRules(LocalDate.now());
                        for (GlobalDeductionRule rule : globalRules) {
                                BigDecimal deductionAmount = BigDecimal.ZERO;
                                if (rule.getPercentage() != null) {
                                        deductionAmount = grossSalary.multiply(rule.getPercentage());
                                } else if (rule.getAmountFixed() != null) {
                                        deductionAmount = rule.getAmountFixed();
                                }

                                if (deductionAmount.compareTo(BigDecimal.ZERO) > 0) {
                                        // Se for INPS, acumular para abater no IRPS
                                        if ("INPS".equals(rule.getDeductionType().getCode())) {
                                                socialSecurityDeduction = socialSecurityDeduction.add(deductionAmount);
                                        }

                                        UUID taxBudgetLineId = findBudgetLineWithFallback(budgetLineMap,
                                                        agent.getOrgUnit(),
                                                        rule.getDeductionType().getEconomicClassCode());

                                        PayrollItem item = PayrollItem.builder()
                                                        .payrollRun(payrollRun)
                                                        .agent(agent.getId())
                                                        .lineType("DEDUCTION")
                                                        .deductionType(rule.getDeductionType())
                                                        .description(rule.getDeductionType().getName())
                                                        .quantity(rule.getPercentage() != null ? rule.getPercentage()
                                                                        : BigDecimal.ONE)
                                                        .unitAmount(grossSalary)
                                                        .totalAmount(deductionAmount)
                                                        .budgetLine(taxBudgetLineId)
                                                        .build();
                                        payrollItemRepository.save(item);
                                }
                        }

                        // 2. Impostos baseados em Escalões (IRPS + Imposto Democracia)
                        // A Base para IRPS é (Bruto - INPS). Assumiremos a mesma para outros impostos
                        // de renda salvo indicação.
                        BigDecimal taxableBase = grossSalary.subtract(socialSecurityDeduction);
                        List<TaxBracket> allBrackets = taxBracketRepository.findActiveBrackets(LocalDate.now());

                        // Agrupar por Tipo de Desconto
                        java.util.Map<DeductionType, List<TaxBracket>> bracketsByType = allBrackets.stream()
                                        .collect(Collectors.groupingBy(TaxBracket::getDeductionType));

                        for (java.util.Map.Entry<DeductionType, List<TaxBracket>> entry : bracketsByType.entrySet()) {
                                DeductionType dedType = entry.getKey();
                                List<TaxBracket> brackets = entry.getValue();

                                // Encontrar o escalão correto para a base tributável
                                TaxBracket bracket = brackets.stream()
                                                .filter(b -> taxableBase.compareTo(b.getLowerLimit()) >= 0 &&
                                                                (b.getUpperLimit() == null || taxableBase
                                                                                .compareTo(b.getUpperLimit()) <= 0))
                                                .findFirst()
                                                .orElse(null);

                                if (bracket != null) {
                                        BigDecimal taxAmount = BigDecimal.ZERO;
                                        String descSuffix = "";

                                        // Lógica 1: Valor Fixo (Prioritário)
                                        if (bracket.getFixedAmount() != null
                                                        && bracket.getFixedAmount().compareTo(BigDecimal.ZERO) > 0) {
                                                taxAmount = bracket.getFixedAmount();
                                                descSuffix = " (Fixo)";
                                        }
                                        // Lógica 2: Percentual Progressivo
                                        else if (bracket.getRatePercentage() != null
                                                        && bracket.getRatePercentage().compareTo(BigDecimal.ZERO) > 0) {
                                                taxAmount = taxableBase.multiply(bracket.getRatePercentage());
                                                if (bracket.getExcessDeduction() != null) {
                                                        taxAmount = taxAmount.subtract(bracket.getExcessDeduction());
                                                }
                                                descSuffix = " (" + bracket.getRatePercentage()
                                                                .multiply(new BigDecimal("100")).setScale(0) + "%)";
                                        }

                                        if (taxAmount.compareTo(BigDecimal.ZERO) > 0) {
                                                UUID taxBudgetLineId = findBudgetLineWithFallback(budgetLineMap,
                                                                agent.getOrgUnit(), dedType.getEconomicClassCode());

                                                PayrollItem taxItem = PayrollItem.builder()
                                                                .payrollRun(payrollRun)
                                                                .agent(agent.getId())
                                                                .lineType("DEDUCTION")
                                                                .deductionType(dedType)
                                                                .description(dedType.getName() + descSuffix)
                                                                .quantity(bracket.getRatePercentage() != null
                                                                                ? bracket.getRatePercentage()
                                                                                : BigDecimal.ONE)
                                                                .unitAmount(bracket.getFixedAmount() != null
                                                                                ? bracket.getFixedAmount()
                                                                                : taxableBase)
                                                                .totalAmount(taxAmount)
                                                                .budgetLine(taxBudgetLineId)
                                                                .build();
                                                payrollItemRepository.save(taxItem);
                                        }
                                }
                        }
                }

                // Contar itens gerados
                List<PayrollItem> generatedItems = payrollItemRepository.findByPayrollRunId(payrollRunId);
                log.info("=== GERAÇÃO CONCLUÍDA === Total de itens gerados: {}", generatedItems.size());

                payrollRun.setStatus("GENERATED");
                payrollRunRepository.save(payrollRun);
        }

        private UUID findBudgetLineWithFallback(Map<String, br.gov.sigefp.budget.domain.BudgetLine> map, UUID orgUnitId,
                        String econCode) {
                if (orgUnitId == null || econCode == null)
                        return null;

                // 1. Tenta específica
                br.gov.sigefp.budget.domain.BudgetLine bl = map.get(orgUnitId + "_" + econCode);
                if (bl != null)
                        return bl.getId();

                // 2. Tenta genérica (99.00.00 - Padrão do Seeder)
                bl = map.get(orgUnitId + "_99.00.00");
                return (bl != null) ? bl.getId() : null;
        }

        private UUID findBudgetLine(Agent agent, String econCode, UUID fiscalYearId) {
                if (econCode == null || fiscalYearId == null)
                        return null;

                // Tenta encontrar por OrgUnit e Código Económico
                if (agent.getOrgUnit() != null) {
                        List<br.gov.sigefp.budget.domain.BudgetLine> unitLines = budgetLineRepository
                                        .findByFiscalYearIdAndOrgUnitId(fiscalYearId, agent.getOrgUnit());

                        // 1. Tenta específica
                        UUID specificId = unitLines.stream()
                                        .filter(bl -> econCode.equals(bl.getEconomicClass()))
                                        .map(br.gov.sigefp.budget.domain.BudgetLine::getId)
                                        .findFirst()
                                        .orElse(null);

                        if (specificId != null)
                                return specificId;

                        // 2. Tenta genérica (99.00.00)
                        return unitLines.stream()
                                        .filter(bl -> "99.00.00".equals(bl.getEconomicClass()))
                                        .map(br.gov.sigefp.budget.domain.BudgetLine::getId)
                                        .findFirst()
                                        .orElse(null);
                }
                return null;
        }

        /**
         * Processa uma execução de folha, criando execuções orçamentárias.
         */
        public void processPayrollRun(UUID payrollRunId) {
                PayrollRun payrollRun = payrollRunRepository.findById(payrollRunId)
                                .orElseThrow(() -> new IllegalArgumentException(
                                                "Execução de folha não encontrada: " + payrollRunId));

                if (!"GENERATED".equals(payrollRun.getStatus())) {
                        throw new BusinessException("Apenas execuções GENERATED podem ser processadas",
                                        "INVALID_RUN_STATUS",
                                        org.springframework.http.HttpStatus.PRECONDITION_FAILED);
                }

                List<PayrollItem> items = payrollItemRepository.findByPayrollRunId(payrollRunId);

                // Validação de Coerência: Todos os itens devem ter linha orçamental
                long missingBudget = items.stream().filter(i -> i.getBudgetLine() == null).count();
                if (missingBudget > 0) {
                        throw new BusinessException(
                                        "Não é possível processar: " + missingBudget + " itens sem dotação orçamental.",
                                        "BUDGET_LINE_MISSING",
                                        org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY);
                }

                payrollRun.setStatus("PROCESSING");
                payrollRunRepository.save(payrollRun);

                try {
                        Long periodId = BudgetIntegrationService.toPeriodId(
                                        payrollRun.getPeriod().getFiscalYear(),
                                        payrollRun.getPeriod().getMonth());

                        for (PayrollItem item : items) {
                                budgetIntegrationService.createCommitmentFromPayrollItem(
                                                item.getBudgetLine(),
                                                periodId,
                                                item.getTotalAmount(),
                                                item.getId());
                        }

                        // Integração com Tesouraria (Geração Automática de Ordens)
                        try {
                                paymentGenerator.generateOrdersFromPayrollRun(payrollRunId, null);
                                log.info("Ordens de pagamento geradas automaticamente após processamento da folha {}",
                                                payrollRunId);
                        } catch (Exception e) {
                                log.error("Erro na geração automática de ordens de pagamento (não bloqueante): {}",
                                                e.getMessage(), e);
                                // Não lançar exceção para não reverter o processamento orçamentário
                        }

                        payrollRun.setStatus("COMPLETED");
                        payrollRunRepository.save(payrollRun);
                        log.info("Folha processada com sucesso: runId={}, itens={}", payrollRunId, items.size());
                } catch (Exception e) {
                        payrollRun.setStatus("FAILED");
                        payrollRunRepository.save(payrollRun);
                        if (e instanceof BusinessException)
                                throw (BusinessException) e;
                        throw new BusinessException("Erro orçamental: " + e.getMessage(), "INTEGRATION_ERROR",
                                        org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR);
                }
        }

        /**
         * Encerra definitivamente uma execução de folha.
         * Dispara a liquidação orçamentária automática.
         */
        public void closePayrollRun(UUID payrollRunId) {
                PayrollRun payrollRun = payrollRunRepository.findById(payrollRunId)
                                .orElseThrow(() -> new ResourceNotFoundException(
                                                "Execução de folha não encontrada: " + payrollRunId));

                if (!"COMPLETED".equals(payrollRun.getStatus())) {
                        throw new BusinessException("Apenas folhas COMPLETED podem ser encerradas", "INVALID_STATUS",
                                        org.springframework.http.HttpStatus.BAD_REQUEST);
                }

                try {
                        // Dispara liquidação orçamentária para cada item da folha
                        List<PayrollItem> items = payrollItemRepository.findByPayrollRunId(payrollRunId);
                        Long periodId = BudgetIntegrationService.toPeriodId(payrollRun.getPeriod().getFiscalYear(),
                                        payrollRun.getPeriod().getMonth());

                        for (PayrollItem item : items) {
                                if (item.getBudgetLine() != null
                                                && item.getTotalAmount().compareTo(BigDecimal.ZERO) > 0) {
                                        budgetIntegrationService.createLiquidationFromPayrollItem(
                                                        item.getBudgetLine(),
                                                        periodId,
                                                        item.getTotalAmount(),
                                                        item.getId());
                                }
                        }

                        payrollRun.setStatus("CLOSED");
                        payrollRunRepository.save(payrollRun);
                        log.info("Folha de pagamento encerrada e liquidada: runId={}", payrollRunId);

                } catch (Exception e) {
                        log.error("Erro ao encerrar folha: {}", e.getMessage(), e);
                        throw new BusinessException("Erro ao processar liquidação orçamentária: " + e.getMessage(),
                                        "CLOSURE_ERROR", org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR);
                }
        }

        @Transactional(readOnly = true)
        public PayrollRunDTO findPayrollRunById(UUID id) {
                PayrollRun payrollRun = payrollRunRepository.findById(id)
                                .orElseThrow(() -> new IllegalArgumentException(
                                                "Execução de folha não encontrada: " + id));

                PayrollRunDTO dto = toRunDTO(payrollRun);

                // Carregar itens
                List<PayrollItem> items = payrollItemRepository.findByPayrollRunId(id);

                // Buscar nomes dos agentes em batch
                List<UUID> agentIds = items.stream()
                                .map(PayrollItem::getAgent)
                                .distinct()
                                .collect(Collectors.toList());

                Map<UUID, String> agentNames = new java.util.HashMap<>();
                if (!agentIds.isEmpty()) {
                        List<Agent> agents = agentRepository.findAllById(agentIds);
                        for (Agent agent : agents) {
                                agentNames.put(agent.getId(), agent.getFullName());
                        }
                }

                // Mapear itens com nome do agente
                dto.setItems(items.stream()
                                .map(item -> toItemDTOWithAgentName(item, agentNames.get(item.getAgent())))
                                .collect(Collectors.toList()));

                return dto;
        }

        /**
         * Gera resumo detalhado da folha de pagamento agrupado por funcionário.
         * Retorna uma lista de Maps onde cada Map representa uma linha (funcionário)
         * com colunas dinâmicas para cada tipo de provento/desconto.
         */
        @Transactional(readOnly = true)
        public java.util.List<java.util.LinkedHashMap<String, Object>> getPayrollDetailedSummary(UUID payrollRunId) {
                List<PayrollItem> items = payrollItemRepository.findByPayrollRunId(payrollRunId);

                if (items.isEmpty()) {
                        return new ArrayList<>();
                }

                // Buscar nomes e matrículas dos agentes
                List<UUID> agentIds = items.stream()
                                .map(PayrollItem::getAgent)
                                .distinct()
                                .collect(Collectors.toList());

                Map<UUID, Agent> agentMap = new java.util.HashMap<>();
                if (!agentIds.isEmpty()) {
                        List<Agent> agents = agentRepository.findAllById(agentIds);
                        for (Agent agent : agents) {
                                agentMap.put(agent.getId(), agent);
                        }
                }

                // Identificar todos os tipos de itens únicos (para criar colunas)
                java.util.Set<String> allDescriptions = items.stream()
                                .map(PayrollItem::getDescription)
                                .collect(java.util.stream.Collectors.toCollection(java.util.LinkedHashSet::new));

                // Agrupar itens por agente
                Map<UUID, List<PayrollItem>> itemsByAgent = items.stream()
                                .collect(Collectors.groupingBy(PayrollItem::getAgent));

                java.util.List<java.util.LinkedHashMap<String, Object>> result = new ArrayList<>();

                for (Map.Entry<UUID, List<PayrollItem>> entry : itemsByAgent.entrySet()) {
                        UUID agentId = entry.getKey();
                        List<PayrollItem> agentItems = entry.getValue();
                        Agent agent = agentMap.get(agentId);

                        java.util.LinkedHashMap<String, Object> row = new java.util.LinkedHashMap<>();

                        // Dados fixos do funcionário
                        row.put("matricula", agent != null ? agent.getMatricula() : "N/A");
                        row.put("funcionario", agent != null ? agent.getFullName() : agentId.toString());

                        // Calcular totais para proventos e descontos
                        BigDecimal totalProventos = BigDecimal.ZERO;
                        BigDecimal totalDescontos = BigDecimal.ZERO;

                        // Mapear valores por descrição
                        Map<String, BigDecimal> valuesByDesc = new java.util.HashMap<>();
                        for (PayrollItem item : agentItems) {
                                valuesByDesc.put(item.getDescription(), item.getTotalAmount());
                                if ("EARNING".equals(item.getLineType())) {
                                        totalProventos = totalProventos.add(item.getTotalAmount());
                                } else {
                                        totalDescontos = totalDescontos.add(item.getTotalAmount());
                                }
                        }

                        // Adicionar cada tipo de item como coluna
                        for (String desc : allDescriptions) {
                                BigDecimal value = valuesByDesc.getOrDefault(desc, BigDecimal.ZERO);
                                row.put(desc, value);
                        }

                        // Totais
                        row.put("totalProventos", totalProventos);
                        row.put("totalDescontos", totalDescontos);
                        row.put("liquido", totalProventos.subtract(totalDescontos));

                        result.add(row);
                }

                // Ordenar por nome
                result.sort((a, b) -> ((String) a.get("funcionario"))
                                .compareToIgnoreCase((String) b.get("funcionario")));

                return result;
        }

        /**
         * Retorna os headers dinâmicos para o relatório detalhado.
         */
        @Transactional(readOnly = true)
        public java.util.LinkedHashMap<String, String> getPayrollDetailedHeaders(UUID payrollRunId) {
                List<PayrollItem> items = payrollItemRepository.findByPayrollRunId(payrollRunId);

                java.util.LinkedHashMap<String, String> headers = new java.util.LinkedHashMap<>();
                headers.put("matricula", "Matrícula");
                headers.put("funcionario", "Funcionário");

                // Adicionar colunas para cada tipo de item
                java.util.Set<String> allDescriptions = items.stream()
                                .map(PayrollItem::getDescription)
                                .collect(java.util.stream.Collectors.toCollection(java.util.LinkedHashSet::new));

                for (String desc : allDescriptions) {
                        headers.put(desc, desc);
                }

                headers.put("totalProventos", "Total Proventos");
                headers.put("totalDescontos", "Total Descontos");
                headers.put("liquido", "Líquido (XOF)");

                return headers;
        }

        @Transactional(readOnly = true)
        public List<PayrollRunDTO> findAllPayrollRuns() {
                return payrollRunRepository.findAll().stream()
                                .map(this::toRunDTO)
                                .collect(Collectors.toList());
        }

        public PayrollPeriodDTO updatePeriodStatus(UUID id, String status) {
                PayrollPeriod period = payrollPeriodRepository.findById(id)
                                .orElseThrow(() -> new ResourceNotFoundException("Período não encontrado: " + id));

                // Validações de transição de status podem ser adicionadas aqui
                period.setStatus(status);
                PayrollPeriod saved = payrollPeriodRepository.save(period);
                return toPeriodDTO(saved);
        }

        private PayrollPeriodDTO toPeriodDTO(PayrollPeriod period) {
                return PayrollPeriodDTO.builder()
                                .id(period.getId())
                                .fiscalYear(period.getFiscalYear())
                                .month(period.getMonth())
                                .status(period.getStatus())
                                .startDate(period.getStartDate())
                                .endDate(period.getEndDate())
                                .build();
        }

        private PayrollRunDTO toRunDTO(PayrollRun run) {
                String ministryName = null;
                if (run.getMinistry() != null) {
                        ministryName = ministryRepository.findById(run.getMinistry())
                                        .map(br.gov.sigefp.org.domain.Ministry::getName).orElse("Desconhecido");
                }

                String orgUnitName = null;
                if (run.getOrgUnit() != null) {
                        orgUnitName = orgUnitRepository.findById(run.getOrgUnit())
                                        .map(br.gov.sigefp.org.domain.OrgUnit::getName).orElse("Desconhecido");
                }

                return PayrollRunDTO.builder()
                                .id(run.getId())
                                .periodId(run.getPeriod().getId())
                                .ministry(run.getMinistry())
                                .ministryName(ministryName)
                                .orgUnit(run.getOrgUnit())
                                .orgUnitName(orgUnitName)
                                .runType(run.getRunType())
                                .status(run.getStatus())
                                .createdAt(run.getCreatedAt())
                                .createdBy(run.getCreatedBy())
                                .build();
        }

        private PayrollItemDTO toItemDTO(PayrollItem item) {
                return toItemDTOWithAgentName(item, null);
        }

        private PayrollItemDTO toItemDTOWithAgentName(PayrollItem item, String agentName) {
                return PayrollItemDTO.builder()
                                .id(item.getId())
                                .agent(item.getAgent())
                                .agentName(agentName != null ? agentName : item.getAgent().toString())
                                .lineType(item.getLineType())
                                .earningTypeId(item.getEarningType() != null ? item.getEarningType().getId() : null)
                                .deductionTypeId(item.getDeductionType() != null ? item.getDeductionType().getId()
                                                : null)
                                .description(item.getDescription())
                                .quantity(item.getQuantity())
                                .unitAmount(item.getUnitAmount())
                                .totalAmount(item.getTotalAmount())
                                .budgetLine(item.getBudgetLine())
                                .build();
        }

        private BigDecimal calculateWorkingDaysFraction(Agent agent, LocalDate pStart, LocalDate pEnd) {
                LocalDate effectiveStart = pStart;
                LocalDate effectiveEnd = pEnd;

                // 1. Adjust for Hire Date
                if (agent.getHireDate() != null && agent.getHireDate().isAfter(pStart)) {
                        effectiveStart = agent.getHireDate();
                }
                // 2. Adjust for Posse Date (if later than Hire)
                if (agent.getPosseDate() != null && agent.getPosseDate().isAfter(effectiveStart)) {
                        effectiveStart = agent.getPosseDate();
                }
                // 3. Adjust for Termination
                if (agent.getTerminationDate() != null && agent.getTerminationDate().isBefore(pEnd)) {
                        effectiveEnd = agent.getTerminationDate();
                }

                // If dates are invalid (start > end), return 0
                if (effectiveStart.isAfter(effectiveEnd)) {
                        return BigDecimal.ZERO;
                }

                long totalDays = java.time.temporal.ChronoUnit.DAYS.between(pStart, pEnd) + 1;
                long effectiveDays = java.time.temporal.ChronoUnit.DAYS.between(effectiveStart, effectiveEnd) + 1;

                if (effectiveDays >= totalDays) {
                        return BigDecimal.ONE;
                }

                return new BigDecimal(effectiveDays).divide(new BigDecimal(totalDays), 4,
                                java.math.RoundingMode.HALF_UP);
        }
}
