package br.gov.sigefp.budget.service;

import br.gov.sigefp.budget.api.dto.BudgetExecutionDTO;
import br.gov.sigefp.budget.domain.BudgetExecution;
import br.gov.sigefp.budget.domain.BudgetLine;
import br.gov.sigefp.budget.repository.BudgetExecutionRepository;
import br.gov.sigefp.budget.repository.BudgetLineRepository;
import br.gov.sigefp.common.exception.BusinessException;
import br.gov.sigefp.common.exception.InsufficientBudgetException;
import br.gov.sigefp.common.exception.ResourceNotFoundException;
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 execuções orçamentárias.
 */
@Service
@RequiredArgsConstructor
@Transactional
public class BudgetExecutionService {

    private final BudgetExecutionRepository budgetExecutionRepository;
    private final BudgetLineRepository budgetLineRepository;
    private final br.gov.sigefp.budget.repository.BudgetAllocationRepository budgetAllocationRepository;

    public BudgetExecutionDTO registerExecution(BudgetExecutionDTO dto) {
        BudgetLine budgetLine = budgetLineRepository.findById(dto.getBudgetLineId())
                .orElseThrow(() -> new ResourceNotFoundException(
                        "Linha orçamentária não encontrada: " + dto.getBudgetLineId()));

        // Validação 1: não permitir registrar movimentos num fiscalYear fechado
        if ("CLOSED".equals(budgetLine.getFiscalYear().getStatus())) {
            throw new BusinessException("Não é possível registrar movimentos em um ano fiscal fechado",
                    "FISCAL_YEAR_CLOSED", org.springframework.http.HttpStatus.CONFLICT);
        }

        // Validação 2: Coerência Governamental - Verificar saldo disponível para
        // COMMITMENT
        if ("COMMITMENT".equals(dto.getMovementType())) {
            BigDecimal totalAllocated = budgetAllocationRepository
                    .calculateTotalAllocatedByBudgetLineId(budgetLine.getId());
            BigDecimal totalCommitted = budgetExecutionRepository
                    .calculateTotalCommittedByBudgetLineId(budgetLine.getId());
            BigDecimal available = totalAllocated.subtract(totalCommitted);

            if (dto.getAmount().compareTo(available) > 0) {
                throw new InsufficientBudgetException(String.format(
                        "Saldo insuficiente na Linha Orçamental %s. Disponível: %s, Solicitado: %s",
                        budgetLine.getCode(), available, dto.getAmount()));
            }
        }

        // Validação 3: Sequência obrigatória para LIQUIDATION (exige COMMITMENT)
        if ("LIQUIDATION".equals(dto.getMovementType())) {
            if (dto.getReferenceId() == null) {
                throw new BusinessException("ReferenceId é obrigatório para LIQUIDATION",
                        "MISSING_REFERENCE_ID", org.springframework.http.HttpStatus.BAD_REQUEST);
            }

            BigDecimal totalCommitted = budgetExecutionRepository
                    .calculateTotalCommittedByReferenceId(dto.getReferenceId());

            if (totalCommitted.compareTo(BigDecimal.ZERO) == 0) {
                throw new BusinessException("Não existe COMMITMENT correspondente para liquidar",
                        "NO_COMMITMENT", org.springframework.http.HttpStatus.CONFLICT);
            }

            BigDecimal totalLiquidated = budgetExecutionRepository
                    .calculateTotalLiquidatedByReferenceId(dto.getReferenceId());
            BigDecimal availableToLiquidate = totalCommitted.subtract(totalLiquidated);

            if (dto.getAmount().compareTo(availableToLiquidate) > 0) {
                throw new BusinessException(
                        String.format("Liquidação não pode exceder empenho. Disponível: %s, Solicitado: %s",
                                availableToLiquidate, dto.getAmount()),
                        "INSUFFICIENT_COMMITMENT", org.springframework.http.HttpStatus.CONFLICT);
            }
        }

        // Validação 4: Sequência obrigatória para PAYMENT (exige LIQUIDATION)
        if ("PAYMENT".equals(dto.getMovementType())) {
            if (dto.getReferenceId() == null) {
                throw new BusinessException("ReferenceId é obrigatório para PAYMENT",
                        "MISSING_REFERENCE_ID", org.springframework.http.HttpStatus.BAD_REQUEST);
            }

            BigDecimal totalLiquidated = budgetExecutionRepository
                    .calculateTotalLiquidatedByReferenceId(dto.getReferenceId());

            if (totalLiquidated.compareTo(BigDecimal.ZERO) == 0) {
                throw new BusinessException("Não existe LIQUIDATION correspondente para pagar",
                        "NO_LIQUIDATION", org.springframework.http.HttpStatus.CONFLICT);
            }

            BigDecimal totalPaid = budgetExecutionRepository
                    .calculateTotalPaidByReferenceId(dto.getReferenceId());
            BigDecimal availableToPay = totalLiquidated.subtract(totalPaid);

            if (dto.getAmount().compareTo(availableToPay) > 0) {
                throw new BusinessException(
                        String.format("Pagamento não pode exceder liquidação. Disponível: %s, Solicitado: %s",
                                availableToPay, dto.getAmount()),
                        "INSUFFICIENT_LIQUIDATION", org.springframework.http.HttpStatus.CONFLICT);
            }
        }

        BudgetExecution execution = BudgetExecution.builder()
                .budgetLine(budgetLine)
                .periodId(dto.getPeriodId())
                .movementType(dto.getMovementType())
                .amount(dto.getAmount())
                .sourceModule(dto.getSourceModule())
                .referenceId(dto.getReferenceId())
                .build();

        BudgetExecution saved = budgetExecutionRepository.save(execution);
        return toDTO(saved);
    }

