/*
 * 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.coa.service.impl;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.kuali.kfs.coa.businessobject.OrganizationReversion;
import org.kuali.kfs.coa.businessobject.OrganizationReversionCategory;
import org.kuali.kfs.coa.businessobject.OrganizationReversionDetail;
import org.kuali.kfs.coa.service.OrganizationReversionDetailTrickleDownInactivationService;
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.service.BusinessObjectService;
import org.kuali.kfs.krad.service.DocumentHeaderService;
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.KFSKeyConstants;
import org.kuali.rice.core.api.config.property.ConfigurationService;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * The default implementation of the OrganizationReversionDetailTrickleDownService
 */
public class OrganizationReversionDetailTrickleDownInactivationServiceImpl implements OrganizationReversionDetailTrickleDownInactivationService {
    private static final Logger LOG = Logger.getLogger(OrganizationReversionDetailTrickleDownInactivationServiceImpl.class);
    protected NoteService noteService;
    protected ConfigurationService kualiConfigurationService;
    protected BusinessObjectService businessObjectService;
    protected DocumentHeaderService documentHeaderService;

    /**
     * @see org.kuali.kfs.coa.service.OrganizationReversionDetailTrickleDownInactivationService#trickleDownInactiveOrganizationReversionDetails(org.kuali.kfs.coa.businessobject.OrganizationReversion, java.lang.String)
     */
    public void trickleDownInactiveOrganizationReversionDetails(OrganizationReversion organizationReversion, String documentNumber) {
        organizationReversion.refreshReferenceObject("organizationReversionDetail");
        trickleDownInactivations(organizationReversion.getOrganizationReversionDetail(), documentNumber);
    }

    /**
     * @see org.kuali.kfs.coa.service.OrganizationReversionDetailTrickleDownInactivationService#trickleDownInactiveOrganizationReversionDetails(org.kuali.kfs.coa.businessobject.OrganizationReversionCategory, java.lang.String)
     */
    public void trickleDownInactiveOrganizationReversionDetails(OrganizationReversionCategory organizationReversionCategory, String documentNumber) {
        Map<String, Object> fieldValues = new HashMap<String, Object>();
        fieldValues.put("organizationReversionCategoryCode", organizationReversionCategory.getOrganizationReversionCategoryCode());
        Collection orgReversionDetails = businessObjectService.findMatching(OrganizationReversionDetail.class, fieldValues);

        List<OrganizationReversionDetail> organizationReversionDetailList = new ArrayList<OrganizationReversionDetail>();
        for (Object orgRevDetailAsObject : orgReversionDetails) {
            organizationReversionDetailList.add((OrganizationReversionDetail) orgRevDetailAsObject);
        }
        trickleDownInactivations(organizationReversionDetailList, documentNumber);
    }

    /**
     * @see org.kuali.kfs.coa.service.OrganizationReversionDetailTrickleDownInactivationService#trickleDownActiveOrganizationReversionDetails(org.kuali.kfs.coa.businessobject.OrganizationReversion, java.lang.String)
     */
    public void trickleDownActiveOrganizationReversionDetails(OrganizationReversion organizationReversion, String documentNumber) {
        organizationReversion.refreshReferenceObject("organizationReversionDetail");
        trickleDownActivations(organizationReversion.getOrganizationReversionDetail(), documentNumber);
    }

    /**
     * @see org.kuali.kfs.coa.service.OrganizationReversionDetailTrickleDownInactivationService#trickleDownActiveOrganizationReversionDetails(org.kuali.kfs.coa.businessobject.OrganizationReversionCategory, java.lang.String)
     */
    public void trickleDownActiveOrganizationReversionDetails(OrganizationReversionCategory organizationReversionCategory, String documentNumber) {
        Map<String, Object> fieldValues = new HashMap<String, Object>();
        fieldValues.put("organizationReversionCategoryCode", organizationReversionCategory.getOrganizationReversionCategoryCode());
        Collection orgReversionDetails = businessObjectService.findMatching(OrganizationReversionDetail.class, fieldValues);

        List<OrganizationReversionDetail> organizationReversionDetailList = new ArrayList<OrganizationReversionDetail>();
        for (Object orgRevDetailAsObject : orgReversionDetails) {
            organizationReversionDetailList.add((OrganizationReversionDetail) orgRevDetailAsObject);
        }
        trickleDownActivations(organizationReversionDetailList, documentNumber);
    }

