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

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.kuali.kfs.coa.businessobject.Account;
import org.kuali.kfs.fp.service.AccountingDocumentPreRuleService;
import org.kuali.kfs.kns.rule.event.PromptBeforeValidationEvent;
import org.kuali.kfs.kns.rules.PromptBeforeValidationBase;
import org.kuali.kfs.kns.service.DataDictionaryService;
import org.kuali.kfs.krad.document.Document;
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.KFSKeyConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.businessobject.AccountingLine;
import org.kuali.kfs.sys.businessobject.AccountingLineOverride;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.document.AccountingDocumentBase;
import org.kuali.kfs.sys.document.authorization.AccountingLineAuthorizer;
import org.kuali.kfs.sys.document.authorization.AccountingLineAuthorizerBase;
import org.kuali.kfs.sys.document.datadictionary.AccountingLineGroupDefinition;
import org.kuali.kfs.sys.document.datadictionary.FinancialSystemTransactionalDocumentEntry;
import org.kuali.rice.core.api.config.property.ConfigurationService;
import org.kuali.rice.kew.api.WorkflowDocument;
import org.kuali.rice.kim.api.identity.Person;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;


/**
 * This service interface defines methods that a AccountingDocumentPreRuleService implementation must provide.
 */

public class AccountingDocumentPreRuleServiceImpl implements AccountingDocumentPreRuleService {
    protected static Logger LOG = Logger.getLogger(AccountingDocumentPreRuleServiceImpl.class);

    /**
     * Access the account override question for all accounting document
     *
     * @param document
     * @param preRule
     * @return
     */
    public boolean expiredAccountOverrideQuestion(AccountingDocumentBase document, PromptBeforeValidationBase preRule, PromptBeforeValidationEvent event) {
        boolean tabStatesOK = true;
        List<AccountingLine> accountLineList = getOverrideQuestionAccount(document);
        if (accountLineList != null && !accountLineList.isEmpty()) {
            String questionText = SpringContext.getBean(ConfigurationService.class).getPropertyValueAsString(KFSKeyConstants.QUESTION_NEED_OVERRIDE_ACCOUNT_FOR_EXPIRED);

            StringBuffer expiredAccounts = new StringBuffer();
            for (AccountingLine accountingLine : accountLineList) {
                expiredAccounts.append(accountingLine.getChartOfAccountsCode());
                expiredAccounts.append("-");
                expiredAccounts.append(accountingLine.getAccountNumber());
                expiredAccounts.append(" ");

            }
            questionText = StringUtils.replace(questionText, "{0}", expiredAccounts.toString());

            boolean overrideAccount = preRule.askOrAnalyzeYesNoQuestion(KFSConstants.OVERRIDE_ACCOUNT_FOR_EXPIRED_QUESTION_ID, questionText);

            if (overrideAccount) {
                Set overrideInputComponents = new HashSet();
                overrideInputComponents.add(AccountingLineOverride.COMPONENT.EXPIRED_ACCOUNT);
                for (AccountingLine accountingLine : accountLineList) {
                    setAccountOverride(document, accountingLine.getAccount(), AccountingLineOverride.valueOf(overrideInputComponents).getCode());
                }
            } else {
                // return to document if the user selects No
                event.setActionForwardName(KFSConstants.MAPPING_BASIC);
                tabStatesOK = false;
            }
        }
        return tabStatesOK;
    }

    /**
     * Set up override for all accounting line with the same account number
     *
     * @param document
     * @param accountLine
     * @param code
     */
    protected void setAccountOverride(AccountingDocumentBase document, Account overrideAccount, String code) {
        List accountLinesFromDoc = new ArrayList<AccountingLine>();
        accountLinesFromDoc.addAll(document.getSourceAccountingLines());
        accountLinesFromDoc.addAll(document.getTargetAccountingLines());

        for (Iterator iter = accountLinesFromDoc.iterator(); iter.hasNext(); ) {
            AccountingLine currentLine = (AccountingLine) iter.next();

            if (overrideAccount.getChartOfAccountsCode().equals(currentLine.getChartOfAccountsCode()) && overrideAccount.getAccountNumber().equals(currentLine.getAccountNumber())) {
                currentLine.setOverrideCode(code);
            }
        }
    }

