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

import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.coa.businessobject.A21SubAccount;
import org.kuali.kfs.integration.ec.EffortCertificationModuleService;
import org.kuali.kfs.integration.ec.EffortCertificationReport;
import org.kuali.kfs.krad.bo.Note;
import org.kuali.kfs.krad.service.DocumentService;
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.module.ld.LaborKeyConstants;
import org.kuali.kfs.module.ld.businessobject.ExpenseTransferAccountingLine;
import org.kuali.kfs.module.ld.businessobject.ExpenseTransferSourceAccountingLine;
import org.kuali.kfs.module.ld.document.SalaryExpenseTransferDocument;
import org.kuali.kfs.module.ld.document.service.SalaryTransferPeriodValidationService;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.rice.core.api.config.property.ConfigurationService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.kim.api.identity.principal.Principal;
import org.kuali.rice.kim.api.services.KimApiServiceLocator;
import org.springframework.transaction.annotation.Transactional;

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

/**
 * @see org.kuali.kfs.module.ld.document.service.SalaryTransferPeriodValidationService
 */
@Transactional
public class SalaryTransferPeriodValidationServiceImpl implements SalaryTransferPeriodValidationService {
    private EffortCertificationModuleService effortCertificationService;
    private DocumentService documentService;
    private NoteService noteService;
    private ConfigurationService kualiConfigurationService;

    /**
     * @see org.kuali.kfs.module.ld.document.service.SalaryTransferPeriodValidationService#validateTransfers(org.kuali.kfs.module.ld.document.SalaryExpenseTransferDocument)
     */
    @Override
    public boolean validateTransfers(SalaryExpenseTransferDocument document) {
        List<ExpenseTransferAccountingLine> transferLinesInOpenPeriod = new ArrayList<ExpenseTransferAccountingLine>();

        // check for closed or open reporting period(s) ... closed periods result in error, open periods require more validation
        List<ExpenseTransferAccountingLine> allLines = new ArrayList<ExpenseTransferAccountingLine>(document.getSourceAccountingLines());
        allLines.addAll(document.getTargetAccountingLines());
        for (ExpenseTransferAccountingLine transferLine : allLines) {
            // check we have enough data for validation, if not business rules will report error
            if (!containsNecessaryData(transferLine)) {
                continue;
            }

            // if closed report found then return error
            EffortCertificationReport closedReport = getClosedReportingPeriod(transferLine);
            if (closedReport != null) {
                putError(LaborKeyConstants.ERROR_EFFORT_CLOSED_REPORT_PERIOD, transferLine, closedReport);
                return false;
            }

            // if open report(s) found then add transfer line to list for further validation
            EffortCertificationReport openReport = getOpenReportingPeriod(transferLine);
            if (openReport != null) {
                transferLinesInOpenPeriod.add(transferLine);
            }
        }

        // verify transfers will not affect the open reporting period
        Map<String, KualiDecimal> accountPeriodTransfer = new HashMap<String, KualiDecimal>();
        EffortCertificationReport emplidReport = null;
        for (ExpenseTransferAccountingLine transferLine : transferLinesInOpenPeriod) {
            emplidReport = isEmployeeWithOpenCertification(transferLine, document.getEmplid());
            if (emplidReport != null) {
                // if employee has a report, transfer lines cannot use cost share sub-accounts
                if (isCostShareSubAccount(transferLine)) {
                    putError(LaborKeyConstants.ERROR_EFFORT_OPEN_PERIOD_COST_SHARE, transferLine, emplidReport);
                    return false;
                }

                // add line amount for validation later
                addAccountTransferAmount(accountPeriodTransfer, transferLine, emplidReport);
            } else {
                // if employee does not have a report, transfer lines cannot use CG accounts
                if (transferLine.getAccount().isForContractsAndGrants()) {
                    EffortCertificationReport openReport = getOpenReportingPeriod(transferLine);
                    putError(LaborKeyConstants.ERROR_EFFORT_OPEN_PERIOD_CG_ACCOUNT, transferLine, openReport);
                    return false;
                }
            }
        }

        // verify balance is same for accounts in transfer map
        for (String transferKey : accountPeriodTransfer.keySet()) {
            KualiDecimal transfer = accountPeriodTransfer.get(transferKey);
            if (transfer.isNonZero()) {
                String[] keyFields = StringUtils.split(transferKey, ",");
                GlobalVariables.getMessageMap().putError(KFSPropertyConstants.SOURCE_ACCOUNTING_LINES, LaborKeyConstants.ERROR_EFFORT_OPEN_PERIOD_ACCOUNTS_NOT_BALANCED, new String[]{keyFields[4], keyFields[0], keyFields[1]});
                return false;
            }
        }

        return true;
    }

