/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.fineract.investor.cob.loan;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.cob.loan.LoanCOBBusinessStep;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAccountSnapshotBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.investor.config.InvestorModuleIsEnabledCondition;
import org.apache.fineract.investor.data.ExternalTransferStatus;
import org.apache.fineract.investor.data.ExternalTransferSubStatus;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferDetails;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMapping;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository;
import org.apache.fineract.investor.domain.LoanOwnershipTransferBusinessEvent;
import org.apache.fineract.investor.service.AccountingService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.springframework.context.annotation.Conditional;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
@Slf4j
@Conditional(InvestorModuleIsEnabledCondition.class)
public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep {

    public static final LocalDate FUTURE_DATE_9999_12_31 = LocalDate.of(9999, 12, 31);
    private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
    private final ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository;
    private final AccountingService accountingService;
    private final BusinessEventNotifierService businessEventNotifierService;

    @Override
    public Loan execute(Loan loan) {
        Long loanId = loan.getId();
        log.debug("start processing loan ownership transfer business step for loan with Id [{}]", loanId);

        LocalDate settlementDate = DateUtils.getBusinessLocalDate();
        List<ExternalAssetOwnerTransfer> transferDataList = externalAssetOwnerTransferRepository.findAll(
                (root, query, criteriaBuilder) -> criteriaBuilder.and(criteriaBuilder.equal(root.get("loanId"), loanId),
                        criteriaBuilder.equal(root.get("settlementDate"), settlementDate),
                        root.get("status").in(List.of(ExternalTransferStatus.PENDING, ExternalTransferStatus.BUYBACK)),
                        criteriaBuilder.greaterThanOrEqualTo(root.get("effectiveDateTo"), FUTURE_DATE_9999_12_31)),
                Sort.by(Sort.Direction.ASC, "id"));
        int size = transferDataList.size();

        if (size == 2) {
            ExternalTransferStatus firstTransferStatus = transferDataList.get(0).getStatus();
            ExternalTransferStatus secondTransferStatus = transferDataList.get(1).getStatus();

            if (!ExternalTransferStatus.PENDING.equals(firstTransferStatus)
                    || !ExternalTransferStatus.BUYBACK.equals(secondTransferStatus)) {
                throw new IllegalStateException(String.format("Illegal transfer found. Expected %s and %s, found: %s and %s",
                        ExternalTransferStatus.PENDING, ExternalTransferStatus.BUYBACK, firstTransferStatus, secondTransferStatus));
            }
            handleSameDaySaleAndBuyback(settlementDate, transferDataList, loan);
        } else if (size == 1) {
            ExternalAssetOwnerTransfer transfer = transferDataList.get(0);
            if (ExternalTransferStatus.PENDING.equals(transfer.getStatus())) {
                handleSale(loan, settlementDate, transfer);
            } else if (ExternalTransferStatus.BUYBACK.equals(transfer.getStatus())) {
                handleBuyback(loan, settlementDate, transfer);
            }
        }

        log.debug("end processing loan ownership transfer business step for loan Id [{}]", loan.getId());
        return loan;
    }

    private void handleSale(final Loan loan, final LocalDate settlementDate, final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
        ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = sellAsset(loan, settlementDate, externalAssetOwnerTransfer);
        businessEventNotifierService.notifyPostBusinessEvent(new LoanOwnershipTransferBusinessEvent(newExternalAssetOwnerTransfer, loan));
        if (!ExternalTransferStatus.DECLINED.equals(newExternalAssetOwnerTransfer.getStatus())) {
            businessEventNotifierService.notifyPostBusinessEvent(new LoanAccountSnapshotBusinessEvent(loan));
        }
    }

    private void handleBuyback(final Loan loan, final LocalDate settlementDate,
            final ExternalAssetOwnerTransfer buybackExternalAssetOwnerTransfer) {
        ExternalAssetOwnerTransfer activeExternalAssetOwnerTransfer = externalAssetOwnerTransferRepository
                .findOne((root, query, criteriaBuilder) -> criteriaBuilder.and(criteriaBuilder.equal(root.get("loanId"), loan.getId()),
                        criteriaBuilder.equal(root.get("owner"), buybackExternalAssetOwnerTransfer.getOwner()),
                        criteriaBuilder.equal(root.get("status"), ExternalTransferStatus.ACTIVE),
                        criteriaBuilder.equal(root.get("effectiveDateTo"), FUTURE_DATE_9999_12_31)))
                .orElseThrow();
        ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = buybackAsset(loan, settlementDate, buybackExternalAssetOwnerTransfer,
                activeExternalAssetOwnerTransfer);
        businessEventNotifierService.notifyPostBusinessEvent(new LoanOwnershipTransferBusinessEvent(newExternalAssetOwnerTransfer, loan));
        businessEventNotifierService.notifyPostBusinessEvent(new LoanAccountSnapshotBusinessEvent(loan));
    }

    private ExternalAssetOwnerTransfer buybackAsset(final Loan loan, final LocalDate settlementDate,
            ExternalAssetOwnerTransfer buybackExternalAssetOwnerTransfer, ExternalAssetOwnerTransfer activeExternalAssetOwnerTransfer) {
        activeExternalAssetOwnerTransfer.setEffectiveDateTo(settlementDate);
        buybackExternalAssetOwnerTransfer.setEffectiveDateTo(settlementDate);
        buybackExternalAssetOwnerTransfer
                .setExternalAssetOwnerTransferDetails(createAssetOwnerTransferDetails(loan, buybackExternalAssetOwnerTransfer));
        externalAssetOwnerTransferRepository.save(activeExternalAssetOwnerTransfer);
        buybackExternalAssetOwnerTransfer = externalAssetOwnerTransferRepository.save(buybackExternalAssetOwnerTransfer);
        externalAssetOwnerTransferLoanMappingRepository.deleteByLoanIdAndOwnerTransfer(loan.getId(), activeExternalAssetOwnerTransfer);
        accountingService.createJournalEntriesForBuybackAssetTransfer(loan, buybackExternalAssetOwnerTransfer);
        return buybackExternalAssetOwnerTransfer;
    }

    private ExternalAssetOwnerTransfer sellAsset(final Loan loan, final LocalDate settlementDate,
            ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
        ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer;
        if (isTransferable(loan)) {
            newExternalAssetOwnerTransfer = createActiveEntry(settlementDate, externalAssetOwnerTransfer);
            createActiveMapping(loan.getId(), newExternalAssetOwnerTransfer);
            newExternalAssetOwnerTransfer
                    .setExternalAssetOwnerTransferDetails(createAssetOwnerTransferDetails(loan, newExternalAssetOwnerTransfer));
            accountingService.createJournalEntriesForSaleAssetTransfer(loan, newExternalAssetOwnerTransfer);
        } else {
            ExternalTransferSubStatus subStatus = ExternalTransferSubStatus.BALANCE_ZERO;
            if (MathUtil.nullToDefault(loan.getTotalOverpaid(), BigDecimal.ZERO).compareTo(BigDecimal.ZERO) > 0) {
                subStatus = ExternalTransferSubStatus.BALANCE_NEGATIVE;
            }
            newExternalAssetOwnerTransfer = createNewEntry(settlementDate, externalAssetOwnerTransfer, ExternalTransferStatus.DECLINED,
                    subStatus, settlementDate, settlementDate);
        }
        return newExternalAssetOwnerTransfer;
    }

    private ExternalAssetOwnerTransferDetails createAssetOwnerTransferDetails(Loan loan,
            ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
        ExternalAssetOwnerTransferDetails details = new ExternalAssetOwnerTransferDetails();
        details.setExternalAssetOwnerTransfer(externalAssetOwnerTransfer);
        details.setTotalOutstanding(Objects.requireNonNullElse(loan.getLoanSummary().getTotalOutstanding(), BigDecimal.ZERO));
        details.setTotalPrincipalOutstanding(
                Objects.requireNonNullElse(loan.getLoanSummary().getTotalPrincipalOutstanding(), BigDecimal.ZERO));
        details.setTotalInterestOutstanding(
                Objects.requireNonNullElse(loan.getLoanSummary().getTotalInterestOutstanding(), BigDecimal.ZERO));
        details.setTotalFeeChargesOutstanding(
                Objects.requireNonNullElse(loan.getLoanSummary().getTotalFeeChargesOutstanding(), BigDecimal.ZERO));
        details.setTotalPenaltyChargesOutstanding(
                Objects.requireNonNullElse(loan.getLoanSummary().getTotalPenaltyChargesOutstanding(), BigDecimal.ZERO));
        details.setTotalOverpaid(Objects.requireNonNullElse(loan.getTotalOverpaid(), BigDecimal.ZERO));
        return details;
    }

    private void createActiveMapping(Long loanId, ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
        ExternalAssetOwnerTransferLoanMapping externalAssetOwnerTransferLoanMapping = new ExternalAssetOwnerTransferLoanMapping();
        externalAssetOwnerTransferLoanMapping.setLoanId(loanId);
        externalAssetOwnerTransferLoanMapping.setOwnerTransfer(externalAssetOwnerTransfer);
        externalAssetOwnerTransferLoanMappingRepository.save(externalAssetOwnerTransferLoanMapping);
    }

    private boolean isTransferable(final Loan loan) {
        return MathUtil.nullToDefault(loan.getLoanSummary().getTotalOutstanding(), BigDecimal.ZERO).compareTo(BigDecimal.ZERO) > 0;
    }

    private void handleSameDaySaleAndBuyback(final LocalDate settlementDate, final List<ExternalAssetOwnerTransfer> transferDataList,
            Loan loan) {
        ExternalAssetOwnerTransfer cancelledPendingTransfer = cancelTransfer(settlementDate, transferDataList.get(0));
        ExternalAssetOwnerTransfer cancelledBuybackTransfer = cancelTransfer(settlementDate, transferDataList.get(1));
        businessEventNotifierService.notifyPostBusinessEvent(new LoanOwnershipTransferBusinessEvent(cancelledPendingTransfer, loan));
        businessEventNotifierService.notifyPostBusinessEvent(new LoanOwnershipTransferBusinessEvent(cancelledBuybackTransfer, loan));
    }

    private ExternalAssetOwnerTransfer cancelTransfer(final LocalDate settlementDate,
            final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
        return createNewEntry(settlementDate, externalAssetOwnerTransfer, ExternalTransferStatus.CANCELLED,
                ExternalTransferSubStatus.SAMEDAY_TRANSFERS, settlementDate, settlementDate);
    }

    private ExternalAssetOwnerTransfer createNewEntry(final LocalDate settlementDate,
            final ExternalAssetOwnerTransfer externalAssetOwnerTransfer, final ExternalTransferStatus status,
            final ExternalTransferSubStatus subStatus, final LocalDate effectiveDateFrom, final LocalDate effectiveDateTo) {
        ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
        newExternalAssetOwnerTransfer.setOwner(externalAssetOwnerTransfer.getOwner());
        newExternalAssetOwnerTransfer.setExternalId(externalAssetOwnerTransfer.getExternalId());
        newExternalAssetOwnerTransfer.setStatus(status);
        newExternalAssetOwnerTransfer.setSubStatus(subStatus);
        newExternalAssetOwnerTransfer.setSettlementDate(settlementDate);
        newExternalAssetOwnerTransfer.setLoanId(externalAssetOwnerTransfer.getLoanId());
        newExternalAssetOwnerTransfer.setExternalLoanId(externalAssetOwnerTransfer.getExternalLoanId());
        newExternalAssetOwnerTransfer.setPurchasePriceRatio(externalAssetOwnerTransfer.getPurchasePriceRatio());
        newExternalAssetOwnerTransfer.setEffectiveDateFrom(effectiveDateFrom);
        newExternalAssetOwnerTransfer.setEffectiveDateTo(effectiveDateTo);

        externalAssetOwnerTransfer.setEffectiveDateTo(settlementDate);
        externalAssetOwnerTransferRepository.save(externalAssetOwnerTransfer);
        return externalAssetOwnerTransferRepository.save(newExternalAssetOwnerTransfer);
    }

    private ExternalAssetOwnerTransfer createActiveEntry(final LocalDate settlementDate,
            final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
        LocalDate effectiveFrom = settlementDate.plusDays(1);
        return createNewEntry(settlementDate, externalAssetOwnerTransfer, ExternalTransferStatus.ACTIVE, null, effectiveFrom,
                FUTURE_DATE_9999_12_31);
    }

    @Override
    public String getEnumStyledName() {
        return "EXTERNAL_ASSET_OWNER_TRANSFER";
    }

    @Override
    public String getHumanReadableName() {
        return "Execute external asset owner transfer";
    }
}
