/*
 * 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.Account;
import org.kuali.kfs.coa.businessobject.SubAccount;
import org.kuali.kfs.coa.service.SubAccountTrickleDownInactivationService;
import org.kuali.kfs.kns.maintenance.Maintainable;
import org.kuali.kfs.kns.service.MaintenanceDocumentDictionaryService;
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.dao.MaintenanceDocumentDao;
import org.kuali.kfs.krad.maintenance.MaintenanceLock;
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.kfs.sys.KFSPropertyConstants;
import org.kuali.rice.core.api.config.property.ConfigurationService;
import org.springframework.transaction.annotation.Transactional;

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

@Transactional
public class SubAccountTrickleDownInactivationServiceImpl implements SubAccountTrickleDownInactivationService {
    private static final Logger LOG = Logger.getLogger(SubAccountTrickleDownInactivationServiceImpl.class);

    protected MaintenanceDocumentDictionaryService maintenanceDocumentDictionaryService;
    protected MaintenanceDocumentDao maintenanceDocumentDao;
    protected NoteService noteService;
    protected ConfigurationService kualiConfigurationService;
    protected DocumentHeaderService documentHeaderService;

    /**
     * Will generate Maintenance Locks for all (active or not) sub-accounts in the system related to the inactivated account using the sub-account
     * maintainable registered for the sub-account maintenance document
     * <p>
     * This version of the method assumes that the sub-account maintainable only requires that the SubAccount BOClass, document number, and SubAccount
     * instance only needs to be passed into it
     *
     * @see org.kuali.kfs.gl.service.SubAccountTrickleDownInactivationService#generateTrickleDownMaintenanceLocks(org.kuali.kfs.coa.businessobject.Account, java.lang.String)
     */
    public List<MaintenanceLock> generateTrickleDownMaintenanceLocks(Account inactivatedAccount, String documentNumber) {
        inactivatedAccount.refreshReferenceObject(KFSPropertyConstants.SUB_ACCOUNTS);
        List<MaintenanceLock> maintenanceLocks = new ArrayList<MaintenanceLock>();

        Maintainable subAccountMaintainable;
        try {
            subAccountMaintainable = (Maintainable) maintenanceDocumentDictionaryService.getMaintainableClass(SubAccount.class.getName()).newInstance();
            subAccountMaintainable.setBoClass(SubAccount.class);
            subAccountMaintainable.setDocumentNumber(documentNumber);
        } catch (Exception e) {
            LOG.error("Unable to instantiate SubAccount Maintainable", e);
            throw new RuntimeException("Unable to instantiate SubAccount Maintainable", e);
        }

        if (ObjectUtils.isNotNull(inactivatedAccount.getSubAccounts()) && !inactivatedAccount.getSubAccounts().isEmpty()) {
            for (Iterator<SubAccount> i = inactivatedAccount.getSubAccounts().iterator(); i.hasNext(); ) {
                SubAccount subAccount = i.next();

                subAccountMaintainable.setBusinessObject(subAccount);
                maintenanceLocks.addAll(subAccountMaintainable.generateMaintenanceLocks());
            }
        }
        return maintenanceLocks;
    }

    public void trickleDownInactivateSubAccounts(Account inactivatedAccount, String documentNumber) {
        List<SubAccount> inactivatedSubAccounts = new ArrayList<SubAccount>();
        Map<SubAccount, String> alreadyLockedSubAccounts = new HashMap<SubAccount, String>();
        List<SubAccount> errorPersistingSubAccounts = new ArrayList<SubAccount>();

        Maintainable subAccountMaintainable;
        try {
            subAccountMaintainable = (Maintainable) maintenanceDocumentDictionaryService.getMaintainableClass(SubAccount.class.getName()).newInstance();
            subAccountMaintainable.setBoClass(SubAccount.class);
            subAccountMaintainable.setDocumentNumber(documentNumber);
        } catch (Exception e) {
            LOG.error("Unable to instantiate SubAccount Maintainable", e);
            throw new RuntimeException("Unable to instantiate SubAccount Maintainable", e);
        }

        inactivatedAccount.refreshReferenceObject(KFSPropertyConstants.SUB_ACCOUNTS);
        if (ObjectUtils.isNotNull(inactivatedAccount.getSubAccounts()) && !inactivatedAccount.getSubAccounts().isEmpty()) {
            for (Iterator<SubAccount> i = inactivatedAccount.getSubAccounts().iterator(); i.hasNext(); ) {
                SubAccount subAccount = i.next();
                if (subAccount.isActive()) {
                    subAccountMaintainable.setBusinessObject(subAccount);
                    List<MaintenanceLock> subAccountLocks = subAccountMaintainable.generateMaintenanceLocks();

                    MaintenanceLock failedLock = verifyAllLocksFromThisDocument(subAccountLocks, documentNumber);
                    if (failedLock != null) {
                        // another document has locked this sub account, so we don't try to inactivate the account
                        alreadyLockedSubAccounts.put(subAccount, failedLock.getDocumentNumber());
                    } else {
                        // no locks other than our own (but there may have been no locks at all), just go ahead and try to update
                        subAccount.setActive(false);

                        try {
                            subAccountMaintainable.saveBusinessObject();
                            inactivatedSubAccounts.add(subAccount);
                        } catch (RuntimeException e) {
                            LOG.error("Unable to trickle-down inactivate sub-account " + subAccount.toString(), e);
                            errorPersistingSubAccounts.add(subAccount);
                        }
                    }
                }
            }

            addNotesToDocument(documentNumber, inactivatedSubAccounts, alreadyLockedSubAccounts, errorPersistingSubAccounts);
        }
    }

    protected void addNotesToDocument(String documentNumber, List<SubAccount> inactivatedSubAccounts, Map<SubAccount, String> alreadyLockedSubAccounts, List<SubAccount> errorPersistingSubAccounts) {
        if (inactivatedSubAccounts.isEmpty() && alreadyLockedSubAccounts.isEmpty() && errorPersistingSubAccounts.isEmpty()) {
            // if we didn't try to inactivate any sub-accounts, then don't bother
            return;
        }
        DocumentHeader noteParent = documentHeaderService.getDocumentHeaderById(documentNumber);
        Note newNote = new Note();

        addNotes(documentNumber, inactivatedSubAccounts, KFSKeyConstants.SUB_ACCOUNT_TRICKLE_DOWN_INACTIVATION, noteParent, newNote);
        addNotes(documentNumber, errorPersistingSubAccounts, KFSKeyConstants.SUB_ACCOUNT_TRICKLE_DOWN_INACTIVATION_ERROR_DURING_PERSISTENCE, noteParent, newNote);
        addMaintenanceLockedNotes(documentNumber, alreadyLockedSubAccounts, KFSKeyConstants.SUB_ACCOUNT_TRICKLE_DOWN_INACTIVATION_RECORD_ALREADY_MAINTENANCE_LOCKED, noteParent, newNote);
    }

    protected void addMaintenanceLockedNotes(String documentNumber, Map<SubAccount, String> lockedSubAccounts, String messageKey, PersistableBusinessObject noteParent, Note noteTemplate) {
        for (Map.Entry<SubAccount, String> entry : lockedSubAccounts.entrySet()) {
            try {
                SubAccount subAccount = entry.getKey();
                String subAccountString = subAccount.getChartOfAccountsCode() + " - " + subAccount.getAccountNumber() + " - " + subAccount.getSubAccountNumber();
                if (StringUtils.isNotBlank(subAccountString)) {
                    String noteTextTemplate = kualiConfigurationService.getPropertyValueAsString(messageKey);
                    String noteText = MessageFormat.format(noteTextTemplate, subAccountString, entry.getValue());
                    Note note = noteService.createNote(noteTemplate, noteParent, GlobalVariables.getUserSession().getPrincipalId());
                    note.setNoteText(noteText);
                    noteService.save(note);
                }
            } catch (Exception e) {
                LOG.error("Unable to create/save notes for document " + documentNumber, e);
                throw new RuntimeException("Unable to create/save notes for document " + documentNumber, e);
            }
        }
    }

    protected void addNotes(String documentNumber, List<SubAccount> listOfSubAccounts, String messageKey, PersistableBusinessObject noteParent, Note noteTemplate) {
        for (int i = 0; i < listOfSubAccounts.size(); i += getNumSubAccountsPerNote()) {
            try {
                String subAccountString = createSubAccountChunk(listOfSubAccounts, i, i + getNumSubAccountsPerNote());
                if (StringUtils.isNotBlank(subAccountString)) {
                    String noteTextTemplate = kualiConfigurationService.getPropertyValueAsString(messageKey);
                    String noteText = MessageFormat.format(noteTextTemplate, subAccountString);
                    Note note = noteService.createNote(noteTemplate, noteParent, GlobalVariables.getUserSession().getPrincipalId());
                    note.setNoteText(noteText);
                    note.setNotePostedTimestampToCurrent();
                    noteService.save(note);
                }
            } catch (Exception e) {
                LOG.error("Unable to create/save notes for document " + documentNumber, e);
                throw new RuntimeException("Unable to create/save notes for document " + documentNumber, e);
            }
        }
    }

    protected String createSubAccountChunk(List<SubAccount> listOfSubAccounts, int startIndex, int endIndex) {
        StringBuilder buf = new StringBuilder();
        for (int i = startIndex; i < endIndex && i < listOfSubAccounts.size(); i++) {
            SubAccount subAccount = listOfSubAccounts.get(i);
            buf.append(subAccount.getChartOfAccountsCode()).append(" - ").append(subAccount.getAccountNumber()).append(" - ")
                .append(subAccount.getSubAccountNumber());
            if (i + 1 < endIndex && i + 1 < listOfSubAccounts.size()) {
                buf.append(", ");
            }
        }
        return buf.toString();
    }

    protected int getNumSubAccountsPerNote() {
        return 20;
    }

    protected MaintenanceLock verifyAllLocksFromThisDocument(List<MaintenanceLock> maintenanceLocks, String documentNumber) {
        for (MaintenanceLock maintenanceLock : maintenanceLocks) {
            String lockingDocNumber = maintenanceDocumentDao.getLockingDocumentNumber(maintenanceLock.getLockingRepresentation(), documentNumber);
            if (StringUtils.isNotBlank(lockingDocNumber)) {
                return maintenanceLock;
            }
        }
        return null;
    }

    public void setMaintenanceDocumentDictionaryService(MaintenanceDocumentDictionaryService maintenanceDocumentDictionaryService) {
        this.maintenanceDocumentDictionaryService = maintenanceDocumentDictionaryService;
    }


    public void setMaintenanceDocumentDao(MaintenanceDocumentDao maintenanceDocumentDao) {
        this.maintenanceDocumentDao = maintenanceDocumentDao;
    }


    public void setNoteService(NoteService noteService) {
        this.noteService = noteService;
    }

    public void setConfigurationService(ConfigurationService kualiConfigurationService) {
        this.kualiConfigurationService = kualiConfigurationService;
    }

    public void setDocumentHeaderService(DocumentHeaderService documentHeaderService) {
        this.documentHeaderService = documentHeaderService;
    }
}