    /**
     * @see org.kuali.kfs.module.ld.document.service.SalaryTransferPeriodValidationService#disapproveSalaryExpenseDocument(org.kuali.kfs.module.ld.document.SalaryExpenseTransferDocument)
     */
    @Override
    public void disapproveSalaryExpenseDocument(SalaryExpenseTransferDocument document) throws Exception {
        // create note explaining why the document was disapproved
        String message = kualiConfigurationService.getPropertyValueAsString(LaborKeyConstants.EFFORT_AUTO_DISAPPROVE_MESSAGE);
        Note cancelNote = documentService.createNoteFromDocument(document, message);

        Principal principal = KimApiServiceLocator.getIdentityService().getPrincipalByPrincipalName(KFSConstants.SYSTEM_USER);
        cancelNote.setAuthorUniversalIdentifier(principal.getPrincipalId());
        noteService.save(cancelNote);
        document.addNote(cancelNote);

        documentService.disapproveDocument(document, "disapproved - failed effort certification checks");
    }

    /**
     * Checks list of report definitions for a closed period.
     *
     * @param transferLine - transfer line to find report definition for
     * @return closed report or null if one is not found
     */
    protected EffortCertificationReport getClosedReportingPeriod(ExpenseTransferAccountingLine transferLine) {
        List<EffortCertificationReport> effortReports = getEffortReportDefinitionsForLine(transferLine);

        for (EffortCertificationReport report : effortReports) {
            if (KFSConstants.PeriodStatusCodes.CLOSED.equals(report.getEffortCertificationReportPeriodStatusCode())) {
                return report;
            }
        }

        return null;
    }

    /**
     * Checks list of report definitions for a open period.
     *
     * @param transferLine - transfer line to find report definition for
     * @return open report or null if one is not found
     */
    protected EffortCertificationReport getOpenReportingPeriod(ExpenseTransferAccountingLine transferLine) {
        List<EffortCertificationReport> effortReports = getEffortReportDefinitionsForLine(transferLine);

        for (EffortCertificationReport report : effortReports) {
            if (KFSConstants.PeriodStatusCodes.OPEN.equals(report.getEffortCertificationReportPeriodStatusCode())) {
                return report;
            }
        }

        return null;
    }

    /**
     * Returns the open report periods from the given list of report definitions.
     *
     * @param effortReports - list of report definitions that are either open or closed
     * @return open effort report definitions
     */
    protected List<EffortCertificationReport> getOpenReportDefinitions(List<EffortCertificationReport> effortReports) {
        List<EffortCertificationReport> openReports = new ArrayList<EffortCertificationReport>();

        for (EffortCertificationReport report : effortReports) {
            if (KFSConstants.PeriodStatusCodes.OPEN.equals(report.getEffortCertificationReportPeriodStatusCode())) {
                openReports.add(report);
            }
        }

        return openReports;
    }

    /**
     * Checks the sub account type code against the values defined for cost share.
     *
     * @param transferLine - line with sub account to check
     * @return true if sub account is cost share, false otherwise
     */
    protected boolean isCostShareSubAccount(ExpenseTransferAccountingLine transferLine) {
        boolean isCostShare = false;

        if (ObjectUtils.isNotNull(transferLine.getSubAccount()) && ObjectUtils.isNotNull(transferLine.getSubAccount().getA21SubAccount())) {
            A21SubAccount a21SubAccount = transferLine.getSubAccount().getA21SubAccount();
            String subAccountTypeCode = a21SubAccount.getSubAccountTypeCode();

            List<String> costShareSubAccountTypeCodes = effortCertificationService.getCostShareSubAccountTypeCodes();
            if (costShareSubAccountTypeCodes.contains(subAccountTypeCode)) {
                isCostShare = true;
            }
        }

        return isCostShare;
    }

    /**
     * Finds all open effort reports for the given transfer line, then checks if the given emplid has a certification for one of
     * those open reports.
     *
     * @param transferLine - line to find open reports for
     * @param emplid       - emplid to check for certification
     * @return report which emplid has certification, or null
     */
    protected EffortCertificationReport isEmployeeWithOpenCertification(ExpenseTransferAccountingLine transferLine, String emplid) {
        List<EffortCertificationReport> effortReports = getEffortReportDefinitionsForLine(transferLine);
        List<EffortCertificationReport> openEffortReports = getOpenReportDefinitions(effortReports);

        return effortCertificationService.isEmployeeWithOpenCertification(openEffortReports, emplid);
    }

