package br.gov.sigefp.treasury.service;

import br.gov.sigefp.common.exception.BusinessException;
import br.gov.sigefp.common.exception.ResourceNotFoundException;
import br.gov.sigefp.treasury.api.dto.CashAccountDTO;
import br.gov.sigefp.treasury.api.dto.CreateCashAccountDTO;
import br.gov.sigefp.treasury.domain.CashAccount;
import br.gov.sigefp.treasury.repository.CashAccountRepository;
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.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * Serviço de aplicação para gestão de contas de caixa e bancárias.
 */
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class CashAccountService {

    private final CashAccountRepository cashAccountRepository;

    public CashAccountDTO create(CreateCashAccountDTO dto) {
        // Validar código único
        if (cashAccountRepository.findByCode(dto.getCode()).isPresent()) {
            throw new BusinessException(
                    "Já existe uma conta com o código: " + dto.getCode(),
                    "DUPLICATE_CODE",
                    org.springframework.http.HttpStatus.CONFLICT);
        }

        // Validar se bankId é obrigatório para BANK_ACCOUNT
        if ("BANK_ACCOUNT".equals(dto.getType()) && dto.getBankId() == null) {
            throw new BusinessException(
                    "BankId é obrigatório para contas bancárias",
                    "MISSING_BANK_ID",
                    org.springframework.http.HttpStatus.BAD_REQUEST);
        }

        CashAccount cashAccount = CashAccount.builder()
                .code(dto.getCode())
                .name(dto.getName())
                .type(dto.getType())
                .orgUnitId(dto.getOrgUnitId())
                .bankId(dto.getBankId())
                .accountNumber(dto.getAccountNumber())
                .branchCode(dto.getBranchCode())
                .currency(dto.getCurrency() != null ? dto.getCurrency() : "XOF")
                .isActive(dto.getIsActive() != null ? dto.getIsActive() : true)
                .currentBalance(BigDecimal.ZERO)
                .availableBalance(BigDecimal.ZERO)
                .build();

        CashAccount saved = cashAccountRepository.save(cashAccount);
        log.info("Conta de caixa criada: id={}, code={}, name={}", saved.getId(), saved.getCode(), saved.getName());

        return toDTO(saved);
    }

    @Transactional(readOnly = true)
    public CashAccountDTO findById(UUID id) {
        CashAccount cashAccount = cashAccountRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Conta não encontrada: " + id));
        return toDTO(cashAccount);
    }

    @Transactional(readOnly = true)
    public List<CashAccountDTO> findAll() {
        return cashAccountRepository.findAll().stream()
                .map(this::toDTO)
                .collect(Collectors.toList());
    }

    @Transactional(readOnly = true)
    public List<CashAccountDTO> findActive() {
        return cashAccountRepository.findByIsActiveTrue().stream()
                .map(this::toDTO)
                .collect(Collectors.toList());
    }

    @Transactional(readOnly = true)
    public List<CashAccountDTO> findByType(String type) {
        return cashAccountRepository.findByType(type).stream()
                .map(this::toDTO)
                .collect(Collectors.toList());
    }

    @Transactional(readOnly = true)
    public BigDecimal getAvailableBalance(UUID cashAccountId) {
        CashAccount cashAccount = cashAccountRepository.findById(cashAccountId)
                .orElseThrow(() -> new ResourceNotFoundException("Conta não encontrada: " + cashAccountId));
        return cashAccount.getAvailableBalance();
    }

    @Transactional(readOnly = true)
    public BigDecimal getCurrentBalance(UUID cashAccountId) {
        CashAccount cashAccount = cashAccountRepository.findById(cashAccountId)
                .orElseThrow(() -> new ResourceNotFoundException("Conta não encontrada: " + cashAccountId));
        return cashAccount.getCurrentBalance();
    }

    /**
     * Atualiza o saldo da conta após uma operação.
     * 
     * @param cashAccountId ID da conta
     * @param amount        Valor da operação (positivo para entrada, negativo para
     *                      saída)
     * @param operation     Tipo de operação (INFLOW, OUTFLOW)
     */
    public void updateBalance(UUID cashAccountId, BigDecimal amount, String operation) {
        CashAccount cashAccount = cashAccountRepository.findById(cashAccountId)
                .orElseThrow(() -> new ResourceNotFoundException("Conta não encontrada: " + cashAccountId));

        if ("INFLOW".equals(operation)) {
            cashAccount.setCurrentBalance(cashAccount.getCurrentBalance().add(amount));
            cashAccount.setAvailableBalance(cashAccount.getAvailableBalance().add(amount));
        } else if ("OUTFLOW".equals(operation)) {
            cashAccount.setCurrentBalance(cashAccount.getCurrentBalance().subtract(amount));
            cashAccount.setAvailableBalance(cashAccount.getAvailableBalance().subtract(amount));
        }

        cashAccountRepository.save(cashAccount);
        log.debug("Saldo atualizado: accountId={}, operation={}, amount={}, newBalance={}",
                cashAccountId, operation, amount, cashAccount.getCurrentBalance());
    }

    /**
     * Compromete saldo disponível (reduz availableBalance sem alterar
     * currentBalance).
     * Usado quando um pagamento é programado mas ainda não executado.
     */
    public void commitBalance(UUID cashAccountId, BigDecimal amount) {
        CashAccount cashAccount = cashAccountRepository.findById(cashAccountId)
                .orElseThrow(() -> new ResourceNotFoundException("Conta não encontrada: " + cashAccountId));

        if (cashAccount.getAvailableBalance().compareTo(amount) < 0) {
            throw new BusinessException(
                    String.format("Saldo disponível insuficiente. Disponível: %s, Solicitado: %s",
                            cashAccount.getAvailableBalance(), amount),
                    "INSUFFICIENT_BALANCE",
                    org.springframework.http.HttpStatus.CONFLICT);
        }

        cashAccount.setAvailableBalance(cashAccount.getAvailableBalance().subtract(amount));
        cashAccountRepository.save(cashAccount);
        log.debug("Saldo comprometido: accountId={}, amount={}, newAvailableBalance={}",
                cashAccountId, amount, cashAccount.getAvailableBalance());
    }

    /**
     * Libera saldo comprometido (aumenta availableBalance).
     * Usado quando um pagamento programado é cancelado.
     */
    public void releaseBalance(UUID cashAccountId, BigDecimal amount) {
        CashAccount cashAccount = cashAccountRepository.findById(cashAccountId)
                .orElseThrow(() -> new ResourceNotFoundException("Conta não encontrada: " + cashAccountId));

        cashAccount.setAvailableBalance(cashAccount.getAvailableBalance().add(amount));
        cashAccountRepository.save(cashAccount);
        log.debug("Saldo liberado: accountId={}, amount={}, newAvailableBalance={}",
                cashAccountId, amount, cashAccount.getAvailableBalance());
    }

    private CashAccountDTO toDTO(CashAccount cashAccount) {
        return CashAccountDTO.builder()
                .id(cashAccount.getId())
                .code(cashAccount.getCode())
                .name(cashAccount.getName())
                .type(cashAccount.getType())
                .bankId(cashAccount.getBankId())
                .accountNumber(cashAccount.getAccountNumber())
                .branchCode(cashAccount.getBranchCode())
                .currency(cashAccount.getCurrency())
                .isActive(cashAccount.getIsActive())
                .currentBalance(cashAccount.getCurrentBalance())
                .availableBalance(cashAccount.getAvailableBalance())
                .createdAt(cashAccount.getCreatedAt())
                .updatedAt(cashAccount.getUpdatedAt())
                .build();
    }
}
