package br.gov.sigefp.rh.service;

import br.gov.sigefp.org.repository.OrgUnitRepository;
import br.gov.sigefp.org.repository.PositionRepository;
import br.gov.sigefp.rh.api.dto.*;
import br.gov.sigefp.rh.domain.*;
import br.gov.sigefp.rh.repository.*;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * Serviço de aplicação para gestão de agentes.
 */
@Service
@RequiredArgsConstructor
@Transactional
@lombok.extern.slf4j.Slf4j
public class AgentService {

    private final AgentRepository agentRepository;
    private final OrgUnitRepository orgUnitRepository;
    private final PositionRepository positionRepository;
    private final AgentStatusHistoryRepository statusHistoryRepository;
    private final CareerEventRepository careerEventRepository;
    private final PerformanceEvaluationRepository performanceEvaluationRepository;
    private final CareerEventService careerEventService;
    private final AgentContractRepository agentContractRepository;
    private final AgentBankAccountRepository agentBankAccountRepository;

    @org.springframework.security.access.prepost.PreAuthorize("hasRole('HR_ADMIN')")
    public AgentDTO create(AgentDTO dto) {
        if (agentRepository.existsByMatricula(dto.getMatricula())) {
            throw new IllegalArgumentException("Matrícula já existe: " + dto.getMatricula());
        }
        if (agentRepository.existsByNif(dto.getNif())) {
            throw new IllegalArgumentException("NIF já existe: " + dto.getNif());
        }
        if (agentRepository.existsByBiNumber(dto.getBiNumber())) {
            throw new IllegalArgumentException("Número de BI já existe: " + dto.getBiNumber());
        }

        // Validações cruzadas
        if (dto.getOrgUnit() != null && !orgUnitRepository.existsById(dto.getOrgUnit())) {
            throw new IllegalArgumentException("Unidade organizacional não encontrada: " + dto.getOrgUnit());
        }
        if (dto.getPosition() != null && !positionRepository.existsById(dto.getPosition())) {
            throw new IllegalArgumentException("Posição não encontrada: " + dto.getPosition());
        }

        // P17: Validar Habilitação Literária
        validateLiteraryQualification(dto.getSalaryCategory(), dto.getLiteraryQualification());

        Agent agent = Agent.builder()
                .matricula(dto.getMatricula())
                .nif(dto.getNif())
                .biNumber(dto.getBiNumber())
                .fullName(dto.getFullName())
                .birthDate(dto.getBirthDate())
                .hireDate(dto.getHireDate())
                .posseDate(dto.getPosseDate())
                .terminationDate(dto.getTerminationDate())
                .appointmentType(dto.getAppointmentType() != null ? dto.getAppointmentType() : "PROVISORIA")
                .functionalSituation(
                        dto.getFunctionalSituation() != null ? dto.getFunctionalSituation() : "ATIVIDADE_NO_QUADRO")
                .status(dto.getStatus() != null ? dto.getStatus() : "REGISTERED")
                .eligibleDependentsCount(dto.getEligibleDependentsCount())
                .literaryQualification(dto.getLiteraryQualification())
                .salaryCategory(dto.getSalaryCategory())
                .salaryGrade(dto.getSalaryGrade())
                .salaryStep(dto.getSalaryStep())
                .orgUnit(dto.getOrgUnit())
                .position(dto.getPosition())
                .nationality(dto.getNationality())
                .phone(dto.getPhone())
                .email(dto.getEmail())
                .address(dto.getAddress())
                .build();

        Agent saved = agentRepository.save(agent);
        careerEventService.recordSimpleEvent(saved, CareerEventType.ADMISSAO, "Admissão inicial no sistema", null,
                saved.getHireDate());
        return toDTO(saved);
    }

