/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 *
 * Copyright 2005-2016 The Kuali Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kuali.kfs.module.cam.document.service.impl;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.coa.businessobject.ObjectCode;
import org.kuali.kfs.coa.service.ObjectCodeService;
import org.kuali.kfs.coreservice.framework.parameter.ParameterService;
import org.kuali.kfs.krad.bo.PersistableBusinessObject;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.krad.util.GlobalVariables;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.module.cam.CamsConstants;
import org.kuali.kfs.module.cam.CamsKeyConstants;
import org.kuali.kfs.module.cam.CamsPropertyConstants;
import org.kuali.kfs.module.cam.batch.AssetDepreciationStep;
import org.kuali.kfs.module.cam.businessobject.Asset;
import org.kuali.kfs.module.cam.businessobject.AssetGlobal;
import org.kuali.kfs.module.cam.businessobject.AssetPayment;
import org.kuali.kfs.module.cam.businessobject.AssetPaymentAllocationType;
import org.kuali.kfs.module.cam.businessobject.AssetPaymentAssetDetail;
import org.kuali.kfs.module.cam.businessobject.AssetPaymentDetail;
import org.kuali.kfs.module.cam.document.AssetPaymentDocument;
import org.kuali.kfs.module.cam.document.dataaccess.AssetPaymentDao;
import org.kuali.kfs.module.cam.document.service.AssetGlobalService;
import org.kuali.kfs.module.cam.document.service.AssetPaymentService;
import org.kuali.kfs.module.cam.document.service.AssetRetirementService;
import org.kuali.kfs.module.cam.document.service.AssetService;
import org.kuali.kfs.module.cam.util.distribution.AssetDistribution;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.businessobject.UniversityDate;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.service.UniversityDateService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.springframework.transaction.annotation.Transactional;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

@Transactional
public class AssetPaymentServiceImpl implements AssetPaymentService {
    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(AssetPaymentServiceImpl.class);

    private BusinessObjectService businessObjectService;
    private AssetPaymentDao assetPaymentDao;
    private ParameterService parameterService;
    private UniversityDateService universityDateService;
    private ObjectCodeService objectCodeService;
    private AssetRetirementService assetRetirementService;
    private AssetService assetService;
    private AssetGlobalService assetGlobalService;

    /**
     * @see org.kuali.kfs.module.cam.document.service.AssetPaymentService#getMaxSequenceNumber(org.kuali.kfs.module.cam.businessobject.AssetPayment)
     */
    public Integer getMaxSequenceNumber(Long capitalAssetNumber) {
        return this.getAssetPaymentDao().getMaxSquenceNumber(capitalAssetNumber);
    }

    /**
     * @see org.kuali.kfs.module.cam.document.service.AssetPaymentService#isPaymentFederalOwned(org.kuali.kfs.module.cam.businessobject.AssetPayment)
     */
    public boolean isPaymentFederalOwned(AssetPayment assetPayment) {
        assetPayment.refreshReferenceObject(CamsPropertyConstants.AssetPayment.OBJECT_CODE_CURRENT);
        if (ObjectUtils.isNotNull(assetPayment.getObjectCodeCurrent())) {
            String subTypeCode = assetPayment.getObjectCodeCurrent().getFinancialObjectSubTypeCode();
            return this.getParameterService().getParameterValuesAsString(Asset.class, CamsConstants.Parameters.FEDERAL_OWNED_OBJECT_SUB_TYPES).contains(subTypeCode);
        }
        return false;
    }


    /**
     * @see org.kuali.kfs.module.cam.document.service.AssetPaymentService#isPaymentEligibleForAccumDeprGLPosting(org.kuali.kfs.module.cam.businessobject.AssetPayment)
     */
    public boolean isPaymentEligibleForAccumDeprGLPosting(AssetPayment assetPayment) {
        KualiDecimal accumlatedDepreciationAmount = assetPayment.getAccumulatedPrimaryDepreciationAmount();
        return accumlatedDepreciationAmount == null ? false : !accumlatedDepreciationAmount.isZero();
    }

    /**
     * @see org.kuali.kfs.module.cam.document.service.AssetPaymentService#isPaymentEligibleForCapitalizationGLPosting(org.kuali.kfs.module.cam.businessobject.AssetPayment)
     */
    public boolean isPaymentEligibleForCapitalizationGLPosting(AssetPayment assetPayment) {
        KualiDecimal accountChargeAmount = assetPayment.getAccountChargeAmount();
        return accountChargeAmount == null ? false : !accountChargeAmount.isZero();
    }