    /**
     * Adds the line amount to the given map that contains the total transfer amount for the account and period.
     *
     * @param accountPeriodTransfer - map holding the total transfers
     * @param effortReport          - open report for transfer line
     * @param transferLine          - line with amount to add
     */
    protected void addAccountTransferAmount(Map<String, KualiDecimal> accountPeriodTransfer, ExpenseTransferAccountingLine transferLine, EffortCertificationReport effortReport) {
        String transferKey = StringUtils.join(new Object[]{transferLine.getPayrollEndDateFiscalYear(), transferLine.getPayrollEndDateFiscalPeriodCode(), transferLine.getChartOfAccountsCode(), transferLine.getAccountNumber(), effortReport.getUniversityFiscalYear() + "-" + effortReport.getEffortCertificationReportNumber()}, ",");

        KualiDecimal transferAmount = transferLine.getAmount().abs();
        if (transferLine instanceof ExpenseTransferSourceAccountingLine) {
            transferAmount = transferAmount.negated();
        }

        if (accountPeriodTransfer.containsKey(transferKey)) {
            transferAmount = transferAmount.add(accountPeriodTransfer.get(transferKey));
        }

        accountPeriodTransfer.put(transferKey, transferAmount);
    }

    /**
     * Gets open or closed report definitions for line pay period and pay type.
     *
     * @param transferLine - line to pull pay period and type from
     * @return - open or closed effort reports for period and type
     */
    protected List<EffortCertificationReport> getEffortReportDefinitionsForLine(ExpenseTransferAccountingLine transferLine) {
        Integer payFiscalYear = transferLine.getPayrollEndDateFiscalYear();
        String payFiscalPeriodCode = transferLine.getPayrollEndDateFiscalPeriodCode();
        String positionObjectGroupCode = transferLine.getLaborObject().getPositionObjectGroupCode();

        return effortCertificationService.findReportDefinitionsForPeriod(payFiscalYear, payFiscalPeriodCode, positionObjectGroupCode);
    }

    /**
     * Verfies the given tranfer line contains the necessary data for performing the effort validations.
     *
     * @param transferLine - line to check
     */
    protected boolean containsNecessaryData(ExpenseTransferAccountingLine transferLine) {
        //KFSMI-798 - refreshNonUpdatableReferences() used instead of refresh(),
        //Both ExpenseTransferSourceAccountingLine and ExpenseTransferTargetAccountingLine do not have any updatable references
        transferLine.refreshNonUpdateableReferences();

        if (ObjectUtils.isNull(transferLine.getAccount()) || ObjectUtils.isNull(transferLine.getLaborObject()) || ObjectUtils.isNull(transferLine.getAmount())) {
            return false;
        }

        if (transferLine.getPayrollEndDateFiscalYear() == null || transferLine.getPayrollEndDateFiscalPeriodCode() == null) {
            return false;
        }

        return true;
    }

    /**
     * Determines whether the error should be associated with the source or target lines, and builds up parameters for error
     * message.
     *
     * @param errorKey     - key for the error message
     * @param transferLine - transfer line which had error
     * @param report       - report which conflicted with line
     */
    protected void putError(String errorKey, ExpenseTransferAccountingLine transferLine, EffortCertificationReport report) {
        String errorLines = KFSPropertyConstants.TARGET_ACCOUNTING_LINES;
        if (transferLine instanceof ExpenseTransferSourceAccountingLine) {
            errorLines = KFSPropertyConstants.SOURCE_ACCOUNTING_LINES;
        }

        String[] errorParameters = new String[3];
        errorParameters[0] = report.getUniversityFiscalYear() + "-" + report.getEffortCertificationReportNumber();
        errorParameters[1] = transferLine.getPayrollEndDateFiscalYear().toString();
        errorParameters[2] = transferLine.getPayrollEndDateFiscalPeriodCode();

        GlobalVariables.getMessageMap().putError(errorLines, errorKey, errorParameters);
    }

    /**
     * Sets the documentService attribute value.
     *
     * @param documentService The documentService to set.
     */
    public void setDocumentService(DocumentService documentService) {
        this.documentService = documentService;
    }

    /**
     * Sets the effortCertificationService attribute value.
     *
     * @param effortCertificationService The effortCertificationService to set.
     */
    public void setEffortCertificationService(EffortCertificationModuleService effortCertificationService) {
        this.effortCertificationService = effortCertificationService;
    }

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

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

}