    /**
     * DTT-3163: Walk through all source and target accounting lines to identify if there is account which is expired and requires
     * approver to override but approver does not have the edit permission
     *
     * @param document
     * @return
     */
    protected List<AccountingLine> getOverrideQuestionAccount(Document document) {
        final Person currentUser = GlobalVariables.getUserSession().getPerson();
        List<AccountingLine> questionAccounts = new ArrayList<AccountingLine>();
        HashMap questionAccountsMap = new HashMap<String, Object>();
        String accountKey = null;

        // expiration warning should be triggered only when document is enrouting and waiting for approval; accounting
        // line changed from active to inactive due to expiration date; the current approval does not have the permission on editing
        // accounting line
        if (ObjectUtils.isNotNull(document.getDocumentHeader())) {
            WorkflowDocument workflowDoc = document.getDocumentHeader().getWorkflowDocument();
            if (ObjectUtils.isNotNull(workflowDoc) && workflowDoc.isEnroute() && workflowDoc.isApprovalRequested()) {
                AccountingDocumentBase acctDoc = (AccountingDocumentBase) document;

                List accountLinesFromDoc = new ArrayList<AccountingLine>();
                accountLinesFromDoc.addAll(acctDoc.getSourceAccountingLines());
                accountLinesFromDoc.addAll(acctDoc.getTargetAccountingLines());

                Map<String, AccountingLineAuthorizer> authorizerMap = new HashMap<String, AccountingLineAuthorizer>();


                for (Iterator iter = accountLinesFromDoc.iterator(); iter.hasNext(); ) {
                    AccountingLine currentLine = (AccountingLine) iter.next();
                    accountKey = currentLine.getChartOfAccountsCode() + "-" + currentLine.getAccountNumber();
                    // if account is a known expired account, skip it.
                    if (questionAccountsMap.containsKey(accountKey)) {
                        continue;
                    }

                    AccountingLineOverride override = AccountingLineOverride.valueOf(currentLine.getOverrideCode());

                    if (AccountingLineOverride.needsExpiredAccountOverride(currentLine.getAccount()) && !override.hasComponent(AccountingLineOverride.COMPONENT.EXPIRED_ACCOUNT)) {
                        // check if the approver has the permission
                        String groupName = getGroupName(currentLine);
                        AccountingLineAuthorizer accountingLineAuthorizer = authorizerMap.get(groupName);
                        if (accountingLineAuthorizer == null) {
                            accountingLineAuthorizer = lookupAccountingLineAuthorizer(currentLine, document, groupName);
                            authorizerMap.put(groupName, accountingLineAuthorizer);
                        }

                        if (accountingLineAuthorizer != null) {
                            final Set<String> currentNodes = acctDoc.getDocumentHeader().getWorkflowDocument().getCurrentNodeNames();
                            boolean lineIsAccessible = accountingLineAuthorizer.hasEditPermissionOnAccountingLine(acctDoc, currentLine, getAccountingLineCollectionProperty(currentLine), currentUser, true, currentNodes);
                            boolean isAccessible = accountingLineAuthorizer.hasEditPermissionOnField(acctDoc, currentLine, getAccountingLineCollectionProperty(currentLine), KFSPropertyConstants.ACCOUNT_NUMBER, lineIsAccessible, true, currentUser, currentNodes);

                            if (!isAccessible) {
                                questionAccounts.add(currentLine);
                                questionAccountsMap.put(accountKey, currentLine);
                            }
                        }
                    }
                }

            }

        }

        return questionAccounts;
    }

    /**
     * Determines the property of the accounting line collection from the error prefixes
     *
     * @return the accounting line collection property
     */
    protected String getAccountingLineCollectionProperty(AccountingLine account) {
        String propertyName = null;
        propertyName = account.isSourceAccountingLine() ? KFSConstants.PermissionAttributeValue.SOURCE_ACCOUNTING_LINES.value : KFSConstants.PermissionAttributeValue.TARGET_ACCOUNTING_LINES.value;

        if (propertyName.equals("newSourceLine"))
            return KFSConstants.PermissionAttributeValue.SOURCE_ACCOUNTING_LINES.value;
        if (propertyName.equals("newTargetLine"))
            return KFSConstants.PermissionAttributeValue.TARGET_ACCOUNTING_LINES.value;
        return propertyName;
    }

    /**
     * @param accountingLines
     * @return Map containing accountingLines from the given List, indexed by their sequenceNumber
     */
    protected Map buildAccountingLineMap(List accountingLines) {
        Map lineMap = new HashMap();

        for (Iterator i = accountingLines.iterator(); i.hasNext(); ) {
            AccountingLine accountingLine = (AccountingLine) i.next();
            Integer sequenceNumber = accountingLine.getSequenceNumber();

            lineMap.put(sequenceNumber, accountingLine);
        }

        return lineMap;
    }

    /**
     * @return hopefully, the best accounting line authorizer implementation to do the KIM check for to see if lines are accessible
     */
    protected AccountingLineAuthorizer lookupAccountingLineAuthorizer(AccountingLine account, Document document, String groupName) {
        Map<String, AccountingLineGroupDefinition> groups = ((FinancialSystemTransactionalDocumentEntry) SpringContext.getBean(DataDictionaryService.class).getDataDictionary().getDictionaryObjectEntry(document.getClass().getName())).getAccountingLineGroups();

        if (groups.isEmpty())
            return new AccountingLineAuthorizerBase(); // no groups? just use the default...
        if (groups.containsKey(groupName))
            return groups.get(groupName).getAccountingLineAuthorizer(); // we've got the group

        Set<String> groupNames = groups.keySet(); // we've got groups, just not the proper name; try our luck and get the
        // first group iterator
        Iterator<String> groupNameIterator = groupNames.iterator();
        String firstGroupName = groupNameIterator.next();
        return groups.get(firstGroupName).getAccountingLineAuthorizer();
    }

    /**
     * Returns the name of the accounting line group which holds the proper authorizer to do the KIM check
     *
     * @return the name of the accouting line group to get the authorizer from
     */
    protected String getGroupName(AccountingLine line) {
        return (line.isSourceAccountingLine() ? KFSConstants.SOURCE_ACCOUNTING_LINES_GROUP_NAME : KFSConstants.TARGET_ACCOUNTING_LINES_GROUP_NAME);
    }
}