    /**
     * @see org.kuali.kfs.module.cam.document.service.AssetPaymentService#isPaymentEligibleForOffsetGLPosting(org.kuali.kfs.module.cam.businessobject.AssetPayment)
     */
    public boolean isPaymentEligibleForOffsetGLPosting(AssetPayment assetPayment) {
        KualiDecimal accountChargeAmount = assetPayment.getAccountChargeAmount();
        if (assetPayment.getAccumulatedPrimaryDepreciationAmount() == null) {
            assetPayment.setAccumulatedPrimaryDepreciationAmount(KualiDecimal.ZERO);
        }
        KualiDecimal accumlatedDepreciationAmount = assetPayment.getAccumulatedPrimaryDepreciationAmount();
        return accountChargeAmount == null ? false : !accountChargeAmount.subtract(accumlatedDepreciationAmount).isZero();
    }

    /**
     * @see org.kuali.kfs.module.cam.document.service.AssetPaymentService#isPaymentFinancialObjectActive(org.kuali.kfs.module.cam.businessobject.AssetPayment)
     */
    public boolean isPaymentFinancialObjectActive(AssetPayment assetPayment) {
        ObjectCode financialObjectCode = new ObjectCode();
        financialObjectCode.setUniversityFiscalYear(getUniversityDateService().getCurrentFiscalYear());
        financialObjectCode.setChartOfAccountsCode(assetPayment.getChartOfAccountsCode());
        financialObjectCode.setFinancialObjectCode(assetPayment.getFinancialObjectCode());
        financialObjectCode = (ObjectCode) getBusinessObjectService().retrieve(financialObjectCode);
        if (ObjectUtils.isNotNull(financialObjectCode)) {
            return financialObjectCode.isActive();
        }
        return false;
    }


    /**
     * @see org.kuali.kfs.module.cam.document.service.AssetPaymentService#processApprovedAssetPayment(org.kuali.kfs.module.cam.document.AssetPaymentDocument)
     */
    public void processApprovedAssetPayment(AssetPaymentDocument document) {
        // Creating new asset payment records
        processPayments(document);
    }