    @org.springframework.security.access.prepost.PreAuthorize("hasRole('HR_ADMIN')")
    public AgentDTO update(UUID id, AgentDTO dto) {
        // P17: Validar Habilitação Literária (em update também)
        validateLiteraryQualification(dto.getSalaryCategory(), dto.getLiteraryQualification());

        Agent agent = agentRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Agente não encontrado: " + id));

        // Capture previous state for history
        String previousStatus = agent.getStatus();
        String previousSituation = agent.getFunctionalSituation();
        UUID previousCategory = agent.getSalaryCategory();
        UUID previousGrade = agent.getSalaryGrade();
        UUID previousStep = agent.getSalaryStep();
        UUID previousOrgUnit = agent.getOrgUnit();
        UUID previousPosition = agent.getPosition();

        StringBuilder changeLog = new StringBuilder();

        // Detect Changes for ChangeLog
        detectAndApplyChanges(agent, dto, changeLog);

        boolean statusChanged = dto.getStatus() != null && !dto.getStatus().equals(previousStatus);
        boolean situationChanged = dto.getFunctionalSituation() != null
                && !dto.getFunctionalSituation().equals(previousSituation);
        boolean salaryChanged = isSalaryChanged(agent, dto);
        boolean orgChanged = isOrgChanged(agent, dto);

        CareerEventType careerEventType = null;
        boolean careerImpact = false;

        if (statusChanged || situationChanged || salaryChanged || orgChanged) {
            careerImpact = true;
            careerEventType = determineEventType(dto, agent, situationChanged, salaryChanged, orgChanged);

            if (careerEventType == CareerEventType.PROMOCAO) {
                validatePromotion(agent);
            }
        }

        // Apply changes to entity
        applyUpdates(agent, dto);

        // Record History
        if (changeLog.length() > 0 || careerImpact) {
            AgentStatusHistory history = AgentStatusHistory.builder()
                    .agent(agent)
                    .previousStatus(previousStatus)
                    .newStatus(agent.getStatus())
                    .previousFunctionalSituation(previousSituation)
                    .newFunctionalSituation(agent.getFunctionalSituation())
                    .eventType(careerImpact && careerEventType != null ? careerEventType.name() : "EDIT")
                    .reason(dto.getStatusChangeReason())
                    .changeLog(changeLog.length() > 0 ? changeLog.toString() : "Atualização cadastral.")
                    .changedAt(LocalDateTime.now())
                    .changedBy("system")
                    .build();
            statusHistoryRepository.save(history);

            if (careerImpact && careerEventType != null) {
                careerEventService.recordEvent(agent, careerEventType, dto.getStatusChangeReason(),
                        dto.getEventDocumentRef(),
                        dto.getEventEffectiveDate(), dto.getEventPublicationDate(),
                        dto.getSalaryCategory(), dto.getSalaryGrade(), dto.getSalaryStep(),
                        dto.getOrgUnit(), dto.getPosition(),
                        previousCategory, previousGrade, previousStep,
                        previousOrgUnit, previousPosition);
            }
        }

        Agent saved = agentRepository.save(agent);
        return toDTO(saved);
    }

    @org.springframework.security.access.prepost.PreAuthorize("hasRole('HR_ADMIN')")
    public void delete(UUID id) {
        Agent agent = agentRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Agente não encontrado: " + id));

        log.info("Excluindo agente: {} (ID: {})", agent.getFullName(), agent.getId());

        // Excluir dados relacionados primeiro (para evitar problemas de FK)
        // Os contratos, contas bancárias e histórico são deletados em cascata via
        // orphanRemoval
        // Mas eventos de carreira e histórico de status precisam ser excluídos
        // explicitamente

        // Excluir eventos de carreira
        careerEventRepository.deleteByAgentId(id);

        // Excluir histórico de status
        statusHistoryRepository.deleteByAgentId(id);

        // Por fim, excluir o agente (contratos e contas bancárias são excluídos em
        // cascata)
        agentRepository.delete(agent);

        log.info("Agente excluído com sucesso: {}", id);
    }