    /**
     * The method which actually does the work of inactivating the details
     *
     * @param organizationReversionDetails the details to inactivate
     * @param documentNumber               the document number which has the inactivations as part of it
     * @return an inactivation status object which will help us save notes
     */
    protected void trickleDownInactivations(List<OrganizationReversionDetail> organizationReversionDetails, String documentNumber) {
        TrickleDownStatus status = new TrickleDownStatus(KFSKeyConstants.ORGANIZATION_REVERSION_DETAIL_TRICKLE_DOWN_INACTIVATION, KFSKeyConstants.ORGANIZATION_REVERSION_DETAIL_TRICKLE_DOWN_INACTIVATION_ERROR_DURING_PERSISTENCE);

        if (!ObjectUtils.isNull(organizationReversionDetails) && !organizationReversionDetails.isEmpty()) {
            for (OrganizationReversionDetail detail : organizationReversionDetails) {
                if (detail.isActive()) {
                    detail.setActive(false);
                    try {
                        businessObjectService.save(detail);
                        status.addOrganizationReversionDetail(detail);
                    } catch (RuntimeException re) {
                        LOG.error("Unable to trickle-down inactivate sub-account " + detail.toString(), re);
                        status.addErrorPersistingOrganizationReversionDetail(detail);
                    }
                }
            }
        }

        status.saveSuccesfullyChangedNotes(documentNumber);
        status.saveErrorNotes(documentNumber);
    }

    /**
     * The method which actually does the work of activating the details
     *
     * @param organizationReversionDetails the details to inactivate
     * @param documentNumber               the document number which has the inactivations as part of it
     * @return an inactivation status object which will help us save notes
     */
    protected void trickleDownActivations(List<OrganizationReversionDetail> organizationReversionDetails, String documentNumber) {
        TrickleDownStatus status = new TrickleDownStatus(KFSKeyConstants.ORGANIZATION_REVERSION_DETAIL_TRICKLE_DOWN_ACTIVATION, KFSKeyConstants.ORGANIZATION_REVERSION_DETAIL_TRICKLE_DOWN_ACTIVATION_ERROR_DURING_PERSISTENCE);

        if (!ObjectUtils.isNull(organizationReversionDetails) && !organizationReversionDetails.isEmpty()) {
            for (OrganizationReversionDetail detail : organizationReversionDetails) {
                if (!detail.isActive() && allowActivation(detail)) {
                    detail.setActive(true);
                    try {
                        businessObjectService.save(detail);
                        status.addOrganizationReversionDetail(detail);
                    } catch (RuntimeException re) {
                        LOG.error("Unable to trickle-down inactivate sub-account " + detail.toString(), re);
                        status.addErrorPersistingOrganizationReversionDetail(detail);
                    }
                }
            }
        }

        status.saveSuccesfullyChangedNotes(documentNumber);
        status.saveErrorNotes(documentNumber);
    }

    /**
     * Determines whether the given organization reversion detail can be activated: ie, that both its owning OrganizationReversion and its related
     * OrganizationReversionCategory are both active
     *
     * @param detail the detail to check
     * @return true if the detail can be activated, false otherwise
     */
    protected boolean allowActivation(OrganizationReversionDetail detail) {
        boolean result = true;
        if (!ObjectUtils.isNull(detail.getOrganizationReversion())) {
            result &= detail.getOrganizationReversion().isActive();
        }
        if (!ObjectUtils.isNull(detail.getOrganizationReversionCategory())) {
            result &= detail.getOrganizationReversionCategory().isActive();
        }
        return result;
    }

    /**
     * Inner class to keep track of what organization reversions were inactivated and which
     * had errors when the persisting of the inactivation was attempted
     */
    protected class TrickleDownStatus {
        private List<OrganizationReversionDetail> organizationReversionDetails;
        private List<OrganizationReversionDetail> errorPersistingOrganizationReversionDetails;
        private String successfullyChangedOrganizationReversionDetailsMessageKey;
        private String erroredOutOrganizationReversionDetailsMessageKey;