    /**
     * Creates a new asset payment record for each new asset payment detail record and then save them
     *
     * @param document
     */
    protected void processPayments(AssetPaymentDocument document) {
        List<AssetPaymentDetail> assetPaymentDetailLines = document.getSourceAccountingLines();
        List<AssetPaymentAssetDetail> assetPaymentAssetDetails = document.getAssetPaymentAssetDetail();
        List<PersistableBusinessObject> assetPayments = new ArrayList<PersistableBusinessObject>();
        Integer maxSequenceNo = new Integer(0);

        //instantiate asset payment distributor
        AssetDistribution paymentDistributor = document.getAssetPaymentDistributor();

        // Calculating the asset payments distributions for each individual asset on the list
        Map<String, Map<AssetPaymentAssetDetail, KualiDecimal>> assetPaymentDistributionMap = paymentDistributor.getAssetPaymentDistributions();

        try {
            // Creating a new payment record for each asset that has payments.
            for (AssetPaymentAssetDetail assetPaymentAssetDetail : assetPaymentAssetDetails) {

                maxSequenceNo = getMaxSequenceNumber(assetPaymentAssetDetail.getCapitalAssetNumber());

                KualiDecimal totalAmount = KualiDecimal.ZERO;
                for (AssetPaymentDetail assetPaymentDetail : assetPaymentDetailLines) {

                    // Retrieve the asset payment from the distribution map
                    KualiDecimal amount = assetPaymentDistributionMap.get(assetPaymentDetail.getAssetPaymentDetailKey()).get(assetPaymentAssetDetail);
                    totalAmount = totalAmount.add(amount);

                    AssetPayment assetPayment = new AssetPayment(assetPaymentDetail);
                    assetPayment.setCapitalAssetNumber(assetPaymentAssetDetail.getCapitalAssetNumber());
                    assetPayment.setTransferPaymentCode(CamsConstants.AssetPayment.TRANSFER_PAYMENT_CODE_N);
                    assetPayment.setPaymentSequenceNumber(++maxSequenceNo);
                    if (StringUtils.isBlank(assetPayment.getDocumentNumber())) {
                        assetPayment.setDocumentNumber(document.getDocumentNumber());
                    }
                    assetPayment.setAccountChargeAmount(amount);

                    KualiDecimal baseAmount = KualiDecimal.ZERO;

                    // If the object sub type is not in the list of federally owned object sub types, then...
                    ObjectCode objectCode = this.getObjectCodeService().getByPrimaryId(assetPaymentDetail.getPostingYear(), assetPaymentDetail.getChartOfAccountsCode(), assetPaymentDetail.getFinancialObjectCode());

                    // Depreciation Base Amount will be assigned to each payment only when the object code's sub type code is not a
                    // federally owned one
                    if (!this.isNonDepreciableFederallyOwnedObjSubType(objectCode.getFinancialObjectSubTypeCode())) {
                        baseAmount = baseAmount.add(amount);
                    }
                    assetPayment.setPrimaryDepreciationBaseAmount(baseAmount);

                    // Resetting each period field its value with nulls
                    this.adjustPaymentAmounts(assetPayment, false, true);

                    // add new payment
                    assetPayments.add(assetPayment);
                }
                // *********************BEGIN - Updating Asset ***********************************************************
                // Retrieving the asset that will have its cost updated
                HashMap<String, Long> keys = new HashMap<String, Long>();
                keys.put(CamsPropertyConstants.Asset.CAPITAL_ASSET_NUMBER, assetPaymentAssetDetail.getCapitalAssetNumber());
                Asset asset = (Asset) getBusinessObjectService().findByPrimaryKey(Asset.class, keys);

                // Set previous total cost
                if (assetPaymentAssetDetail.getPreviousTotalCostAmount() == null) {
                    assetPaymentAssetDetail.setPreviousTotalCostAmount(new KualiDecimal(0));
                }

                // Setting the asset's new cost.
                asset.setTotalCostAmount(assetPaymentAssetDetail.getPreviousTotalCostAmount().add(totalAmount));

                // Setting the asset's financial object sub-type Code. Only when the asset doesn't have one.
                if (asset.getFinancialObjectSubTypeCode() == null || asset.getFinancialObjectSubTypeCode().trim().equals("")) {
                    asset.setFinancialObjectSubTypeCode(assetPaymentDetailLines.get(0).getObjectCode().getFinancialObjectSubTypeCode());
                }
                // Saving changes
                getBusinessObjectService().save(asset);
                // *********************END - Updating Asset ***********************************************************
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        // Finally, saving all the asset payment records.
        this.getBusinessObjectService().save(assetPayments);
    }


    /**
     * @see org.kuali.kfs.module.cam.document.service.AssetPaymentService#adjustPaymentAmounts(org.kuali.kfs.module.cam.businessobject.AssetPayment,
     * boolean, boolean)
     */
    public void adjustPaymentAmounts(AssetPayment assetPayment, boolean reverseAmount, boolean nullPeriodDepreciation) throws IllegalAccessException, InvocationTargetException {
        LOG.debug("Starting - adjustAmounts() ");
        PropertyDescriptor[] propertyDescriptors = PropertyUtils.getPropertyDescriptors(AssetPayment.class);
        for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
            Method readMethod = propertyDescriptor.getReadMethod();
            if (readMethod != null && propertyDescriptor.getPropertyType() != null && KualiDecimal.class.isAssignableFrom(propertyDescriptor.getPropertyType())) {
                KualiDecimal amount = (KualiDecimal) readMethod.invoke(assetPayment);
                Method writeMethod = propertyDescriptor.getWriteMethod();
                if (writeMethod != null && amount != null) {
                    // Reset periodic depreciation expenses
                    if (nullPeriodDepreciation && Pattern.matches(CamsConstants.SET_PERIOD_DEPRECIATION_AMOUNT_REGEX, writeMethod.getName().toLowerCase())) {
                        Object[] nullVal = new Object[]{null};
                        writeMethod.invoke(assetPayment, nullVal);
                    } else if (reverseAmount) {
                        // reverse the amounts
                        writeMethod.invoke(assetPayment, (amount.negated()));
                    }
                }

            }
        }
        LOG.debug("Finished - adjustAmounts()");
    }


    /**
     * @see org.kuali.kfs.module.cam.document.service.AssetPaymentService#isPaymentEligibleForGLPosting(org.kuali.kfs.module.cam.businessobject.AssetPayment)
     */
    public boolean isPaymentEligibleForGLPosting(AssetPayment assetPayment) {
        // Transfer payment code flag is not Y
        boolean isEligible = !CamsConstants.AssetPayment.TRANSFER_PAYMENT_CODE_Y.equals(assetPayment.getTransferPaymentCode());
        // Financial object code is currently active
        isEligible &= isPaymentFinancialObjectActive(assetPayment);
        // Payment is not federally funded
        isEligible &= !isPaymentFederalOwned(assetPayment);
        return isEligible;
    }

    /**
     * Checks if object sub type is a non-depreciable federally owned object sub type
     *
     * @param string objectSubType2
     * @return true if is NON_DEPRECIABLE_FEDERALLY_OWNED_OBJECT_SUB_TYPES
     */
    public boolean isNonDepreciableFederallyOwnedObjSubType(String objectSubType) {
        List<String> federallyOwnedObjectSubTypes = new ArrayList<>();
        if (this.getParameterService().parameterExists(AssetDepreciationStep.class, CamsConstants.Parameters.NON_DEPRECIABLE_FEDERALLY_OWNED_OBJECT_SUB_TYPES)) {
            federallyOwnedObjectSubTypes = new ArrayList<>(this.getParameterService().getParameterValuesAsString(AssetDepreciationStep.class, CamsConstants.Parameters.NON_DEPRECIABLE_FEDERALLY_OWNED_OBJECT_SUB_TYPES));
        }
        return federallyOwnedObjectSubTypes.contains(objectSubType);
    }

    /**
     * @see org.kuali.kfs.module.cam.document.service.AssetPaymentService#extractPostedDatePeriod(org.kuali.kfs.module.cam.businessobject.AssetPaymentDetail)
     */
    public boolean extractPostedDatePeriod(AssetPaymentDetail assetPaymentDetail) {
        Map<String, Object> primaryKeys = new HashMap<>();
        primaryKeys.put(KFSPropertyConstants.UNIVERSITY_DATE, assetPaymentDetail.getExpenditureFinancialDocumentPostedDate());
        UniversityDate universityDate = businessObjectService.findByPrimaryKey(UniversityDate.class, primaryKeys);
        if (universityDate != null) {
            assetPaymentDetail.setPostingYear(universityDate.getUniversityFiscalYear());
            assetPaymentDetail.setPostingPeriodCode(universityDate.getUniversityFiscalAccountingPeriod());
            return true;
        } else {
            return false;
        }
    }

    /**
     * @see org.kuali.kfs.module.cam.document.service.AssetPaymentService#getAssetPaymentDetailQuantity(org.kuali.kfs.module.cam.businessobject.AssetGlobal)
     */
    public Integer getAssetPaymentDetailQuantity(AssetGlobal assetGlobal) {
        Integer assetPaymentDetailQuantity = 0;
        for (AssetPaymentDetail assetPaymentDetail : assetGlobal.getAssetPaymentDetails()) {
            assetPaymentDetailQuantity++;
        }
        return assetPaymentDetailQuantity;
    }


    /**
     * @see org.kuali.kfs.module.cam.document.service.AssetPaymentService#validateAssets(java.lang.String,
     * org.kuali.kfs.module.cam.businessobject.Asset)
     */
    public boolean validateAssets(String errorPath, Asset asset) {
        boolean valid = true;

        // Validating the asset is a capital asset
        if (!this.getAssetService().isCapitalAsset(asset)) {
            GlobalVariables.getMessageMap().putError(errorPath, CamsKeyConstants.Payment.ERROR_NON_CAPITAL_ASSET, asset.getCapitalAssetNumber().toString());
            valid &= false;
        }

        // Validating the asset hasn't been retired
        if (this.getAssetService().isAssetRetired(asset)) {
            GlobalVariables.getMessageMap().putError(errorPath, CamsKeyConstants.Retirement.ERROR_NON_ACTIVE_ASSET_RETIREMENT, asset.getCapitalAssetNumber().toString());
            valid &= false;
        }
        return valid;
    }


    /**
     * This method determines whether or not an asset has different object sub type codes in its documents.
     *
     * @return true when the asset has payments with object codes that point to different object sub type codes
     */
    public boolean hasDifferentObjectSubTypes(AssetPaymentDocument document) {
        List<String> subTypes = new ArrayList<String>();
        subTypes = new ArrayList<String>(SpringContext.getBean(ParameterService.class).getParameterValuesAsString(Asset.class, CamsConstants.Parameters.OBJECT_SUB_TYPE_GROUPS));

        List<AssetPaymentDetail> assetPaymentDetails = document.getSourceAccountingLines();
        List<String> validObjectSubTypes = new ArrayList<String>();

        String objectSubTypeCode = null;

        /*
         * Expected system parameter elements (object sub types). [BD,BF] [CM,CF,CO] [UC,UF,UO] [LI,LF]
         */

        // Getting the list of object sub type codes from the asset payments on the jsp.
        List<String> objectSubTypeList = new ArrayList<String>();
        for (AssetPaymentDetail assetPaymentDetail : assetPaymentDetails) {
            assetPaymentDetail.refreshReferenceObject(CamsPropertyConstants.AssetPaymentDetail.OBJECT_CODE);
            if (ObjectUtils.isNull(assetPaymentDetail.getObjectCode())) {
                return false;
            }
            objectSubTypeList.add(assetPaymentDetail.getObjectCode().getFinancialObjectSubTypeCode());
        }

        if (!SpringContext.getBean(AssetService.class).isObjectSubTypeCompatible(objectSubTypeList)) {
            return true;
        }

        List<AssetPaymentAssetDetail> assetPaymentAssetDetails = document.getAssetPaymentAssetDetail();
        for (AssetPaymentAssetDetail assetPaymentAssetDetail : assetPaymentAssetDetails) {
            assetPaymentAssetDetail.getAsset().refreshReferenceObject(CamsPropertyConstants.Asset.ASSET_PAYMENTS);
            List<AssetPayment> assetPayments = assetPaymentAssetDetail.getAsset().getAssetPayments();

            // Comparing against the already approved asset payments
            if (!assetPayments.isEmpty()) {
                for (AssetPayment assetPayment : assetPayments) {
                    String paymentSubObjectType = assetPayment.getFinancialObject().getFinancialObjectSubTypeCode();

                    validObjectSubTypes = new ArrayList<String>();
                    for (String subType : subTypes) {
                        validObjectSubTypes = Arrays.asList(StringUtils.split(subType, ","));
                        if (validObjectSubTypes.contains(paymentSubObjectType)) {
                            break;
                        }
                        validObjectSubTypes = new ArrayList<String>();
                    }
                    if (validObjectSubTypes.isEmpty())
                        validObjectSubTypes.add(paymentSubObjectType);

                    // Comparing the same asset payment document
                    for (AssetPaymentDetail assetPaymentDetail : assetPaymentDetails) {
                        if (!validObjectSubTypes.contains(assetPaymentDetail.getObjectCode().getFinancialObjectSubTypeCode())) {
                            // Differences where found.
                            return true;
                        }
                    }
                }
            }
        }
        // If none object sub types are different...
        return false;
    }

    public void setBusinessObjectService(BusinessObjectService businessObjectService) {
        this.businessObjectService = businessObjectService;
    }

    public BusinessObjectService getBusinessObjectService() {
        return businessObjectService;
    }

    public ParameterService getParameterService() {
        return parameterService;
    }


    public void setParameterService(ParameterService parameterService) {
        this.parameterService = parameterService;
    }


    public AssetPaymentDao getAssetPaymentDao() {
        return assetPaymentDao;
    }


    public void setAssetPaymentDao(AssetPaymentDao assetPaymentDao) {
        this.assetPaymentDao = assetPaymentDao;
    }

    public ObjectCodeService getObjectCodeService() {
        return objectCodeService;
    }

    public void setObjectCodeService(ObjectCodeService objectCodeService) {
        this.objectCodeService = objectCodeService;
    }

    public UniversityDateService getUniversityDateService() {
        return universityDateService;
    }

    public void setUniversityDateService(UniversityDateService universityDateService) {
        this.universityDateService = universityDateService;
    }

    public AssetRetirementService getAssetRetirementService() {
        return assetRetirementService;
    }

    public void setAssetRetirementService(AssetRetirementService assetRetirementService) {
        this.assetRetirementService = assetRetirementService;
    }

    public AssetService getAssetService() {
        return assetService;
    }

    public void setAssetService(AssetService assetService) {
        this.assetService = assetService;
    }

    /**
     * @see org.kuali.kfs.module.cam.document.service.AssetPaymentService#getAssetDistributionTypeColumnName(java.lang.String)
     */
    public AssetPaymentAllocationType getAssetDistributionType(String distributionCode) {
        HashMap<String, String> keys = new HashMap<String, String>();
        keys.put("distributionCode", distributionCode);
        AssetPaymentAllocationType d = (AssetPaymentAllocationType) getBusinessObjectService().findByPrimaryKey(AssetPaymentAllocationType.class, keys);
        return d;
    }

}