    private void detectAndApplyChanges(Agent agent, AgentDTO dto, StringBuilder log) {
        compareAndLog(log, "Nome", agent.getFullName(), dto.getFullName());
        compareAndLog(log, "NIF", agent.getNif(), dto.getNif());
        compareAndLog(log, "BI", agent.getBiNumber(), dto.getBiNumber());
        compareAndLog(log, "Email", agent.getEmail(), dto.getEmail());
        compareAndLog(log, "Telefone", agent.getPhone(), dto.getPhone());
        compareAndLog(log, "Endereço", agent.getAddress(), dto.getAddress());
        compareAndLog(log, "Situação Funcional", agent.getFunctionalSituation(), dto.getFunctionalSituation());
        compareAndLog(log, "Status", agent.getStatus(), dto.getStatus());
        compareAndLog(log, "Nº Dependentes", agent.getEligibleDependentsCount(), dto.getEligibleDependentsCount());
        compareAndLog(log, "Habilitação Literária", agent.getLiteraryQualification(), dto.getLiteraryQualification());

        if (dto.getOrgUnit() != null && !Objects.equals(dto.getOrgUnit(), agent.getOrgUnit())) {
            log.append("Unidade Orgânica: [").append(agent.getOrgUnit()).append("] -> [").append(dto.getOrgUnit())
                    .append("]. ");
        }
        if (dto.getPosition() != null && !Objects.equals(dto.getPosition(), agent.getPosition())) {
            log.append("Cargo/Posição: [").append(agent.getPosition()).append("] -> [").append(dto.getPosition())
                    .append("]. ");
        }
        if (isSalaryChanged(agent, dto)) {
            log.append("Estrutura salarial/carreira alterada. ");
        }
    }

    private void compareAndLog(StringBuilder log, String fieldName, Object oldValue, Object newValue) {
        if (newValue != null && !Objects.equals(oldValue, newValue)) {
            String oldStr = oldValue != null ? oldValue.toString() : "N/A";
            String newStr = newValue.toString();
            log.append(fieldName).append(": [").append(oldStr).append("] -> [").append(newStr).append("]. ");
        }
    }

    private void applyUpdates(Agent agent, AgentDTO dto) {
        if (dto.getMatricula() != null)
            agent.setMatricula(dto.getMatricula());
        if (dto.getNif() != null)
            agent.setNif(dto.getNif());
        if (dto.getBiNumber() != null)
            agent.setBiNumber(dto.getBiNumber());
        if (dto.getFullName() != null)
            agent.setFullName(dto.getFullName());
        if (dto.getBirthDate() != null)
            agent.setBirthDate(dto.getBirthDate());
        if (dto.getHireDate() != null)
            agent.setHireDate(dto.getHireDate());
        if (dto.getPosseDate() != null)
            agent.setPosseDate(dto.getPosseDate());
        if (dto.getTerminationDate() != null)
            agent.setTerminationDate(dto.getTerminationDate());
        if (dto.getAppointmentType() != null)
            agent.setAppointmentType(dto.getAppointmentType());
        if (dto.getFunctionalSituation() != null)
            agent.setFunctionalSituation(dto.getFunctionalSituation());
        if (dto.getEligibleDependentsCount() != null)
            agent.setEligibleDependentsCount(dto.getEligibleDependentsCount());
        if (dto.getStatus() != null)
            agent.setStatus(dto.getStatus());
        if (dto.getOrgUnit() != null)
            agent.setOrgUnit(dto.getOrgUnit());
        if (dto.getPosition() != null)
            agent.setPosition(dto.getPosition());
        if (dto.getNationality() != null)
            agent.setNationality(dto.getNationality());
        if (dto.getPhone() != null)
            agent.setPhone(dto.getPhone());
        if (dto.getEmail() != null)
            agent.setEmail(dto.getEmail());
        if (dto.getAddress() != null)
            agent.setAddress(dto.getAddress());
        if (dto.getLiteraryQualification() != null)
            agent.setLiteraryQualification(dto.getLiteraryQualification());
        if (dto.getSalaryCategory() != null)
            agent.setSalaryCategory(dto.getSalaryCategory());
        if (dto.getSalaryGrade() != null)
            agent.setSalaryGrade(dto.getSalaryGrade());
        if (dto.getSalaryStep() != null)
            agent.setSalaryStep(dto.getSalaryStep());
    }

    private boolean isSalaryChanged(Agent agent, AgentDTO dto) {
        return (dto.getSalaryCategory() != null && !Objects.equals(dto.getSalaryCategory(), agent.getSalaryCategory()))
                || (dto.getSalaryGrade() != null && !Objects.equals(dto.getSalaryGrade(), agent.getSalaryGrade()))
                || (dto.getSalaryStep() != null && !Objects.equals(dto.getSalaryStep(), agent.getSalaryStep()));
    }