        /**
         * Constructs a OrganizationReversionDetailTrickleDownInactivationServiceImpl
         */
        public TrickleDownStatus(String successfullyChangedOrganizationReversionDetailsMessageKey, String erroredOutOrganizationReversionDetailsMessageKey) {
            organizationReversionDetails = new ArrayList<OrganizationReversionDetail>();
            errorPersistingOrganizationReversionDetails = new ArrayList<OrganizationReversionDetail>();
            this.successfullyChangedOrganizationReversionDetailsMessageKey = successfullyChangedOrganizationReversionDetailsMessageKey;
            this.erroredOutOrganizationReversionDetailsMessageKey = erroredOutOrganizationReversionDetailsMessageKey;
        }

        /**
         * Adds an organization reversion detail which had a successfully persisted activation to the message list
         *
         * @param organizationReversionDetail the detail to add to the list
         */
        public void addOrganizationReversionDetail(OrganizationReversionDetail organizationReversionDetail) {
            organizationReversionDetails.add(organizationReversionDetail);
        }

        /**
         * Adds an organization reversion detail which could not successful persist its activation to the error message list
         *
         * @param organizationReversionDetail the detail to add to the list
         */
        public void addErrorPersistingOrganizationReversionDetail(OrganizationReversionDetail organizationReversionDetail) {
            errorPersistingOrganizationReversionDetails.add(organizationReversionDetail);
        }

        /**
         * @return the number of details we want per note
         */
        protected int getDetailsPerNote() {
            return 20;
        }

        /**
         * Builds a List of Notes out of a list of OrganizationReversionDescriptions
         *
         * @param messageKey                   the key of the note text in ApplicationResources.properties
         * @param noteParent                   the thing to stick the note on
         * @param organizationReversionDetails the List of OrganizationReversionDetails to make notes about
         * @return a List of Notes
         */
        protected List<Note> generateNotes(String messageKey, PersistableBusinessObject noteParent, List<OrganizationReversionDetail> organizationReversionDetails) {
            List<Note> notes = new ArrayList<Note>();
            List<String> organizationReversionDetailsDescriptions = generateOrganizationReversionDetailsForNotes(organizationReversionDetails);
            Note noteTemplate = new Note();
            for (String description : organizationReversionDetailsDescriptions) {
                if (!StringUtils.isBlank(description)) {
                    notes.add(buildNote(description, messageKey, noteTemplate, noteParent));
                }
            }
            return notes;
        }

        /**
         * Builds a note
         *
         * @param description  a description to put into the message of the note
         * @param messageKey   the key of the note text in ApplicationResources.properties
         * @param noteTemplate the template for the note
         * @param noteParent   the thing to stick the note on
         * @return the built note
         */
        protected Note buildNote(String description, String messageKey, Note noteTemplate, PersistableBusinessObject noteParent) {
            Note note = null;
            try {
                final String noteTextTemplate = kualiConfigurationService.getPropertyValueAsString(messageKey);
                final String noteText = MessageFormat.format(noteTextTemplate, description);
                note = noteService.createNote(noteTemplate, noteParent, GlobalVariables.getUserSession().getPrincipalId());
                note.setNoteText(noteText);
            } catch (Exception e) {
                // noteService.createNote throws *Exception*???
                // weak!!
                throw new RuntimeException("Cannot create note", e);
            }
            return note;
        }

