package br.gov.sigefp.budget.service;

import br.gov.sigefp.budget.api.dto.BudgetLineDTO;
import br.gov.sigefp.budget.domain.BudgetLine;
import br.gov.sigefp.budget.domain.FiscalYear;
import br.gov.sigefp.budget.repository.BudgetAllocationRepository;
import br.gov.sigefp.budget.repository.BudgetExecutionRepository;
import br.gov.sigefp.budget.repository.BudgetLineRepository;
import br.gov.sigefp.budget.repository.FiscalYearRepository;
import br.gov.sigefp.org.repository.MinistryRepository;
import br.gov.sigefp.org.repository.OrgUnitRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * Serviço de aplicação para gestão de linhas orçamentárias.
 */
@Service
@RequiredArgsConstructor
@Transactional
public class BudgetLineService {

    private final BudgetLineRepository budgetLineRepository;
    private final FiscalYearRepository fiscalYearRepository;
    private final MinistryRepository ministryRepository;
    private final OrgUnitRepository orgUnitRepository;

    public BudgetLineDTO create(BudgetLineDTO dto) {
        // Validação: não permitir criar duas linhas com o mesmo code para o mesmo
        // fiscalYear
        if (budgetLineRepository.existsByFiscalYearIdAndCode(dto.getFiscalYearId(), dto.getCode())) {
            throw new IllegalArgumentException(
                    String.format("Linha orçamentária com código %s já existe para o ano fiscal informado",
                            dto.getCode()));
        }

        FiscalYear fiscalYear = fiscalYearRepository.findById(dto.getFiscalYearId())
                .orElseThrow(() -> new IllegalArgumentException("Ano fiscal não encontrado: " + dto.getFiscalYearId()));

        // Validações cruzadas
        if (dto.getMinistryId() != null && !ministryRepository.existsById(dto.getMinistryId())) {
            throw new IllegalArgumentException("Ministério não encontrado: " + dto.getMinistryId());
        }
        if (dto.getOrgUnitId() != null && !orgUnitRepository.existsById(dto.getOrgUnitId())) {
            throw new IllegalArgumentException("Unidade organizacional não encontrada: " + dto.getOrgUnitId());
        }
        // Validação: orgUnit deve pertencer ao mesmo ministry
        if (dto.getMinistryId() != null && dto.getOrgUnitId() != null) {
            orgUnitRepository.findById(dto.getOrgUnitId())
                    .ifPresent(orgUnit -> {
                        if (!dto.getMinistryId().equals(orgUnit.getMinistry())) {
                            throw new IllegalArgumentException(
                                    "Unidade organizacional deve pertencer ao mesmo ministério");
                        }
                    });
        }

        BudgetLine budgetLine = BudgetLine.builder()
                .fiscalYear(fiscalYear)
                .code(dto.getCode())
                .description(dto.getDescription())
                .ministry(dto.getMinistryId())
                .orgUnit(dto.getOrgUnitId())
                .economicClass(dto.getEconomicClass())
                .build();

        BudgetLine saved = budgetLineRepository.save(budgetLine);

        return toDTO(saved);
    }