    private boolean isOrgChanged(Agent agent, AgentDTO dto) {
        return (dto.getOrgUnit() != null && !Objects.equals(dto.getOrgUnit(), agent.getOrgUnit()))
                || (dto.getPosition() != null && !Objects.equals(dto.getPosition(), agent.getPosition()));
    }

    private CareerEventType determineEventType(AgentDTO dto, Agent agent, boolean situationChanged,
            boolean salaryChanged, boolean orgChanged) {
        if (salaryChanged) {
            if (dto.getSalaryCategory() != null
                    && !Objects.equals(dto.getSalaryCategory(), agent.getSalaryCategory())) {
                return CareerEventType.PROMOCAO;
            }
            return CareerEventType.PROGRESSAO;
        }

        if (situationChanged) {
            return switch (dto.getFunctionalSituation()) {
                case "SUBSTITUICAO" -> CareerEventType.SUBSTITUICAO;
                case "TRANSFERENCIA" -> CareerEventType.TRANSFERENCIA;
                case "COMISSAO_SERVICO" -> CareerEventType.COMISSAO_SERVICO;
                case "RECLASSIFICACAO" -> CareerEventType.RECLASSIFICACAO;
                case "DEFINITIVA" -> CareerEventType.NOMEACAO_DEFINITIVA;
                default -> CareerEventType.TRANSFERENCIA; // Fallback para outros movimentos
            };
        }

        if (orgChanged)
            return CareerEventType.TRANSFERENCIA;

        return CareerEventType.PROGRESSAO; // Default
    }

    @Transactional(readOnly = true)
    public AgentDTO findById(UUID id) {
        Agent agent = agentRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Agente não encontrado: " + id));
        return toDTO(agent);
    }

    @Transactional(readOnly = true)
    public Page<AgentDTO> findAll(Pageable pageable) {
        return agentRepository.findAll(pageable).map(this::toDTO);
    }

    @Transactional(readOnly = true)
    public Page<AgentDTO> search(String query, Pageable pageable) {
        if (query == null || query.isBlank()) {
            return findAll(pageable);
        }
        return agentRepository.findByFullNameContainingIgnoreCaseOrMatriculaContainingIgnoreCase(
                query, query, pageable).map(this::toDTO);
    }

    @Transactional(readOnly = true)
    public Page<AgentDTO> findAllWithFilters(
            String query,
            String status,
            UUID ministryId,
            UUID orgUnitId,
            UUID positionId,
            String functionalSituation,
            String appointmentType,
            Pageable pageable) {

        Specification<Agent> spec = buildSpecification(query, status, ministryId, orgUnitId, positionId,
                functionalSituation, appointmentType);
        Page<Agent> agentsPage = agentRepository.findAll(spec, pageable);

        // Otimização N+1: Carregar todos os contratos ativos e contas bancárias
        // principais em lote para os agentes da página
        List<UUID> agentIds = agentsPage.getContent().stream().map(Agent::getId).collect(Collectors.toList());

        if (!agentIds.isEmpty()) {
            // Isso aciona o carregamento das coleções para a sessão atual (Hibernate Batch
            // Fetching deve estar configurado no yml ou na entidade)
            // Como as relações estão mapeadas com @OneToMany, o Hibernate pode usar
            // batch-size se configurado.
            // Alternativamente, podemos fazer uma query explícita para "aquecer" o cache de
            // primeiro nível.
            agentContractRepository.findAllByAgentIdInAndIsActiveTrue(agentIds);
            agentBankAccountRepository.findAllByAgentIdInAndIsPrimaryTrue(agentIds);
        }

        return agentsPage.map(this::toDTO);
    }

