/*
 * 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.ar.document;

import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.coa.businessobject.Account;
import org.kuali.kfs.coa.businessobject.Chart;
import org.kuali.kfs.coa.businessobject.ObjectCode;
import org.kuali.kfs.coa.businessobject.ProjectCode;
import org.kuali.kfs.coa.businessobject.SubAccount;
import org.kuali.kfs.coa.businessobject.SubObjectCode;
import org.kuali.kfs.coreservice.framework.parameter.ParameterService;
import org.kuali.kfs.krad.bo.Note;
import org.kuali.kfs.krad.exception.ValidationException;
import org.kuali.kfs.krad.rules.rule.event.KualiDocumentEvent;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.module.ar.ArConstants;
import org.kuali.kfs.module.ar.businessobject.AccountsReceivableDocumentHeader;
import org.kuali.kfs.module.ar.businessobject.CustomerInvoiceDetail;
import org.kuali.kfs.module.ar.businessobject.ReceivableCustomerInvoiceDetail;
import org.kuali.kfs.module.ar.businessobject.SalesTaxCustomerInvoiceDetail;
import org.kuali.kfs.module.ar.businessobject.WriteoffCustomerInvoiceDetail;
import org.kuali.kfs.module.ar.businessobject.WriteoffTaxCustomerInvoiceDetail;
import org.kuali.kfs.module.ar.document.service.AccountsReceivablePendingEntryService;
import org.kuali.kfs.module.ar.document.service.AccountsReceivableTaxService;
import org.kuali.kfs.module.ar.document.service.CustomerInvoiceWriteoffDocumentService;
import org.kuali.kfs.module.ar.document.service.CustomerService;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntry;
import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySequenceHelper;
import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySourceDetail;
import org.kuali.kfs.sys.businessobject.TaxDetail;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.document.AmountTotaling;
import org.kuali.kfs.sys.document.GeneralLedgerPendingEntrySource;
import org.kuali.kfs.sys.document.GeneralLedgerPostingDocumentBase;
import org.kuali.kfs.sys.service.GeneralLedgerPendingEntryService;
import org.kuali.kfs.sys.service.TaxService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange;

import java.sql.Date;
import java.util.ArrayList;
import java.util.List;

public class CustomerInvoiceWriteoffDocument extends GeneralLedgerPostingDocumentBase implements GeneralLedgerPendingEntrySource, AmountTotaling {

    protected static final String REQUIRES_APPROVAL_NODE = "RequiresApproval";
    protected String chartOfAccountsCode;
    protected String accountNumber;
    protected String subAccountNumber;
    protected String financialObjectCode;
    protected String financialSubObjectCode;
    protected String projectCode;
    protected String organizationReferenceIdentifier;
    protected String financialDocumentReferenceInvoiceNumber;
    protected String statusCode;

    protected String customerNote;

    protected Account account;
    protected Chart chartOfAccounts;
    protected SubAccount subAccount;
    protected ObjectCode financialObject;
    protected SubObjectCode financialSubObject;
    protected ProjectCode project;
    protected CustomerInvoiceDocument customerInvoiceDocument;
    protected AccountsReceivableDocumentHeader accountsReceivableDocumentHeader;
    protected KualiDecimal invoiceWriteoffAmount;

    // GLPEs from invoice to be written off - used for collecting tax amounts that should be written off
    protected class WriteOffGlpes {
        GeneralLedgerPendingEntry glpe;
        boolean isWriteOffDetail;

        public WriteOffGlpes(GeneralLedgerPendingEntry pe, boolean writeOff) {
            this.glpe = pe;
            this.isWriteOffDetail = writeOff;
        }
    }

    public AccountsReceivableDocumentHeader getAccountsReceivableDocumentHeader() {
        return accountsReceivableDocumentHeader;
    }

    public void setAccountsReceivableDocumentHeader(AccountsReceivableDocumentHeader accountsReceivableDocumentHeader) {
        this.accountsReceivableDocumentHeader = accountsReceivableDocumentHeader;
    }

    public String getChartOfAccountsCode() {
        return chartOfAccountsCode;
    }

    public void setChartOfAccountsCode(String chartOfAccountsCode) {
        this.chartOfAccountsCode = chartOfAccountsCode;
    }

    public String getAccountNumber() {
        return accountNumber;
    }

    public void setAccountNumber(String accountNumber) {
        this.accountNumber = accountNumber;
    }

    public String getSubAccountNumber() {
        return subAccountNumber;
    }

    public void setSubAccountNumber(String subAccountNumber) {
        this.subAccountNumber = subAccountNumber;
    }

    public String getFinancialObjectCode() {
        return financialObjectCode;
    }

    public void setFinancialObjectCode(String financialObjectCode) {
        this.financialObjectCode = financialObjectCode;
    }

    public String getFinancialSubObjectCode() {
        return financialSubObjectCode;
    }

    public void setFinancialSubObjectCode(String financialSubObjectCode) {
        this.financialSubObjectCode = financialSubObjectCode;
    }

    public String getProjectCode() {
        return projectCode;
    }

    public void setProjectCode(String projectCode) {
        this.projectCode = projectCode;
    }

    public String getOrganizationReferenceIdentifier() {
        return organizationReferenceIdentifier;
    }

    public void setOrganizationReferenceIdentifier(String organizationReferenceIdentifier) {
        this.organizationReferenceIdentifier = organizationReferenceIdentifier;
    }

    public String getFinancialDocumentReferenceInvoiceNumber() {
        return financialDocumentReferenceInvoiceNumber;
    }

    public void setFinancialDocumentReferenceInvoiceNumber(String financialDocumentReferenceInvoiceNumber) {
        this.financialDocumentReferenceInvoiceNumber = financialDocumentReferenceInvoiceNumber;
    }

    public Account getAccount() {
        return account;
    }

    public void setAccount(Account account) {
        this.account = account;
    }

    public Chart getChartOfAccounts() {
        return chartOfAccounts;
    }

    public void setChartOfAccounts(Chart chartOfAccounts) {
        this.chartOfAccounts = chartOfAccounts;
    }

    public SubAccount getSubAccount() {
        return subAccount;
    }

    public void setSubAccount(SubAccount subAccount) {
        this.subAccount = subAccount;
    }

    public ObjectCode getFinancialObject() {
        return financialObject;
    }

    public void setFinancialObject(ObjectCode financialObject) {
        this.financialObject = financialObject;
    }

    public SubObjectCode getFinancialSubObject() {
        return financialSubObject;
    }

    public void setFinancialSubObject(SubObjectCode financialSubObject) {
        this.financialSubObject = financialSubObject;
    }

    public ProjectCode getProject() {
        return project;
    }

    public void setProject(ProjectCode project) {
        this.project = project;
    }

    public CustomerInvoiceDocument getCustomerInvoiceDocument() {
        if (ObjectUtils.isNull(customerInvoiceDocument) && StringUtils.isNotEmpty(financialDocumentReferenceInvoiceNumber)) {
            refreshReferenceObject("customerInvoiceDocument");
        }

        return customerInvoiceDocument;
    }

    public void setCustomerInvoiceDocument(CustomerInvoiceDocument customerInvoiceDocument) {
        this.customerInvoiceDocument = customerInvoiceDocument;
    }

    /**
     * This method returns all the applicable invoice details for writeoff. This method also sets the writeoff document number on
     * each of the invoice details for making retrieval of the actual writeoff amount easier.
     *
     * @return
     */
    public List<CustomerInvoiceDetail> getCustomerInvoiceDetailsForWriteoff() {
        List<CustomerInvoiceDetail> customerInvoiceDetailsForWriteoff = new ArrayList<CustomerInvoiceDetail>();
        for (CustomerInvoiceDetail customerInvoiceDetail : getCustomerInvoiceDocument().getCustomerInvoiceDetailsWithoutDiscounts()) {
            customerInvoiceDetail.setCustomerInvoiceWriteoffDocumentNumber(this.documentNumber);
            customerInvoiceDetailsForWriteoff.add(customerInvoiceDetail);
        }

        return customerInvoiceDetailsForWriteoff;
    }

    /**
     * This method returns the total amount to be written off
     *
     * @return
     */
    public KualiDecimal getInvoiceWriteoffAmount() {
        // only pull the invoice open amount as the invoice writeoff amount while the doc
        // is in play. once its been approved, rely on the amount stored in the db.
        if (!KFSConstants.DocumentStatusCodes.APPROVED.equals(getFinancialSystemDocumentHeader().getFinancialDocumentStatusCode())) {
            invoiceWriteoffAmount = customerInvoiceDocument.getOpenAmount();
        }
        return invoiceWriteoffAmount;
    }

    public void setInvoiceWriteoffAmount(KualiDecimal invoiceWriteoffAmount) {
        this.invoiceWriteoffAmount = invoiceWriteoffAmount;
    }

    public String getStatusCode() {
        return statusCode;
    }

    public void setStatusCode(String statusCode) {
        this.statusCode = statusCode;
    }

    /**
     * Initializes the values for a new document.
     */
    public void initiateDocument() {
        setStatusCode(ArConstants.CustomerInvoiceWriteoffStatuses.INITIATE);
    }

    /**
     * Clear out the initially populated fields.
     */
    public void clearInitFields() {
        setFinancialDocumentReferenceInvoiceNumber(null);
    }

    @Override
    public List<String> getWorkflowEngineDocumentIdsToLock() {
        if (StringUtils.isNotBlank(getFinancialDocumentReferenceInvoiceNumber())) {
            List<String> documentIds = new ArrayList<String>();
            documentIds.add(getFinancialDocumentReferenceInvoiceNumber());
            return documentIds;
        }
        return null;

    }

    /**
     * When document is processed do the following: 1) Apply amounts to writeoff invoice 2) Mark off invoice indiciator
     *
     * @see org.kuali.kfs.sys.document.GeneralLedgerPostingDocumentBase#doRouteStatusChange()
     */
    @Override
    public void doRouteStatusChange(DocumentRouteStatusChange statusChangeEvent) {
        super.doRouteStatusChange(statusChangeEvent);
        if (getDocumentHeader().getWorkflowDocument().isProcessed()) {
            CustomerInvoiceWriteoffDocumentService writeoffService = SpringContext.getBean(CustomerInvoiceWriteoffDocumentService.class);
            writeoffService.completeWriteoffProcess(this);
        }
    }

    /**
     * do all the calculations before the document gets saved gets called for 'Submit', 'Save', and 'Blanket Approved'
     *
     * @see org.kuali.rice.krad.document.Document#prepareForSave(org.kuali.rice.krad.rule.event.KualiDocumentEvent)
     */
    @Override
    public void prepareForSave(KualiDocumentEvent event) {
        // generate GLPEs
        if (!SpringContext.getBean(GeneralLedgerPendingEntryService.class).generateGeneralLedgerPendingEntries(this)) {
            logErrors();
            throw new ValidationException("general ledger GLPE generation failed");
        }
        super.prepareForSave(event);
    }

    @Override
    public boolean generateDocumentGeneralLedgerPendingEntries(GeneralLedgerPendingEntrySequenceHelper sequenceHelper) {
        return true;
    }

    /**
     * This method creates the following GLPE's for the customer invoice writeoff 1. C Receivable object code with remaining amount
     * 2. D Writeoff object code (or Writeoff FAU) with remaining amount 3. C Receivable object code for tax on state tax account 4.
     * D Sales Tax object code for tax on the state tax account 5. C Receivable object code for tax on district tax account 6. D
     * District tax object code for $1.00 on the district tax account
     *
     * @see org.kuali.kfs.service.impl.GenericGeneralLedgerPendingEntryGenerationProcessImpl#processGenerateGeneralLedgerPendingEntries(org.kuali.kfs.sys.document.GeneralLedgerPendingEntrySource,
     * org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySourceDetail,
     * org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySequenceHelper)
     */

    @Override
    public boolean generateGeneralLedgerPendingEntries(GeneralLedgerPendingEntrySourceDetail glpeSourceDetail, GeneralLedgerPendingEntrySequenceHelper sequenceHelper) {
        CustomerInvoiceDetail customerInvoiceDetail = (CustomerInvoiceDetail) glpeSourceDetail;

        // if invoice item open amount <= 0 -> do not generate GLPEs for this glpeSourceDetail
        if (!customerInvoiceDetail.getAmountOpen().isPositive()) {
            return true;
        }

        KualiDecimal amount;

        String writeoffTaxGenerationOption = SpringContext.getBean(ParameterService.class).getParameterValueAsString(CustomerInvoiceWriteoffDocument.class, ArConstants.ALLOW_SALES_TAX_LIABILITY_ADJUSTMENT_IND);
        boolean hasWriteoffTaxClaimOnCashOffset = ArConstants.ALLOW_SALES_TAX_LIABILITY_ADJUSTMENT_IND_NO.equals(writeoffTaxGenerationOption);

        // if sales tax is enabled generate tax GLPEs
        if (SpringContext.getBean(AccountsReceivableTaxService.class).isCustomerInvoiceDetailTaxable(getCustomerInvoiceDocument(), customerInvoiceDetail)) {

            amount = customerInvoiceDetail.getInvoiceItemPreTaxAmount();
            addReceivableGLPEs(sequenceHelper, glpeSourceDetail, true, amount);
            sequenceHelper.increment();
            addWriteoffGLPEs(sequenceHelper, glpeSourceDetail, true, amount);
            addSalesTaxGLPEs(sequenceHelper, glpeSourceDetail, true, amount);
        } else {
            amount = customerInvoiceDetail.getAmountOpen();
            addReceivableGLPEs(sequenceHelper, glpeSourceDetail, true, amount);
            sequenceHelper.increment();
            addWriteoffGLPEs(sequenceHelper, glpeSourceDetail, true, amount);
        }
        return true;
    }

    /**
     * This method creates the receivable GLPEs for customer invoice details using the remaining amount
     *
     * @param poster
     * @param sequenceHelper
     * @param postable
     * @param explicitEntry
     */
    protected void addReceivableGLPEs(GeneralLedgerPendingEntrySequenceHelper sequenceHelper, GeneralLedgerPendingEntrySourceDetail glpeSourceDetail, boolean hasClaimOnCashOffset, KualiDecimal amount) {

        CustomerInvoiceDetail customerInvoiceDetail = (CustomerInvoiceDetail) glpeSourceDetail;
        ReceivableCustomerInvoiceDetail receivableCustomerInvoiceDetail = new ReceivableCustomerInvoiceDetail(customerInvoiceDetail, getCustomerInvoiceDocument());
        boolean isDebit = false;

        AccountsReceivablePendingEntryService service = SpringContext.getBean(AccountsReceivablePendingEntryService.class);
        service.createAndAddGenericInvoiceRelatedGLPEs(this, receivableCustomerInvoiceDetail, sequenceHelper, isDebit, hasClaimOnCashOffset, amount);
    }

    /**
     * This method adds writeoff GLPE's for the customer invoice details using the remaining amount.
     *
     * @param poster
     * @param sequenceHelper
     * @param postable
     * @param explicitEntry
     */
    protected void addWriteoffGLPEs(GeneralLedgerPendingEntrySequenceHelper sequenceHelper, GeneralLedgerPendingEntrySourceDetail glpeSourceDetail, boolean hasClaimOnCashOffset, KualiDecimal amount) {

        CustomerInvoiceDetail customerInvoiceDetail = (CustomerInvoiceDetail) glpeSourceDetail;
        WriteoffCustomerInvoiceDetail writeoffCustomerInvoiceDetail = new WriteoffCustomerInvoiceDetail(customerInvoiceDetail, this);
        boolean isDebit = true;

        AccountsReceivablePendingEntryService service = SpringContext.getBean(AccountsReceivablePendingEntryService.class);
        service.createAndAddGenericInvoiceRelatedGLPEs(this, writeoffCustomerInvoiceDetail, sequenceHelper, isDebit, hasClaimOnCashOffset, amount);
    }

    protected void addSalesTaxGLPEs(GeneralLedgerPendingEntrySequenceHelper sequenceHelper, GeneralLedgerPendingEntrySourceDetail glpeSourceDetail, boolean hasWriteoffTaxClaimOnCashOffset, KualiDecimal amount) {
        List<GeneralLedgerPendingEntry> invGlpes = getCustomerInvoiceDocument().getGeneralLedgerPendingEntries();
        CustomerInvoiceDetail customerInvoiceDetail = (CustomerInvoiceDetail) glpeSourceDetail;
        WriteoffCustomerInvoiceDetail writeoffCustomerInvoiceDetail = new WriteoffCustomerInvoiceDetail(customerInvoiceDetail, this);

        boolean isDebit = true;

        String postalCode = SpringContext.getBean(AccountsReceivableTaxService.class).getPostalCodeForTaxation(getCustomerInvoiceDocument());
        Date dateOfTransaction = getCustomerInvoiceDocument().getBillingDate();

        List<TaxDetail> salesTaxDetails = SpringContext.getBean(TaxService.class).getSalesTaxDetails(dateOfTransaction, postalCode, amount);

        AccountsReceivablePendingEntryService service = SpringContext.getBean(AccountsReceivablePendingEntryService.class);
        SalesTaxCustomerInvoiceDetail salesTaxCustomerInvoiceDetail;
        ReceivableCustomerInvoiceDetail receivableCustomerInvoiceDetail;
        WriteoffTaxCustomerInvoiceDetail writeoffTaxCustomerInvoiceDetail;

        for (TaxDetail salesTaxDetail : salesTaxDetails) {
            salesTaxCustomerInvoiceDetail = new SalesTaxCustomerInvoiceDetail(salesTaxDetail, customerInvoiceDetail);
            receivableCustomerInvoiceDetail = new ReceivableCustomerInvoiceDetail(salesTaxCustomerInvoiceDetail, getCustomerInvoiceDocument());
            writeoffTaxCustomerInvoiceDetail = new WriteoffTaxCustomerInvoiceDetail(salesTaxCustomerInvoiceDetail, this);

            CustomerInvoiceDetail customerInvDetail = (hasWriteoffTaxClaimOnCashOffset) ? writeoffTaxCustomerInvoiceDetail : salesTaxCustomerInvoiceDetail;
            List<WriteOffGlpes> newGlpes = findGeneralLedgerPendingEntryForDetail(invGlpes, receivableCustomerInvoiceDetail, salesTaxCustomerInvoiceDetail);

            for (WriteOffGlpes writeOffGlpe : newGlpes) {
                sequenceHelper.increment();
                if (writeOffGlpe.isWriteOffDetail) {
                    service.createAndAddGenericInvoiceRelatedGLPEs(this, customerInvDetail, sequenceHelper, isDebit, hasWriteoffTaxClaimOnCashOffset, writeOffGlpe.glpe.getTransactionLedgerEntryAmount());
                } else {
                    service.createAndAddGenericInvoiceRelatedGLPEs(this, receivableCustomerInvoiceDetail, sequenceHelper, !isDebit, hasWriteoffTaxClaimOnCashOffset, writeOffGlpe.glpe.getTransactionLedgerEntryAmount());
                }
                invGlpes.remove(writeOffGlpe.glpe);
            }

        }
    }


    /**
     * find all glpes that match this sales tax entry
     *
     * @param glpes
     * @param glpeSourceDetail
     * @param glpeWriteOffDetail
     * @return array returned of all matches glpes
     */
    protected List<WriteOffGlpes> findGeneralLedgerPendingEntryForDetail(List<GeneralLedgerPendingEntry> glpes, GeneralLedgerPendingEntrySourceDetail glpeSourceDetail, GeneralLedgerPendingEntrySourceDetail glpeWriteOffDetail) {
        int counter = 0;
        ArrayList<WriteOffGlpes> arrGlpeMatches = new ArrayList<WriteOffGlpes>();

        for (GeneralLedgerPendingEntry glpe : glpes) {
            if ((glpeSourceDetail.getAccountNumber().matches(glpe.getAccountNumber())) && (glpeSourceDetail.getChartOfAccountsCode().matches(glpe.getChartOfAccountsCode())) && (glpeSourceDetail.getObjectCode().getFinancialObjectCode().matches(glpe.getFinancialObjectCode()))) {
                arrGlpeMatches.add(new WriteOffGlpes(glpe, false));
            } else if ((glpeWriteOffDetail.getAccountNumber().matches(glpe.getAccountNumber())) && (glpeWriteOffDetail.getChartOfAccountsCode().matches(glpe.getChartOfAccountsCode())) && (glpeWriteOffDetail.getObjectCode().getFinancialObjectCode().matches(glpe.getFinancialObjectCode()))) {
                arrGlpeMatches.add(new WriteOffGlpes(glpe, true));
            }
        }
        return arrGlpeMatches;
    }

    @Override
    public KualiDecimal getGeneralLedgerPendingEntryAmountForDetail(GeneralLedgerPendingEntrySourceDetail glpeSourceDetail) {
        return null;
    }

    @Override
    public List<GeneralLedgerPendingEntrySourceDetail> getGeneralLedgerPendingEntrySourceDetails() {
        List<GeneralLedgerPendingEntrySourceDetail> generalLedgerPendingEntrySourceDetails = new ArrayList<GeneralLedgerPendingEntrySourceDetail>();
        generalLedgerPendingEntrySourceDetails.addAll(getCustomerInvoiceDocument().getCustomerInvoiceDetailsWithoutDiscounts());
        return generalLedgerPendingEntrySourceDetails;

    }

    @Override
    public boolean isDebit(GeneralLedgerPendingEntrySourceDetail postable) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public KualiDecimal getTotalDollarAmount() {
        return getInvoiceWriteoffAmount();
    }

    /**
     * Gets the customerNote attribute.
     *
     * @return Returns the customerNote.
     */
    public String getCustomerNote() {
        return customerNote;
    }

    /**
     * Sets the customerNote attribute value.
     *
     * @param customerNote The customerNote to set.
     */
    public void setCustomerNote(String customerNote) {
        this.customerNote = customerNote;
    }

    public void populateCustomerNote() {
        customerNote = "";
        CustomerService customerService = SpringContext.getBean(CustomerService.class);
        List<Note> boNotes = customerService.getCustomerNotes(this.getCustomerInvoiceDocument().getCustomer().getCustomerNumber());
        StringBuffer customerNotes = new StringBuffer();
        if (boNotes.size() > 0) {

            for (Note note : boNotes) {
                customerNotes.append(note.getNoteText().trim() + " ");
            }
            this.customerNote = customerNotes.toString().trim();
        }
    }

    /**
     * Answers true when invoice write off amount is greater than default approved amount ($50???)
     *
     * @see org.kuali.kfs.sys.document.FinancialSystemTransactionalDocumentBase#answerSplitNodeQuestion(java.lang.String)
     */
    @Override
    public boolean answerSplitNodeQuestion(String nodeName) throws UnsupportedOperationException {
        if (REQUIRES_APPROVAL_NODE.equals(nodeName)) {

            // grab the approval threshold from the param service
            ParameterService paramService = SpringContext.getBean(ParameterService.class);
            KualiDecimal approvalThreshold = new KualiDecimal(paramService.getParameterValueAsString(CustomerInvoiceWriteoffDocument.class, ArConstants.WRITEOFF_APPROVAL_THRESHOLD));

            return (approvalThreshold.isLessThan(getInvoiceWriteoffAmount()));
        }
        throw new UnsupportedOperationException("answerSplitNode('" + nodeName + "') called but no handler for this nodeName present.");
    }


}