    public BudgetLineDTO update(UUID id, BudgetLineDTO dto) {
        BudgetLine budgetLine = budgetLineRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Linha orçamentária não encontrada: " + id));

        if (dto.getCode() != null && !dto.getCode().equals(budgetLine.getCode())) {
            if (budgetLineRepository.existsByFiscalYearIdAndCode(budgetLine.getFiscalYear().getId(), dto.getCode())) {
                throw new IllegalArgumentException(
                        String.format("Linha orçamentária com código %s já existe para o ano fiscal", dto.getCode()));
            }
            budgetLine.setCode(dto.getCode());
        }

        if (dto.getDescription() != null) {
            budgetLine.setDescription(dto.getDescription());
        }
        if (dto.getMinistryId() != null) {
            if (!ministryRepository.existsById(dto.getMinistryId())) {
                throw new IllegalArgumentException("Ministério não encontrado: " + dto.getMinistryId());
            }
            budgetLine.setMinistry(dto.getMinistryId());
        }
        if (dto.getOrgUnitId() != null) {
            if (!orgUnitRepository.existsById(dto.getOrgUnitId())) {
                throw new IllegalArgumentException("Unidade organizacional não encontrada: " + dto.getOrgUnitId());
            }
            // Validação: orgUnit deve pertencer ao mesmo ministry
            orgUnitRepository.findById(dto.getOrgUnitId())
                    .ifPresent(orgUnit -> {
                        UUID currentMinistryId = budgetLine.getMinistry();
                        if (currentMinistryId != null && !currentMinistryId.equals(orgUnit.getMinistry())) {
                            throw new IllegalArgumentException(
                                    "Unidade organizacional deve pertencer ao mesmo ministério");
                        }
                    });
            budgetLine.setOrgUnit(dto.getOrgUnitId());
        }
        if (dto.getEconomicClass() != null) {
            budgetLine.setEconomicClass(dto.getEconomicClass());
        }

        BudgetLine saved = budgetLineRepository.save(budgetLine);
        return toDTO(saved);
    }

    @Transactional(readOnly = true)
    public BudgetLineDTO findById(UUID id) {
        BudgetLine budgetLine = budgetLineRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Linha orçamentária não encontrada: " + id));
        return toDTO(budgetLine);
    }

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

    @Transactional(readOnly = true)
    public List<BudgetLineDTO> findByFiscalYearId(UUID fiscalYearId) {
        return budgetLineRepository.findByFiscalYearId(fiscalYearId).stream()
                .map(this::toDTO)
                .collect(Collectors.toList());
    }

    @Transactional(readOnly = true)
    public Page<BudgetLineDTO> findWithFilters(UUID fiscalYearId, UUID ministryId, UUID orgUnitId, String search,
            Pageable pageable) {
        return budgetLineRepository.findAll((root, query, cb) -> {
            var predicates = new java.util.ArrayList<jakarta.persistence.criteria.Predicate>();

            if (fiscalYearId != null) {
                predicates.add(cb.equal(root.get("fiscalYear").get("id"), fiscalYearId));
            }

            if (ministryId != null) {
                predicates.add(cb.equal(root.get("ministry"), ministryId));
            }

            if (orgUnitId != null) {
                predicates.add(cb.equal(root.get("orgUnit"), orgUnitId));
            }

            if (search != null && !search.trim().isEmpty()) {
                String likePattern = "%" + search.trim().toLowerCase() + "%";
                predicates.add(cb.or(
                        cb.like(cb.lower(root.get("code")), likePattern),
                        cb.like(cb.lower(root.get("description")), likePattern),
                        cb.like(cb.lower(root.get("economicClass")), likePattern)));
            }

            return cb.and(predicates.toArray(new jakarta.persistence.criteria.Predicate[0]));
        }, pageable).map(this::toDTO);
    }

    private BudgetLineDTO toDTO(BudgetLine budgetLine) {
        // Valores calculados via @Formula no carregamento da entidade (Performance N+2
        // fix)
        BigDecimal totalAllocated = budgetLine.getTotalAllocated() != null ? budgetLine.getTotalAllocated()
                : BigDecimal.ZERO;
        BigDecimal totalCommitted = budgetLine.getTotalCommitted() != null ? budgetLine.getTotalCommitted()
                : BigDecimal.ZERO;
        BigDecimal availableBalance = totalAllocated.subtract(totalCommitted);

        return BudgetLineDTO.builder()
                .id(budgetLine.getId())
                .fiscalYearId(budgetLine.getFiscalYear().getId())
                .code(budgetLine.getCode())
                .description(budgetLine.getDescription())
                .ministryId(budgetLine.getMinistry())
                .orgUnitId(budgetLine.getOrgUnit())
                .economicClass(budgetLine.getEconomicClass())
                .totalAllocated(totalAllocated)
                .totalCommitted(totalCommitted)
                .availableBalance(availableBalance)
                .build();
    }
}