    @Transactional(readOnly = true)
    public br.gov.sigefp.rh.api.dto.AgentStatsDTO getStats(
            String query,
            String status,
            UUID ministryId,
            UUID orgUnitId,
            UUID positionId,
            String functionalSituation,
            String appointmentType) {

        log.info("Calculando estatísticas otimizadas com filtros - Ministério: {}, OrgUnit: {}, Query: {}", ministryId,
                orgUnitId,
                query);

        Specification<Agent> baseSpec = buildSpecification(query, null, ministryId, orgUnitId, positionId,
                functionalSituation, appointmentType);

        // Usar a nova consulta agregada para obter todos os status de uma vez
        // Nota: Infelizmente a Specification não é trivial de passar para uma Query
        // customizada com GROUP BY
        // sem usar CriteriaBuilder complexo. Para manter a performance, vamos usar o
        // repositório para o total
        // e otimizar as consultas individuais se a especificação for complexa, ou usar
        // uma abordagem simplificada
        // se possível.

        // Por enquanto, vamos reduzir as chamadas mantendo a lógica de Specification
        // mas consolidando em uma abordagem mais eficiente
        // se buildSpecification for nulo (ou seja, sem filtros extras).

        long total = agentRepository.count(baseSpec);

        br.gov.sigefp.rh.api.dto.AgentStatsDTO stats = br.gov.sigefp.rh.api.dto.AgentStatsDTO.builder()
                .total(total)
                .build();

        if (total > 0) {
            // Executar consultas de contagem por status usando a mesma especificação
            stats.setActive(agentRepository
                    .count(baseSpec.and(br.gov.sigefp.rh.specification.AgentSpecifications.hasStatus("ACTIVE"))));
            stats.setInactive(agentRepository
                    .count(baseSpec.and(br.gov.sigefp.rh.specification.AgentSpecifications.hasStatus("INACTIVE"))));
            stats.setSuspended(agentRepository
                    .count(baseSpec.and(br.gov.sigefp.rh.specification.AgentSpecifications.hasStatus("SUSPENDED"))));
            stats.setTerminated(agentRepository
                    .count(baseSpec.and(br.gov.sigefp.rh.specification.AgentSpecifications.hasStatus("TERMINATED"))));
        }

        return stats;
    }

    private Specification<Agent> buildSpecification(
            String query,
            String status,
            UUID ministryId,
            UUID orgUnitId,
            UUID positionId,
            String functionalSituation,
            String appointmentType) {

        Specification<Agent> spec = Specification.where(null);

        if (query != null && !query.isBlank()) {
            spec = spec.and(br.gov.sigefp.rh.specification.AgentSpecifications.searchByQuery(query));
        }
        if (status != null && !status.isBlank()) {
            spec = spec.and(br.gov.sigefp.rh.specification.AgentSpecifications.hasStatus(status));
        }

        // Se um ministério for selecionado, filtramos por todas as unidades
        // subordinadas a ele
        if (ministryId != null) {
            List<UUID> ministryOrgUnits = orgUnitRepository.findByMinistryId(ministryId)
                    .stream().map(br.gov.sigefp.org.domain.OrgUnit::getId).collect(Collectors.toList());

            // Garantir que a unidade do próprio ministério esteja na lista
            if (!ministryOrgUnits.contains(ministryId)) {
                ministryOrgUnits.add(ministryId);
            }

            if (!ministryOrgUnits.isEmpty()) {
                spec = spec.and(br.gov.sigefp.rh.specification.AgentSpecifications.hasOrgUnitIn(ministryOrgUnits));
            } else {
                spec = spec.and((root, q, cb) -> cb.disjunction());
            }
        }

        if (orgUnitId != null) {
            spec = spec.and(br.gov.sigefp.rh.specification.AgentSpecifications.hasOrgUnit(orgUnitId));
        }
        if (positionId != null) {
            spec = spec.and(br.gov.sigefp.rh.specification.AgentSpecifications.hasPosition(positionId));
        }
        if (functionalSituation != null && !functionalSituation.isBlank()) {
            spec = spec.and(
                    br.gov.sigefp.rh.specification.AgentSpecifications.hasFunctionalSituation(functionalSituation));
        }
        if (appointmentType != null && !appointmentType.isBlank()) {
            spec = spec.and(br.gov.sigefp.rh.specification.AgentSpecifications.hasAppointmentType(appointmentType));
        }

        return spec;
    }

    private void validatePromotion(Agent agent) {
        // Regra do Decreto 12-A/94: Avaliação de desempenho de, no mínimo, "Bom" nos
        // últimos três anos.
        int currentYear = LocalDate.now().getYear();
        List<PerformanceEvaluation> evals = performanceEvaluationRepository
                .findByAgentIdAndReferenceYearBetweenOrderByReferenceYearDesc(agent.getId(), currentYear - 3,
                        currentYear - 1);

        if (evals.size() < 3) {
            throw new IllegalStateException(
                    "O agente deve ter pelo menos 3 anos de avaliações de desempenho para ser promovido.");
        }

        for (PerformanceEvaluation eval : evals) {
            if (eval.getScore() < 14) { // 14 é o mínimo para "Bom" na escala 0-20
                throw new IllegalStateException(
                        "O agente não cumpre o requisito de avaliação 'Bom' (mínimo 14 pontos) no ano "
                                + eval.getReferenceYear());
            }
        }
    }

