/**
 * 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.portfolio.savings.domain;

import static org.apache.fineract.portfolio.savings.DepositsApiConstants.dateFormatParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.depositAmountParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.depositPeriodFrequencyIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.depositPeriodParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.expectedFirstDepositOnDateParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.localeParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.maturityInstructionIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferInterestToSavingsParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferToSavingsIdParamName;

import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType;
import org.apache.fineract.portfolio.savings.SavingsPeriodFrequencyType;
import org.apache.fineract.portfolio.savings.service.SavingsEnumerations;

@Entity
@Table(name = "m_deposit_account_term_and_preclosure")
public class DepositAccountTermAndPreClosure extends AbstractPersistableCustom {

    @Column(name = "deposit_amount", scale = 6, precision = 19, nullable = true)
    private BigDecimal depositAmount;

    @Column(name = "maturity_amount", scale = 6, precision = 19, nullable = true)
    private BigDecimal maturityAmount;

    @Column(name = "maturity_date", nullable = true)
    private LocalDate maturityDate;

    @Column(name = "expected_firstdepositon_date")
    private LocalDate expectedFirstDepositOnDate;

    @Column(name = "deposit_period", nullable = true)
    private Integer depositPeriod;

    @Column(name = "deposit_period_frequency_enum", nullable = true)
    private Integer depositPeriodFrequency;

    @Column(name = "on_account_closure_enum", nullable = true)
    private Integer onAccountClosureType;

    @Embedded
    private DepositPreClosureDetail preClosureDetail;

    @Embedded
    protected DepositTermDetail depositTermDetail;

    @OneToOne
    @JoinColumn(name = "savings_account_id", nullable = false)
    private SavingsAccount account;

    @Column(name = "transfer_interest_to_linked_account", nullable = false)
    private boolean transferInterestToLinkedAccount;

    @Column(name = "transfer_to_savings_account_id")
    private Long transferToSavingsAccountId;

    protected DepositAccountTermAndPreClosure() {

    }

    public static DepositAccountTermAndPreClosure createNew(DepositPreClosureDetail preClosureDetail, DepositTermDetail depositTermDetail,
            SavingsAccount account, BigDecimal depositAmount, BigDecimal maturityAmount, final LocalDate maturityDate,
            Integer depositPeriod, final SavingsPeriodFrequencyType depositPeriodFrequency, final LocalDate expectedFirstDepositOnDate,
            final DepositAccountOnClosureType accountOnClosureType, Boolean trasferInterest, Long transferToSavingsId) {

        return new DepositAccountTermAndPreClosure(preClosureDetail, depositTermDetail, account, depositAmount, maturityAmount,
                maturityDate, depositPeriod, depositPeriodFrequency, expectedFirstDepositOnDate, accountOnClosureType, trasferInterest,
                transferToSavingsId);
    }

    private DepositAccountTermAndPreClosure(DepositPreClosureDetail preClosureDetail, DepositTermDetail depositTermDetail,
            SavingsAccount account, BigDecimal depositAmount, BigDecimal maturityAmount, final LocalDate maturityDate,
            Integer depositPeriod, final SavingsPeriodFrequencyType depositPeriodFrequency, final LocalDate expectedFirstDepositOnDate,
            final DepositAccountOnClosureType accountOnClosureType, Boolean transferInterest, Long transferToSavingsId) {
        this.depositAmount = depositAmount;
        this.maturityAmount = maturityAmount;
        this.maturityDate = maturityDate;
        this.depositPeriod = depositPeriod;
        this.depositPeriodFrequency = (depositPeriodFrequency == null) ? null : depositPeriodFrequency.getValue();
        this.preClosureDetail = preClosureDetail;
        this.depositTermDetail = depositTermDetail;
        this.account = account;
        this.expectedFirstDepositOnDate = expectedFirstDepositOnDate;
        this.onAccountClosureType = (accountOnClosureType == null) ? null : accountOnClosureType.getValue();
        this.transferInterestToLinkedAccount = transferInterest;
        this.transferToSavingsAccountId = transferToSavingsId;
    }

    public Map<String, Object> update(final JsonCommand command, final DataValidatorBuilder baseDataValidator) {
        final Map<String, Object> actualChanges = new LinkedHashMap<>(10);

        if (command.isChangeInBigDecimalParameterNamed(depositAmountParamName, this.depositAmount)) {
            final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(depositAmountParamName);
            actualChanges.put(depositAmountParamName, newValue);
            this.depositAmount = newValue;
        }

        if (command.isChangeInIntegerParameterNamed(depositPeriodParamName, this.depositPeriod)) {
            final Integer newValue = command.integerValueOfParameterNamed(depositPeriodParamName);
            actualChanges.put(depositPeriodParamName, newValue);
            this.depositPeriod = newValue;
        }

        if (command.isChangeInIntegerParameterNamed(depositPeriodFrequencyIdParamName, this.depositPeriodFrequency)) {
            final Integer newValue = command.integerValueOfParameterNamed(depositPeriodFrequencyIdParamName);
            actualChanges.put(depositPeriodFrequencyIdParamName, SavingsEnumerations.depositTermFrequencyType(newValue));
            this.depositPeriodFrequency = newValue;
        }

        final String localeAsInput = command.locale();
        final String dateFormat = command.dateFormat();
        if (command.isChangeInLocalDateParameterNamed(expectedFirstDepositOnDateParamName, this.getExpectedFirstDepositOnDate())) {
            final String newValueAsString = command.stringValueOfParameterNamed(expectedFirstDepositOnDateParamName);
            actualChanges.put(expectedFirstDepositOnDateParamName, newValueAsString);
            actualChanges.put(localeParamName, localeAsInput);
            actualChanges.put(dateFormatParamName, dateFormat);
            this.expectedFirstDepositOnDate = command.localDateValueOfParameterNamed(expectedFirstDepositOnDateParamName);
        }

        if (command.isChangeInBooleanParameterNamed(transferInterestToSavingsParamName, this.transferInterestToLinkedAccount)) {
            final Boolean newValue = command.booleanPrimitiveValueOfParameterNamed(transferInterestToSavingsParamName);
            actualChanges.put(transferInterestToSavingsParamName, newValue);
            this.transferInterestToLinkedAccount = newValue;
        }

        if (command.isChangeInIntegerParameterNamed(maturityInstructionIdParamName, this.onAccountClosureType)
                || command.integerValueOfParameterNamed(maturityInstructionIdParamName) == null) {
            final Integer newValue = command.integerValueOfParameterNamed(maturityInstructionIdParamName);
            actualChanges.put(maturityInstructionIdParamName, newValue);
            this.onAccountClosureType = newValue != null ? DepositAccountOnClosureType.fromInt(newValue).getValue() : null;
        }

        if (command.isChangeInLongParameterNamed(transferToSavingsIdParamName, this.transferToSavingsAccountId)
                || command.integerValueOfParameterNamed(transferToSavingsIdParamName) == null) {
            final Long newValue = command.longValueOfParameterNamed(transferToSavingsIdParamName);
            actualChanges.put(transferToSavingsIdParamName, newValue);
            this.transferToSavingsAccountId = newValue;
        }

        if (this.preClosureDetail != null) {
            actualChanges.putAll(this.preClosureDetail.update(command, baseDataValidator));
        }

        if (this.depositTermDetail != null) {
            actualChanges.putAll(this.depositTermDetail.update(command, baseDataValidator));
        }
        return actualChanges;
    }

    public DepositPreClosureDetail depositPreClosureDetail() {
        return this.preClosureDetail;
    }

    public DepositTermDetail depositTermDetail() {
        return this.depositTermDetail;
    }

    public BigDecimal depositAmount() {
        return this.depositAmount;
    }

    public Integer depositPeriod() {
        return this.depositPeriod;
    }

    public Integer depositPeriodFrequency() {
        return this.depositPeriodFrequency;
    }

    public SavingsPeriodFrequencyType depositPeriodFrequencyType() {
        return SavingsPeriodFrequencyType.fromInt(depositPeriodFrequency);
    }

    public void updateAccountReference(final SavingsAccount account) {
        this.account = account;
    }

    public void updateMaturityDetails(final BigDecimal maturityAmount, final LocalDate maturityDate) {
        this.maturityAmount = maturityAmount;
        this.maturityDate = maturityDate;
    }

    public void updateMaturityDetails(final BigDecimal depositAmount, final BigDecimal interestPayable, final LocalDate maturityDate) {
        this.depositAmount = depositAmount;
        this.maturityAmount = this.depositAmount.add(interestPayable);
        this.maturityDate = maturityDate;
    }

    public void updateDepositAmount(final BigDecimal depositAmount) {
        this.depositAmount = depositAmount;
    }

    public LocalDate getMaturityLocalDate() {
        return this.maturityDate;
    }

    public LocalDate getExpectedFirstDepositOnDate() {
        return this.expectedFirstDepositOnDate;
    }

    public boolean isPreClosurePenalApplicable() {
        if (this.preClosureDetail != null) {
            return this.preClosureDetail.preClosurePenalApplicable();
        }
        return false;
    }

    public Integer getActualDepositPeriod(final LocalDate interestPostingUpToDate, final SavingsPeriodFrequencyType periodFrequencyType) {
        LocalDate depositFromDate = getExpectedFirstDepositOnDate();

        if (depositFromDate == null) {
            depositFromDate = this.account.accountSubmittedOrActivationDate();
        }

        Integer actualDepositPeriod = this.depositPeriod;
        if (depositFromDate == null || getMaturityLocalDate() == null || interestPostingUpToDate.isEqual(getMaturityLocalDate())) {
            return actualDepositPeriod;
        }

        final SavingsPeriodFrequencyType depositPeriodFrequencyType = periodFrequencyType;
        switch (depositPeriodFrequencyType) {
            case DAYS:
                actualDepositPeriod = Math.toIntExact(ChronoUnit.DAYS.between(depositFromDate, interestPostingUpToDate));
            break;
            case WEEKS:
                actualDepositPeriod = Math.toIntExact(ChronoUnit.WEEKS.between(depositFromDate, interestPostingUpToDate));
            break;
            case MONTHS:
                actualDepositPeriod = Math.toIntExact(ChronoUnit.MONTHS.between(depositFromDate, interestPostingUpToDate));
            break;
            case YEARS:
                actualDepositPeriod = Math.toIntExact(ChronoUnit.YEARS.between(depositFromDate, interestPostingUpToDate));
            break;
            case INVALID:
                actualDepositPeriod = this.depositPeriod;// default value
            break;
        }
        return actualDepositPeriod;
    }

    public BigDecimal maturityAmount() {
        return this.maturityAmount;
    }

    public void updateOnAccountClosureStatus(final DepositAccountOnClosureType onClosureType) {
        this.onAccountClosureType = onClosureType.getValue();
    }

    public boolean isReinvestOnClosure() {
        return DepositAccountOnClosureType.fromInt(this.onAccountClosureType).isReinvest();
    }

    public boolean isTransferToSavingsOnClosure() {
        return DepositAccountOnClosureType.fromInt(this.onAccountClosureType).isTransferToSavings();
    }

    public DepositAccountTermAndPreClosure copy(BigDecimal depositAmount) {
        final SavingsAccount account = null;
        final BigDecimal maturityAmount = null;
        final BigDecimal actualDepositAmount = depositAmount;
        final LocalDate maturityDate = null;
        final Integer depositPeriod = this.depositPeriod;
        final SavingsPeriodFrequencyType depositPeriodFrequency = SavingsPeriodFrequencyType.fromInt(this.depositPeriodFrequency);
        final DepositPreClosureDetail preClosureDetail = this.preClosureDetail.copy();
        final DepositTermDetail depositTermDetail = this.depositTermDetail.copy();
        final LocalDate expectedFirstDepositOnDate = null;
        final Boolean transferInterestToLinkedAccount = false;

        final DepositAccountOnClosureType accountOnClosureType = null;
        final Long transferToSavingsId = null;
        return DepositAccountTermAndPreClosure.createNew(preClosureDetail, depositTermDetail, account, actualDepositAmount, maturityAmount,
                maturityDate, depositPeriod, depositPeriodFrequency, expectedFirstDepositOnDate, accountOnClosureType,
                transferInterestToLinkedAccount, transferToSavingsId);
    }

    public void updateExpectedFirstDepositDate(final LocalDate expectedFirstDepositOnDate) {
        this.expectedFirstDepositOnDate = expectedFirstDepositOnDate;
    }

    public boolean isTransferInterestToLinkedAccount() {
        return this.transferInterestToLinkedAccount;
    }

    public boolean isAfterExpectedFirstDepositDate(final LocalDate compareDate) {
        boolean isAfterExpectedFirstDepositDate = false;
        if (this.expectedFirstDepositOnDate != null) {
            isAfterExpectedFirstDepositDate = compareDate.isAfter(getExpectedFirstDepositOnDate());
        }
        return isAfterExpectedFirstDepositDate;
    }

    public Integer getOnAccountClosureType() {
        return onAccountClosureType;
    }

    public Long getTransferToSavingsAccountId() {
        return transferToSavingsAccountId;
    }
}