        /**
         * Builds organization reverion detail descriptions to populate notes
         *
         * @param organizationReversionDetails the list of details to convert to notes
         * @return a List of notes
         */
        protected List<String> generateOrganizationReversionDetailsForNotes(List<OrganizationReversionDetail> organizationReversionDetails) {
            List<String> orgRevDetailDescriptions = new ArrayList<String>();

            if (organizationReversionDetails.size() > 0) {
                StringBuilder description = new StringBuilder();
                description.append(getOrganizationReversionDetailDescription(organizationReversionDetails.get(0)));

                int count = 1;
                while (count < organizationReversionDetails.size()) {
                    if (count % getDetailsPerNote() == 0) { // time for a new note
                        orgRevDetailDescriptions.add(description.toString());
                        description = new StringBuilder();
                    } else {
                        description.append(", ");
                    }
                    description.append(getOrganizationReversionDetailDescription(organizationReversionDetails.get(count)));
                    count += 1;
                }

                // add the last description
                orgRevDetailDescriptions.add(description.toString());
            }

            return orgRevDetailDescriptions;
        }

        /**
         * Beautifully and eloquently describes an organization reversion detail
         *
         * @param organizationReversionDetail the organization reversion detail to describe
         * @return the funny, heart-breaking, and ultimately inspiring resultant description
         */
        protected String getOrganizationReversionDetailDescription(OrganizationReversionDetail organizationReversionDetail) {
            return organizationReversionDetail.getChartOfAccountsCode() + " - " + organizationReversionDetail.getOrganizationCode() + " Category: " + organizationReversionDetail.getOrganizationReversionCategoryCode();
        }

        /**
         * Saves notes to a document
         *
         * @param organizationReversionDetails the details to make notes about
         * @param messageKey                   the message key of the text of the note
         * @param documentNumber               the document number to write to
         */
        protected void saveAllNotes(List<OrganizationReversionDetail> organizationReversionDetails, String messageKey, String documentNumber) {
            DocumentHeader noteParent = documentHeaderService.getDocumentHeaderById(documentNumber);
            List<Note> notes = generateNotes(messageKey, noteParent, organizationReversionDetails);
            noteService.saveNoteList(notes);
        }

        /**
         * Adds all the notes about successful inactivations
         *
         * @param documentNumber document number to save them to
         */
        public void saveSuccesfullyChangedNotes(String documentNumber) {
            saveAllNotes(organizationReversionDetails, successfullyChangedOrganizationReversionDetailsMessageKey, documentNumber);
        }

        /**
         * Adds all the notes about inactivations which couldn't be saved
         *
         * @param documentNumber the document number to save them to
         */
        public void saveErrorNotes(String documentNumber) {
            saveAllNotes(errorPersistingOrganizationReversionDetails, erroredOutOrganizationReversionDetailsMessageKey, documentNumber);
        }

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

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

    /**
     * Gets the kualiConfigurationService attribute.
     *
     * @return Returns the kualiConfigurationService.
     */
    public ConfigurationService getConfigurationService() {
        return kualiConfigurationService;
    }

    /**
     * Sets the kualiConfigurationService attribute value.
     *
     * @param kualiConfigurationService The kualiConfigurationService to set.
     */
    public void setConfigurationService(ConfigurationService kualiConfigurationService) {
        this.kualiConfigurationService = kualiConfigurationService;
    }

    /**
     * Gets the noteService attribute.
     *
     * @return Returns the noteService.
     */
    public NoteService getNoteService() {
        return noteService;
    }

    /**
     * Sets the noteService attribute value.
     *
     * @param noteService The noteService to set.
     */
    public void setNoteService(NoteService noteService) {
        this.noteService = noteService;
    }

    /**
     * Gets the businessObjectService attribute.
     *
     * @return Returns the businessObjectService.
     */
    public BusinessObjectService getBusinessObjectService() {
        return businessObjectService;
    }

    /**
     * Sets the businessObjectService attribute value.
     *
     * @param businessObjectService The businessObjectService to set.
     */
    public void setBusinessObjectService(BusinessObjectService businessObjectService) {
        this.businessObjectService = businessObjectService;
    }

    /**
     * Gets the documentHeaderService attribute.
     *
     * @return Returns the documentHeaderService.
     */
    public DocumentHeaderService getDocumentHeaderService() {
        return documentHeaderService;
    }

    /**
     * Sets the documentHeaderService attribute value.
     *
     * @param documentHeaderService The documentHeaderService to set.
     */
    public void setDocumentHeaderService(DocumentHeaderService documentHeaderService) {
        this.documentHeaderService = documentHeaderService;
    }
}