    public List<CareerTimelineDTO> getTimeline(UUID agentId) {
        // Ordenar por data de eficácia ascendente (mais antigo primeiro) para timeline
        // cronológica
        return careerEventRepository.findByAgentIdOrderByEffectiveDateAsc(agentId).stream()
                .map(e -> CareerTimelineDTO.builder().date(e.getEffectiveDate()).eventType(e.getEventType().name())
                        .eventTypeName(translateEventType(e.getEventType()))
                        .reason(e.getReason())
                        .documentRef(e.getDocumentRef())
                        .totalBaseAmount(e.getTotalBaseAmount())
                        .cargoAmount(e.getCargoAmount())
                        .exercicioAmount(e.getExercicioAmount())
                        .changeSummary(buildChangeSummary(e))
                        .build())
                .collect(Collectors.toList());
    }

    @Transactional(readOnly = true)
    public List<AgentStatusHistoryDTO> getStatusHistory(UUID agentId) {
        return statusHistoryRepository.findByAgentIdOrderByChangedAtDesc(agentId).stream()
                .map(h -> AgentStatusHistoryDTO.builder()
                        .id(h.getId())
                        .previousStatus(h.getPreviousStatus())
                        .newStatus(h.getNewStatus())
                        .previousFunctionalSituation(h.getPreviousFunctionalSituation())
                        .newFunctionalSituation(h.getNewFunctionalSituation())
                        .eventType(h.getEventType())
                        .reason(h.getReason())
                        .changeLog(h.getChangeLog())
                        .changedAt(h.getChangedAt())
                        .changedBy(h.getChangedBy())
                        .build())
                .collect(Collectors.toList());
    }

    private String translateEventType(CareerEventType type) {
        return switch (type) {
            case ADMISSAO -> "Admissão";
            case NOMEACAO_PROVISORIA -> "Nomeação Provisória";
            case NOMEACAO_DEFINITIVA -> "Nomeação Definitiva";
            case PROMOCAO -> "Promoção";
            case PROGRESSAO -> "Progressão";
            case SUBSTITUICAO -> "Substituição";
            case TRANSFERENCIA -> "Transferência";
            case RECLASSIFICACAO -> "Reclassificação";
            case COMISSAO_SERVICO -> "Comissão de Serviço";
            case TERMINATION -> "Cessação de Funções";
            default -> type.name();
        };
    }

    private String buildChangeSummary(CareerEvent e) {
        StringBuilder sb = new StringBuilder();

        // Verificar mudanças na estrutura salarial
        if (e.getNewCategory() != null && !e.getNewCategory().equals(e.getPreviousCategory())) {
            sb.append("Mudança de Categoria. ");
        }
        if (e.getNewGrade() != null && !e.getNewGrade().equals(e.getPreviousGrade())) {
            sb.append("Mudança de Grau/Escalão. ");
        }
        if (e.getNewStep() != null && !e.getNewStep().equals(e.getPreviousStep())) {
            sb.append("Mudança de Nível/Step. ");
        }

        // Verificar mudanças organizacionais
        if (e.getNewOrgUnit() != null && !e.getNewOrgUnit().equals(e.getPreviousOrgUnit())) {
            sb.append("Transferência de Unidade Orgânica. ");
        }
        if (e.getNewPosition() != null && !e.getNewPosition().equals(e.getPreviousPosition())) {
            sb.append("Mudança de Cargo/Posição. ");
        }

        // Se não houver mudanças específicas, retornar mensagem genérica baseada no
        // tipo de evento
        if (sb.length() == 0) {
            sb.append("Evento registrado conforme documentação legal.");
        }

        return sb.toString().trim();
    }