    @Transactional(readOnly = true)
    public BigDecimal calculateTotalCommittedByBudgetLineId(UUID budgetLineId) {
        return budgetExecutionRepository.calculateTotalCommittedByBudgetLineId(budgetLineId);
    }

    @Transactional(readOnly = true)
    public BigDecimal calculateAvailableBalance(UUID budgetLineId) {
        // Este método será usado pelo BudgetLineService, mas pode ser chamado
        // diretamente
        // O cálculo é: totalAllocated - totalCommitted
        // Mas precisamos do BudgetAllocationRepository para isso
        // Por enquanto, retornamos apenas o comprometido
        return budgetExecutionRepository.calculateTotalCommittedByBudgetLineId(budgetLineId);
    }

    @Transactional(readOnly = true)
    public List<BudgetExecutionDTO> findByBudgetLineId(UUID budgetLineId) {
        return budgetExecutionRepository.findByBudgetLineId(budgetLineId).stream()
                .map(this::toDTO)
                .collect(Collectors.toList());
    }

    @Transactional(readOnly = true)
    public List<BudgetExecutionDTO> findByBudgetLineIdAndMovementType(UUID budgetLineId, String movementType) {
        return budgetExecutionRepository.findByBudgetLineIdAndMovementType(budgetLineId, movementType).stream()
                .map(this::toDTO)
                .collect(Collectors.toList());
    }

    @Transactional(readOnly = true)
    public List<BudgetExecutionDTO> findByPeriodId(Long periodId) {
        return budgetExecutionRepository.findByPeriodId(periodId).stream()
                .map(this::toDTO)
                .collect(Collectors.toList());
    }

    @Transactional(readOnly = true)
    public Page<BudgetExecutionDTO> findByFilters(UUID budgetLineId, Long periodId, String movementType,
            Pageable pageable) {

        org.springframework.data.jpa.domain.Specification<BudgetExecution> spec = (root, query, cb) -> {
            var predicates = new java.util.ArrayList<jakarta.persistence.criteria.Predicate>();

            if (budgetLineId != null) {
                predicates.add(cb.equal(root.get("budgetLine").get("id"), budgetLineId));
            }
            if (periodId != null) {
                predicates.add(cb.equal(root.get("periodId"), periodId));
            }
            if (movementType != null) {
                predicates.add(cb.equal(root.get("movementType"), movementType));
            }

            return cb.and(predicates.toArray(new jakarta.persistence.criteria.Predicate[0]));
        };

        return budgetExecutionRepository.findAll(spec, pageable)
                .map(this::toDTO);
    }

    @Transactional(readOnly = true)
    public List<BudgetExecutionDTO> findByFiltersList(UUID budgetLineId, Long periodId, String movementType) {
        return budgetExecutionRepository.findByFiltersList(budgetLineId, periodId, movementType).stream()
                .map(this::toDTO)
                .collect(Collectors.toList());
    }

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

    private BudgetExecutionDTO toDTO(BudgetExecution execution) {
        String typePt = translateMovementType(execution.getMovementType());
        String sourcePt = translateSourceModule(execution.getSourceModule());

        StringBuilder desc = new StringBuilder();
        desc.append(typePt);

        if (sourcePt != null && !sourcePt.isEmpty()) {
            desc.append(" - ").append(sourcePt);
        }

        if (execution.getPeriodId() != null) {
            String p = execution.getPeriodId().toString();
            if (p.length() == 6) {
                desc.append(" (Ref: ").append(p.substring(4, 6)).append("/").append(p.substring(0, 4)).append(")");
            } else {
                desc.append(" (Ref: ").append(p).append(")");
            }
        }

        return BudgetExecutionDTO.builder()
                .id(execution.getId())
                .budgetLineId(execution.getBudgetLine().getId())
                .budgetLineCode(
                        execution.getBudgetLine().getCode() + " - " + execution.getBudgetLine().getDescription())
                .periodId(execution.getPeriodId())
                .movementType(execution.getMovementType())
                .amount(execution.getAmount())
                .createdAt(execution.getCreatedAt())
                .sourceModule(execution.getSourceModule())
                .referenceId(execution.getReferenceId())
                .description(desc.toString())
                .build();
    }

    private String translateMovementType(String type) {
        if (type == null)
            return "";
        return switch (type) {
            case "COMMITMENT" -> "Empenho";
            case "LIQUIDATION" -> "Liquidação";
            case "PAYMENT" -> "Pagamento";
            default -> type;
        };
    }

    private String translateSourceModule(String module) {
        if (module == null)
            return "";
        return switch (module.toLowerCase()) {
            case "rh", "payroll" -> "Folha de Pagamento";
            case "treasury" -> "Tesouraria";
            case "budget" -> "Orçamento";
            default -> module;
        };
    }

    private String formatCurrency(BigDecimal amount) {
        return amount != null ? amount.toString() : "0.00";
    }
}
