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

import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.coreservice.framework.parameter.ParameterService;
import org.kuali.kfs.kns.document.MaintenanceDocument;
import org.kuali.kfs.krad.bo.DocumentHeader;
import org.kuali.kfs.krad.bo.Note;
import org.kuali.kfs.krad.bo.PersistableBusinessObject;
import org.kuali.kfs.krad.document.Document;
import org.kuali.kfs.krad.maintenance.MaintenanceLock;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.krad.service.DocumentService;
import org.kuali.kfs.krad.service.KRADServiceLocator;
import org.kuali.kfs.krad.service.NoteService;
import org.kuali.kfs.krad.util.GlobalVariables;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.document.FinancialSystemMaintainable;
import org.kuali.kfs.vnd.VendorConstants;
import org.kuali.kfs.vnd.VendorKeyConstants;
import org.kuali.kfs.vnd.VendorParameterConstants;
import org.kuali.kfs.vnd.VendorPropertyConstants;
import org.kuali.kfs.vnd.VendorUtils;
import org.kuali.kfs.vnd.businessobject.VendorDetail;
import org.kuali.kfs.vnd.businessobject.VendorHeader;
import org.kuali.kfs.vnd.businessobject.VendorTaxChange;
import org.kuali.kfs.vnd.document.service.VendorService;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.kew.api.WorkflowDocument;
import org.kuali.rice.kim.api.identity.Person;
import org.kuali.rice.kim.api.services.KimApiServiceLocator;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class VendorMaintainableImpl extends FinancialSystemMaintainable {
    protected static final String VENDOR_REQUIRES_APPROVAL_SPLIT_NODE = "RequiresApproval";
    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(VendorMaintainableImpl.class);

    /**
     * @see org.kuali.rice.kns.maintenance.KualiMaintainableImpl#setGenerateDefaultValues(boolean)
     */
    @Override
    public void setGenerateDefaultValues(String docTypeName) {
        super.setGenerateDefaultValues(docTypeName);

        List<Note> notes = new ArrayList<Note>();

        if (getBusinessObject().getObjectId() != null) {
            NoteService noteService = KRADServiceLocator.getNoteService();
            notes = noteService.getByRemoteObjectId(this.getBusinessObject().getObjectId());

            if (notes.isEmpty()) {
                notes.add(getNewBoNoteForAdding(VendorConstants.VendorCreateAndUpdateNotePrefixes.ADD));
            }
        }
    }

    /**
     * Overrides the kuali default documents title with a Vendor-specific document title style
     *
     * @see org.kuali.rice.kns.maintenance.KualiMaintainableImpl#getDocumentTitle(org.kuali.rice.kns.document.MaintenanceDocument)
     */
    @Override
    public String getDocumentTitle(MaintenanceDocument document) {
        String documentTitle = "";
        // Check if we are choosing to override the Kuali default document title.
        if (SpringContext.getBean(ParameterService.class).getParameterValueAsBoolean(VendorDetail.class, VendorParameterConstants.OVERRIDE_VENDOR_DOC_TITLE)) {
            // We are overriding the standard with a Vendor-specific document title style.
            if (document.isOldBusinessObjectInDocument()) {
                documentTitle = "Edit Vendor - ";
            } else {
                documentTitle = "New Vendor - ";
            }

            try {
                Person initUser = KimApiServiceLocator.getPersonService().getPerson(document.getDocumentHeader().getWorkflowDocument().getInitiatorPrincipalId());
                documentTitle += initUser.getCampusCode();
            } catch (Exception e) {
                throw new RuntimeException("Document Initiator not found " + e.getMessage());
            }

            VendorDetail newBo = (VendorDetail) document.getNewMaintainableObject().getBusinessObject();

            if (StringUtils.isNotBlank(newBo.getVendorName())) {
                documentTitle += " '" + newBo.getVendorName() + "'";
            } else {
                if (StringUtils.isNotBlank(newBo.getVendorFirstName())) {
                    documentTitle += " '" + newBo.getVendorFirstName() + " ";
                    if (StringUtils.isBlank(newBo.getVendorLastName())) {
                        documentTitle += "'";
                    }
                }

                if (StringUtils.isNotBlank(newBo.getVendorLastName())) {
                    if (StringUtils.isBlank(newBo.getVendorFirstName())) {
                        documentTitle += " '";
                    }
                    documentTitle += newBo.getVendorLastName() + "'";
                }
            }

            if (newBo.getVendorHeader().getVendorForeignIndicator()) {
                documentTitle += " (F)";
            }

            if (!newBo.isVendorParentIndicator()) {
                documentTitle += " (D)";
            }
        } else { // We are using the Kuali default document title.
            documentTitle = super.getDocumentTitle(document);
        }
        return documentTitle;
    }

    @Override
    public void doRouteStatusChange(DocumentHeader header) {
        super.doRouteStatusChange(header);
        VendorDetail vendorDetail = (VendorDetail) getBusinessObject();
        WorkflowDocument workflowDoc = header.getWorkflowDocument();

        // This code is only executed when the final approval occurs
        if (workflowDoc.isProcessed()) {
            // This id and versionNumber null check is needed here since those fields are always null for a fresh maintenance doc.
            if (vendorDetail.isVendorParentIndicator() && vendorDetail.getVendorHeaderGeneratedIdentifier() != null) {
                VendorDetail previousParent = SpringContext.getBean(VendorService.class).getParentVendor(vendorDetail.getVendorHeaderGeneratedIdentifier());
                // We'll only need to do the following if the previousParent is not the same as the current vendorDetail, because
                // the following lines are for vendor parent indicator changes.
                if (vendorDetail.getVendorDetailAssignedIdentifier() == null ||
                    previousParent.getVendorHeaderGeneratedIdentifier().intValue() != vendorDetail.getVendorHeaderGeneratedIdentifier().intValue() ||
                    previousParent.getVendorDetailAssignedIdentifier().intValue() != vendorDetail.getVendorDetailAssignedIdentifier().intValue()) {
                    previousParent.setVendorParentIndicator(false);
                    addNoteForParentIndicatorChange(vendorDetail, previousParent, header.getDocumentNumber());
                    SpringContext.getBean(BusinessObjectService.class).save(previousParent);
                }
            }

            // If this is a pre-existing parent vendor, and if the Tax Number or the Tax Type Code will change, log the change in the
            // Tax Change table.
            if (vendorDetail.isVendorParentIndicator()) {
                VendorDetail oldVendorDetail = SpringContext.getBean(VendorService.class).getVendorDetail(vendorDetail.getVendorHeaderGeneratedIdentifier(), vendorDetail.getVendorDetailAssignedIdentifier());
                if (ObjectUtils.isNotNull(oldVendorDetail)) {
                    VendorHeader oldVendorHeader = oldVendorDetail.getVendorHeader();
                    VendorHeader newVendorHeader = vendorDetail.getVendorHeader();

                    if (ObjectUtils.isNotNull(oldVendorHeader)) { // Does not apply if this is a new parent vendor.
                        String oldVendorTaxNumber = oldVendorHeader.getVendorTaxNumber();
                        String oldVendorTaxTypeCode = oldVendorHeader.getVendorTaxTypeCode();

                        String vendorTaxNumber = newVendorHeader.getVendorTaxNumber();
                        String vendorTaxTypeCode = newVendorHeader.getVendorTaxTypeCode();

                        if ((!StringUtils.equals(vendorTaxNumber, oldVendorTaxNumber)) || (!StringUtils.equals(vendorTaxTypeCode, oldVendorTaxTypeCode))) {
                            VendorTaxChange taxChange = new VendorTaxChange(vendorDetail.getVendorHeaderGeneratedIdentifier(), SpringContext.getBean(DateTimeService.class).getCurrentTimestamp(), oldVendorTaxNumber, oldVendorTaxTypeCode, GlobalVariables.getUserSession().getPerson().getPrincipalId());
                            SpringContext.getBean(BusinessObjectService.class).save(taxChange);
                        }
                    }
                }
            }

        }//endif isProcessed()
    }

    /**
     * Add a note to the previous parent vendor to denote that parent vendor indicator change had occurred.
     *
     * @param newVendorDetail     The current vendor
     * @param oldVendorDetail     The parent vendor of the current vendor prior to this change.
     * @param getDocumentNumber() The document number of the document where we're attempting the parent vendor indicator change.
     */
    private void addNoteForParentIndicatorChange(VendorDetail newVendorDetail, VendorDetail oldVendorDetail, String docNumber) {
        String noteText = VendorUtils.buildMessageText(VendorKeyConstants.MESSAGE_VENDOR_PARENT_TO_DIVISION, docNumber, newVendorDetail.getVendorName() + " (" + newVendorDetail.getVendorNumber() + ")");
        Note newBONote = new Note();
        newBONote.setNoteText(noteText);
        try {
            NoteService noteService = SpringContext.getBean(NoteService.class);
            newBONote = noteService.createNote(newBONote, oldVendorDetail, GlobalVariables.getUserSession().getPrincipalId());
            newBONote.setNotePostedTimestampToCurrent();

            noteService.save(newBONote);
        } catch (Exception e) {
            throw new RuntimeException("Caught Exception While Trying To Add Note to Vendor", e);
        }

        NoteService noteService = KRADServiceLocator.getNoteService();
        List<Note> notes = noteService.getByRemoteObjectId(oldVendorDetail.getObjectId());
        notes.add(newBONote);
    }

    /**
     * Refreshes the vendorDetail. Currently we need this mainly for refreshing the soldToVendor object after returning from the
     * lookup for a sold to vendor.
     *
     * @see org.kuali.rice.kns.maintenance.KualiMaintainableImpl#refresh(java.lang.String, java.util.Map,
     * org.kuali.rice.kns.document.MaintenanceDocument)
     */
    @Override
    public void refresh(String refreshCaller, Map fieldValues, MaintenanceDocument document) {
        PersistableBusinessObject oldBo = document.getOldMaintainableObject().getBusinessObject();
        if (ObjectUtils.isNotNull(oldBo)) {
            oldBo.refreshNonUpdateableReferences();
        }
        VendorDetail newBo = (VendorDetail) document.getNewMaintainableObject().getBusinessObject();
        // Here we have to temporarily save vendorHeader into a temp object, then put back
        // the vendorHeader into the newBo after the refresh, so that we don't lose the
        // values
        VendorHeader tempHeader = newBo.getVendorHeader();
        newBo.refreshNonUpdateableReferences();
        newBo.setVendorHeader(tempHeader);
        super.refresh(refreshCaller, fieldValues, document);
    }

    /**
     * Temporarily saves vendorHeader into a temp object, then put back the vendorHeader into the VendorDetail after the refresh, so
     * that we don't lose the values
     */
    public void refreshBusinessObject() {
        VendorDetail vd = (VendorDetail) getBusinessObject();
        // Here we have to temporarily save vendorHeader into a temp object, then put back
        // the vendorHeader into the VendorDetail after the refresh, so that we don't lose the
        // values
        VendorHeader tempHeader = vd.getVendorHeader();
        vd.refreshNonUpdateableReferences();
        vd.setVendorHeader(tempHeader);
    }


    /**
     * Checks whether the vendor has already had a vendor detail assigned id. If not, it will call the private method to set the
     * detail assigned id. The method will also call the vendorService to determine whether it should save the vendor header (i.e.
     * if this is a parent) and will save the vendor header accordingly. This is because we are not going to save vendor header
     * automatically along with the saving of vendor detail, so if the vendor is a parent, we have to save the vendor header
     * separately. Restriction-related information will be changed based on whether the Vendor Restricted Indicator was changed. If
     * the Tax Number or Tax Type code have changed, the fact will be recorded with a new record in the Tax Change table. Finally
     * the method will call the saveBusinessObject( ) of the super class to save the vendor detail.
     *
     * @see org.kuali.rice.kns.maintenance.KualiMaintainableImpl#saveBusinessObject()
     */
    @Override
    public void saveBusinessObject() {
        VendorDetail vendorDetail = (VendorDetail) super.getBusinessObject();
        VendorHeader vendorHeader = vendorDetail.getVendorHeader();

        // Update miscellaneous information and save the Vendor Header if this is a parent vendor.
        setVendorName(vendorDetail);
        vendorHeader.setVendorHeaderGeneratedIdentifier(vendorDetail.getVendorHeaderGeneratedIdentifier());
        if (ObjectUtils.isNull(vendorDetail.getVendorDetailAssignedIdentifier())) {
            setDetailAssignedId(vendorDetail);
        }
        if (vendorDetail.isVendorParentIndicator()) {
            SpringContext.getBean(VendorService.class).saveVendorHeader(vendorDetail);
        }
        super.saveBusinessObject();
        // Populate generateid and detail assigned id in doc content after bo is persisted.

        try {
            Document document = SpringContext.getBean(DocumentService.class).getByDocumentHeaderId(getDocumentNumber());
            VendorDetail vndDetail = (VendorDetail) ((MaintenanceDocument) document).getNewMaintainableObject().getBusinessObject();
            if (vndDetail.getVendorHeaderGeneratedIdentifier() == null
                || KFSConstants.MAINTENANCE_NEWWITHEXISTING_ACTION.equals(getMaintenanceAction())) {
                ((MaintenanceDocument) document).getNewMaintainableObject().setBusinessObject(vendorDetail);
                SpringContext.getBean(DocumentService.class).saveDocument(document);
            }
        } catch (Exception e) {
            LOG.error("Vendor doc not saved successfully " + e.getMessage());
        }
    }

    /**
     * @see org.kuali.rice.kns.maintenance.KualiMaintainableImpl#processAfterEdit()
     */
    @Override
    public void processAfterEdit(MaintenanceDocument document, Map<String, String[]> parameters) {

        List<Note> notes = new ArrayList<Note>();
        if (document.getOldMaintainableObject().getBusinessObject().getObjectId() != null) {
            NoteService noteService = KRADServiceLocator.getNoteService();
            notes = noteService.getByRemoteObjectId(this.getBusinessObject().getObjectId());
        }

        setVendorCreateAndUpdateNote(notes, VendorConstants.VendorCreateAndUpdateNotePrefixes.CHANGE);
        document.setNotes(notes);

        super.processAfterEdit(document, parameters);
    }

    /**
     * Checks whether the previous note was an "Add" with the same document number as this one
     *
     * @param notes  List of exisiting notes.
     * @param prefix String to determine if it is a note "Add" or a note "Change"
     */
    private void setVendorCreateAndUpdateNote(List<Note> notes, String prefix) {
        boolean shouldAddNote = true;

        if (prefix.equals(VendorConstants.VendorCreateAndUpdateNotePrefixes.CHANGE)) {
            // Check whether the previous note was an "Add" with the same document number as this one
            if (!notes.isEmpty()) {
                Note previousNote = notes.get(notes.size() - 1);
                if (previousNote.getNoteText().contains(getDocumentNumber())) {
                    shouldAddNote = false;
                }
            }
        }
        if (shouldAddNote) {
            notes.add(getNewBoNoteForAdding(prefix));
        }
    }

    /**
     * creates a new bo note and sets the timestamp.
     *
     * @return a newly created note
     */
    protected Note getNewBoNoteForAdding(String prefix) {
        Note newBoNote = new Note();
        newBoNote.setNoteText(prefix + " vendor document ID " + getDocumentNumber());
        newBoNote.setNotePostedTimestampToCurrent();

        try {
            newBoNote = SpringContext.getBean(NoteService.class).createNote(newBoNote, this.getBusinessObject(), GlobalVariables.getUserSession().getPrincipalId());
        } catch (Exception e) {
            throw new RuntimeException("Caught Exception While Trying To Add Note to Vendor", e);
        }

        return newBoNote;
    }

    /**
     * Concatenates the vendorLastName and a delimiter and the vendorFirstName fields into vendorName field of the vendorDetail
     * object.
     *
     * @param vendorDetail VendorDetail The vendor whose name field we are trying to assign
     */
    private void setVendorName(VendorDetail vendorDetail) {
        if (vendorDetail.isVendorFirstLastNameIndicator()) {
            vendorDetail.setVendorName(vendorDetail.getVendorLastName() + VendorConstants.NAME_DELIM + vendorDetail.getVendorFirstName());
        }
    }

    /**
     * If the vendorFirstLastNameIndicator is true, this method will set the vendor first name and vendor last name fields from the
     * vendorName field, then set the vendorName field to null. Then it sets the businessObject of this maintainable to the
     * VendorDetail object that contains our modification to the name fields.
     *
     * @see org.kuali.rice.kns.maintenance.Maintainable#saveBusinessObject()
     */
    @Override
    public void setBusinessObject(PersistableBusinessObject bo) {
        VendorDetail originalBo = (VendorDetail) bo;

        String vendorName = originalBo.getVendorName();
        if (originalBo.isVendorFirstLastNameIndicator() && ObjectUtils.isNotNull(vendorName)) {
            int start = vendorName.indexOf(VendorConstants.NAME_DELIM);
            if (start >= 0) {
                String lastName = vendorName.substring(0, start);
                String firstName = new String();
                if (start + VendorConstants.NAME_DELIM.length() <= vendorName.length()) {
                    firstName = vendorName.substring(start + VendorConstants.NAME_DELIM.length(), vendorName.length());
                }

                originalBo.setVendorFirstName((ObjectUtils.isNotNull(firstName) ? firstName.trim() : firstName));
                originalBo.setVendorLastName((ObjectUtils.isNotNull(lastName) ? lastName.trim() : lastName));
                originalBo.setVendorName(null);
            }
        }

        super.setBusinessObject(originalBo);
    }

    /**
     * Sets a valid detail assigned id to a vendor if the vendor has not had a detail assigned id yet. If this is a new parent whose
     * header id is also null, this method will assign 0 as the detail assigned id. If this is a new division vendor, it will look
     * for the count of vendor details in the database whose vendor header id match with the vendor header id of this new division,
     * then look for the count of vendor details in the database, in a while loop, to find if a vendor detail with the same header
     * id and detail id as the count has existed. If a vendor with such criteria exists, this method will increment the count
     * by 1 and look up in the database again. If it does not exist, assign the count as the vendor detail id and change the
     * boolean flag to stop the loop, because we have already found the valid detail assigned id that we were looking for
     *
     * @param vendorDetail VendorDetail The vendor whose detail assigned id we're trying to assign.
     */
    private void setDetailAssignedId(VendorDetail vendorDetail) {
        // If this is a new parent, let's set the detail id to 0.
        if (ObjectUtils.isNull(vendorDetail.getVendorHeaderGeneratedIdentifier())) {
            vendorDetail.setVendorDetailAssignedIdentifier(new Integer(0));
        } else {
            // Try to get the count of all the vendor whose header id is the same as this header id.
            Map criterias = new HashMap();
            criterias.put(VendorPropertyConstants.VENDOR_HEADER_GENERATED_ID, vendorDetail.getVendorHeaderGeneratedIdentifier());
            BusinessObjectService boService = SpringContext.getBean(BusinessObjectService.class);
            int count = boService.countMatching(VendorDetail.class, criterias);
            boolean validId = false;
            while (!validId) {
                criterias.put(VendorPropertyConstants.VENDOR_DETAIL_ASSIGNED_ID, count);
                int result = boService.countMatching(VendorDetail.class, criterias);
                if (result > 0) {
                    // increment the detail id by 1
                    count++;
                } else {
                    // count is a validId, so we'll use count as our vendor detail assigned id
                    validId = true;
                    vendorDetail.setVendorDetailAssignedIdentifier(new Integer(count));
                }
            }
        }
    }

    /**
     * Returns the locking representation of the vendor. If the vendor detail id is not null, call the super class
     * implementation of generateMaintenanceLocks which will set the locking key to be the header and detail ids. However, if the
     * detail id is null, that means this is a new vendor (parent or division) and we should ignore locking.
     *
     * @see org.kuali.rice.kns.maintenance.Maintainable#generateMaintenanceLocks()
     */
    @Override
    public List<MaintenanceLock> generateMaintenanceLocks() {
        if (ObjectUtils.isNotNull(((VendorDetail) getBusinessObject()).getVendorDetailAssignedIdentifier())) {
            return super.generateMaintenanceLocks();
        } else {
            return new ArrayList();
        }
    }

    /**
     * Create a new division vendor if the user clicks on the "Create a new division" link. By default, the vendorParentIndicator is
     * set to true in the constructor of VendorDetail, but if we're creating a new division, it's not a parent, so we need to set
     * the vendorParentIndicator to false in this case.
     *
     * @see org.kuali.rice.kns.maintenance.Maintainable#setupNewFromExisting()
     */
    @Override
    public void setupNewFromExisting(MaintenanceDocument document, Map<String, String[]> parameters) {
        super.setupNewFromExisting(document, parameters);

        ((VendorDetail) super.getBusinessObject()).setVendorParentIndicator(false);
        ((VendorDetail) super.getBusinessObject()).setActiveIndicator(true);

        List<Note> notes = new ArrayList<Note>();

        if (getBusinessObject().getObjectId() != null) {
            NoteService noteService = KRADServiceLocator.getNoteService();
            notes = noteService.getByRemoteObjectId(this.getBusinessObject().getObjectId());
        }

        setVendorCreateAndUpdateNote(notes, VendorConstants.VendorCreateAndUpdateNotePrefixes.ADD);

        document.setNotes(notes);
    }

    /**
     * @see org.kuali.rice.kns.maintenance.KualiMaintainableImpl#isRelationshipRefreshable(java.lang.Class, java.lang.String)
     */
    @Override
    protected boolean isRelationshipRefreshable(Class boClass, String relationshipName) {
        if (VendorDetail.class.isAssignableFrom(boClass) && VendorConstants.VENDOR_HEADER_ATTR.equals(relationshipName)) {
            return false;
        }
        return super.isRelationshipRefreshable(boClass, relationshipName);
    }

    /**
     * @see org.kuali.kfs.sys.document.FinancialSystemMaintainable#answerSplitNodeQuestion(java.lang.String)
     */
    @Override
    protected boolean answerSplitNodeQuestion(String nodeName) throws UnsupportedOperationException {
        if (nodeName.equals(VENDOR_REQUIRES_APPROVAL_SPLIT_NODE)) {
            return SpringContext.getBean(VendorService.class).shouldVendorRouteForApproval(getDocumentNumber());
        }
        return super.answerSplitNodeQuestion(nodeName);
    }
}