    private AgentDTO toDTO(Agent agent) {
        // Otimização: Os contratos e contas bancárias já devem estar no cache de
        // primeiro nível
        // devido à busca em lote feita no findAllWithFilters.

        // Buscar contrato ativo de forma eficiente
        AgentContractDTO activeContractDTO = agent.getContracts().stream()
                .filter(c -> Boolean.TRUE.equals(c.getIsActive()))
                .findFirst()
                .map(this::contractToDTO)
                .orElse(null);

        // Buscar conta bancária principal de forma eficiente
        AgentBankAccountDTO primaryBankAccountDTO = agent.getBankAccounts().stream()
                .filter(acc -> Boolean.TRUE.equals(acc.getIsPrimary()))
                .findFirst()
                .map(this::bankAccountToDTO)
                .orElse(null);

        // Preencher orgUnit e position do contrato ativo se existir, caso contrário
        // usar do agente
        UUID orgUnit = activeContractDTO != null && activeContractDTO.getOrgUnit() != null
                ? activeContractDTO.getOrgUnit()
                : agent.getOrgUnit();
        UUID position = activeContractDTO != null && activeContractDTO.getPosition() != null
                ? activeContractDTO.getPosition()
                : agent.getPosition();

        // Preencher salaryCategory, salaryGrade e salaryStep do contrato ativo se
        // existir
        UUID salaryCategory = activeContractDTO != null && activeContractDTO.getSalaryCategory() != null
                ? activeContractDTO.getSalaryCategory()
                : agent.getSalaryCategory();
        UUID salaryGrade = activeContractDTO != null && activeContractDTO.getSalaryGrade() != null
                ? activeContractDTO.getSalaryGrade()
                : agent.getSalaryGrade();
        UUID salaryStep = activeContractDTO != null && activeContractDTO.getSalaryStep() != null
                ? activeContractDTO.getSalaryStep()
                : agent.getSalaryStep();

        return AgentDTO.builder()
                .id(agent.getId())
                .matricula(agent.getMatricula())
                .nif(agent.getNif())
                .biNumber(agent.getBiNumber())
                .fullName(agent.getFullName())
                .birthDate(agent.getBirthDate())
                .hireDate(agent.getHireDate())
                .posseDate(agent.getPosseDate())
                .terminationDate(agent.getTerminationDate())
                .appointmentType(agent.getAppointmentType())
                .functionalSituation(agent.getFunctionalSituation())
                .status(agent.getStatus())
                .eligibleDependentsCount(agent.getEligibleDependentsCount())
                .literaryQualification(agent.getLiteraryQualification())
                .salaryCategory(salaryCategory)
                .salaryGrade(salaryGrade)
                .salaryStep(salaryStep)
                .orgUnit(orgUnit)
                .position(position)
                .nationality(agent.getNationality())
                .phone(agent.getPhone())
                .email(agent.getEmail())
                .address(agent.getAddress())
                .activeContract(activeContractDTO)
                .primaryBankAccount(primaryBankAccountDTO)
                .build();
    }

    private AgentContractDTO contractToDTO(AgentContract contract) {
        return AgentContractDTO.builder()
                .id(contract.getId())
                .contractType(contract.getContractType())
                .startDate(contract.getStartDate())
                .endDate(contract.getEndDate())
                .weeklyHours(contract.getWeeklyHours())
                .baseSalaryRef(contract.getBaseSalaryRef())
                .orgUnit(contract.getOrgUnit())
                .position(contract.getPosition())
                .salaryCategory(contract.getSalaryCategory())
                .salaryGrade(contract.getSalaryGrade())
                .salaryStep(contract.getSalaryStep())
                .legalActReference(contract.getLegalActReference())
                .isActive(contract.getIsActive())
                .build();
    }

    private AgentBankAccountDTO bankAccountToDTO(AgentBankAccount account) {
        return AgentBankAccountDTO.builder()
                .id(account.getId())
                .bank(account.getBank())
                .branchCode(account.getBranchCode())
                .accountNumber(account.getAccountNumber())
                .isPrimary(account.getIsPrimary())
                .build();
    }

    private void validateLiteraryQualification(UUID salaryCategory, String literaryQualification) {
        if (salaryCategory != null && (literaryQualification == null || literaryQualification.trim().isEmpty())) {
            throw new IllegalArgumentException(
                    "Habilitação Literária é obrigatória quando há Categoria Salarial definida.");
        }
    }
}
