/*
 * 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.bc.document.web.struts;

import org.apache.commons.lang.StringUtils;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.kuali.kfs.kns.question.ConfirmationQuestion;
import org.kuali.kfs.kns.service.BusinessObjectDictionaryService;
import org.kuali.kfs.kns.util.KNSGlobalVariables;
import org.kuali.kfs.krad.util.GlobalVariables;
import org.kuali.kfs.krad.util.MessageMap;
import org.kuali.kfs.module.bc.BCConstants;
import org.kuali.kfs.module.bc.BCConstants.SynchronizationCheckType;
import org.kuali.kfs.module.bc.BCKeyConstants;
import org.kuali.kfs.module.bc.BCPropertyConstants;
import org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding;
import org.kuali.kfs.module.bc.document.BudgetConstructionDocument;
import org.kuali.kfs.module.bc.document.service.BudgetDocumentService;
import org.kuali.kfs.module.bc.document.service.LockService;
import org.kuali.kfs.module.bc.document.service.SalarySettingService;
import org.kuali.kfs.module.bc.document.validation.event.AddAppointmentFundingEvent;
import org.kuali.kfs.module.bc.document.validation.event.BudgetExpansionEvent;
import org.kuali.kfs.module.bc.document.validation.event.SaveSalarySettingEvent;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.ObjectUtil;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.rice.core.api.util.type.KualiInteger;
import org.kuali.rice.kim.api.identity.Person;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

import static org.kuali.kfs.module.bc.BCConstants.AppointmentFundingDurationCodes.LWPA;
import static org.kuali.kfs.module.bc.BCConstants.AppointmentFundingDurationCodes.LWPF;
import static org.kuali.kfs.module.bc.BCConstants.AppointmentFundingDurationCodes.NONE;

/**
 * the base struts action for the detail salary setting
 */
public abstract class DetailSalarySettingAction extends SalarySettingBaseAction {
    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DetailSalarySettingAction.class);

    private SalarySettingService salarySettingService = SpringContext.getBean(SalarySettingService.class);
    private BudgetDocumentService budgetDocumentService = SpringContext.getBean(BudgetDocumentService.class);
    private Person currentUser = GlobalVariables.getUserSession().getPerson();

    /**
     * @see org.kuali.kfs.module.bc.document.web.struts.SalarySettingBaseAction#execute(org.apache.struts.action.ActionMapping,
     * org.apache.struts.action.ActionForm, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
     */
    @Override
    public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
        ActionForward executeAction = null;

        try {
            executeAction = super.execute(mapping, form, request, response);
        } catch (Exception e) {
            // release all locks when encountering runtime exception
            DetailSalarySettingForm salarySettingForm = (DetailSalarySettingForm) form;
            if (!salarySettingForm.isViewOnlyEntry()) {
                salarySettingForm.releaseTransactionLocks();
                salarySettingForm.releasePositionAndFundingLocks();
            }

            LOG.fatal("Unexpected errors occurred.", e);

            // re-throw the exception
            throw new ServletException(e);
        }

        return executeAction;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.web.struts.BudgetExpansionAction#close(org.apache.struts.action.ActionMapping,
     * org.apache.struts.action.ActionForm, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
     */
    @Override
    public ActionForward close(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
        DetailSalarySettingForm salarySettingForm = (DetailSalarySettingForm) form;
        String buttonClicked = request.getParameter(KFSConstants.QUESTION_CLICKED_BUTTON);
        boolean isClose = StringUtils.equals(ConfirmationQuestion.YES, buttonClicked) || StringUtils.equals(ConfirmationQuestion.NO, buttonClicked);

        ActionForward closeActionForward;
        if (salarySettingForm.isViewOnlyEntry() || salarySettingForm.isSalarySettingClosed()) {
            closeActionForward = this.returnAfterClose(salarySettingForm, mapping, request, response);
        } else {
            closeActionForward = super.close(mapping, salarySettingForm, request, response);
        }

        // release all locks before closing the current expansion screen
        if (isClose && !salarySettingForm.isViewOnlyEntry() && salarySettingForm.isSalarySettingClosed()) {
            salarySettingForm.releasePositionAndFundingLocks();
            if (form instanceof PositionSalarySettingForm) {
                // handle case where there are no funding lines attached to position
                this.unlockPositionOnly((PositionSalarySettingForm) form);
            }
        }

        return closeActionForward;
    }

    /**
     * @see org.kuali.rice.kns.web.struts.action.KualiAction#refresh(org.apache.struts.action.ActionMapping,
     * org.apache.struts.action.ActionForm, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
     */
    @Override
    public ActionForward refresh(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
        super.refresh(mapping, form, request, response);

        DetailSalarySettingForm salarySettingForm = (DetailSalarySettingForm) form;
        String refreshCaller = request.getParameter(KFSConstants.REFRESH_CALLER);

        if (refreshCaller != null && refreshCaller.endsWith(KFSConstants.LOOKUPABLE_SUFFIX)) {
            // user could return from a lookup using a different fiscal year - do not allow it to change the funding line
            if (!salarySettingForm.getNewBCAFLine().getUniversityFiscalYear().equals(salarySettingForm.getUniversityFiscalYear())) {
                salarySettingForm.getNewBCAFLine().setUniversityFiscalYear(salarySettingForm.getUniversityFiscalYear());
            }
            if (salarySettingForm instanceof PositionSalarySettingForm) {
                // check that the object code is consistent with the position default object
                PositionSalarySettingForm ssPosForm = (PositionSalarySettingForm) salarySettingForm;
                if (!ssPosForm.getNewBCAFLine().getFinancialObjectCode().equals(ssPosForm.getBudgetConstructionPosition().getIuDefaultObjectCode())) {
                    ssPosForm.getNewBCAFLine().setFinancialObjectCode(ssPosForm.getBudgetConstructionPosition().getIuDefaultObjectCode());
                }
            }
            salarySettingForm.refreshBCAFLine(salarySettingForm.getNewBCAFLine());
        }

        return mapping.findForward(KFSConstants.MAPPING_BASIC);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.web.struts.SalarySettingBaseAction#save(org.apache.struts.action.ActionMapping,
     * org.apache.struts.action.ActionForm, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
     */
    @Override
    public ActionForward save(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
        DetailSalarySettingForm salarySettingForm = (DetailSalarySettingForm) form;
        List<PendingBudgetConstructionAppointmentFunding> savableAppointmentFundings = salarySettingForm.getSavableAppointmentFundings();
        List<PendingBudgetConstructionAppointmentFunding> appointmentFundings = salarySettingForm.getAppointmentFundings();

        if (savableAppointmentFundings == null || savableAppointmentFundings.isEmpty()) {
            KNSGlobalVariables.getMessageList().add(BCKeyConstants.MESSAGE_SALARY_SETTING_SAVED);
            return mapping.findForward(KFSConstants.MAPPING_BASIC);
        }

        MessageMap errorMap = GlobalVariables.getMessageMap();
        for (PendingBudgetConstructionAppointmentFunding savableFunding : savableAppointmentFundings) {
            String errorKeyPrefix = this.getErrorKeyPrefixOfAppointmentFundingLine(appointmentFundings, savableFunding);

            // retrieve corresponding document in advance in order to use the rule framework
            BudgetConstructionDocument document = budgetDocumentService.getBudgetConstructionDocument(savableFunding);
            if (document == null) {
                errorMap.putError(errorKeyPrefix, BCKeyConstants.ERROR_BUDGET_DOCUMENT_NOT_FOUND, savableFunding.getAppointmentFundingString());
                return mapping.findForward(KFSConstants.MAPPING_BASIC);
            }

            // bypass validation rules if the funding line has been marked as purged or deleted
            if (savableFunding.isPurged() || savableFunding.isAppointmentFundingDeleteIndicator()) {
                continue;
            }

            salarySettingService.recalculateDerivedInformation(savableFunding);

            // validate the savable appointment funding lines
            boolean isValid = this.invokeRules(new SaveSalarySettingEvent(KFSConstants.EMPTY_STRING, errorKeyPrefix, document, savableFunding, this.getSynchronizationCheckType()));
            if (!isValid) {
                return mapping.findForward(KFSConstants.MAPPING_BASIC);
            }
        }

        // acquire transaction locks for all funding lines
        boolean transactionLocked = salarySettingForm.acquireTransactionLocks(GlobalVariables.getMessageMap());
        if (!transactionLocked) {
            return mapping.findForward(KFSConstants.MAPPING_BASIC);
        }

        // test if Form is IncumbentSS and call saveSalarySetting() with a isSalarySettingByIncumbent true
        // so it knows when to remove purged funding position locks when it is the last line for the position
        if (form instanceof IncumbentSalarySettingForm) {
            salarySettingService.saveSalarySetting(savableAppointmentFundings, Boolean.TRUE);
        } else {
            salarySettingService.saveSalarySetting(savableAppointmentFundings, Boolean.FALSE);
        }

        // release all transaction locks
        salarySettingForm.releaseTransactionLocks();

        this.clearPurgedAppointmentFundings(appointmentFundings);

        KNSGlobalVariables.getMessageList().add(BCKeyConstants.MESSAGE_SALARY_SETTING_SAVED);
        return mapping.findForward(KFSConstants.MAPPING_BASIC);
    }

    /**
     * adds an appointment funding line to the set of existing funding lines
     */
    public ActionForward addAppointmentFundingLine(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
        DetailSalarySettingForm salarySettingForm = (DetailSalarySettingForm) form;
        List<PendingBudgetConstructionAppointmentFunding> appointmentFundings = salarySettingForm.getAppointmentFundings();

        PendingBudgetConstructionAppointmentFunding newAppointmentFunding = salarySettingForm.getNewBCAFLine();
        SpringContext.getBean(BusinessObjectDictionaryService.class).performForceUppercase(newAppointmentFunding);

        salarySettingForm.refreshBCAFLine(newAppointmentFunding);

        // setup a working appointment funding so that the default values can be applied
        PendingBudgetConstructionAppointmentFunding workingAppointmentFunding = new PendingBudgetConstructionAppointmentFunding();
        ObjectUtil.buildObject(workingAppointmentFunding, newAppointmentFunding);
        this.applyDefaultValuesIfEmpty(workingAppointmentFunding);

        MessageMap errorMap = GlobalVariables.getMessageMap();

        // retrieve corresponding document in advance in order to use the rule framework
        BudgetConstructionDocument document = budgetDocumentService.getBudgetConstructionDocument(workingAppointmentFunding);
        if (document == null) {
            errorMap.putError(BCPropertyConstants.NEW_BCAF_LINE, BCKeyConstants.ERROR_BUDGET_DOCUMENT_NOT_FOUND, workingAppointmentFunding.getAppointmentFundingString());
            return mapping.findForward(KFSConstants.MAPPING_BASIC);
        }

        // check special case where emplid is vacant and force funding duration to none
        String emplid = workingAppointmentFunding.getEmplid();
        if (StringUtils.isNotEmpty(emplid) && StringUtils.equals(emplid, BCConstants.VACANT_EMPLID)) {
            workingAppointmentFunding.setAppointmentFundingDurationCode(BCConstants.AppointmentFundingDurationCodes.NONE.durationCode);
        }
        salarySettingService.recalculateDerivedInformation(workingAppointmentFunding);

        // validate the new appointment funding line
        BudgetExpansionEvent addAppointmentFundingEvent = new AddAppointmentFundingEvent(KFSConstants.EMPTY_STRING, BCPropertyConstants.NEW_BCAF_LINE, document, appointmentFundings, workingAppointmentFunding, this.getSynchronizationCheckType());
        boolean isValid = this.invokeRules(addAppointmentFundingEvent);
        if (!isValid) {
            return mapping.findForward(KFSConstants.MAPPING_BASIC);
        }

        // set remaining flags
        boolean vacatable = salarySettingService.canBeVacant(appointmentFundings, workingAppointmentFunding);
        workingAppointmentFunding.setVacatable(vacatable);

        Integer fiscalYear = workingAppointmentFunding.getUniversityFiscalYear();
        String chartCode = workingAppointmentFunding.getChartOfAccountsCode();
        String objectCode = workingAppointmentFunding.getFinancialObjectCode();
        boolean hourlyPaid = salarySettingService.isHourlyPaidObject(fiscalYear, chartCode, objectCode);
        workingAppointmentFunding.setHourlyPaid(hourlyPaid);

        // update the access flags of the current funding line
        boolean accessModeUpdated = salarySettingForm.updateAccessMode(workingAppointmentFunding, errorMap);
        if (!accessModeUpdated) {
            return mapping.findForward(KFSConstants.MAPPING_BASIC);
        }

        // have no permission to do salary setting on the new line
        if (workingAppointmentFunding.isDisplayOnlyMode()) {
            errorMap.putError(BCPropertyConstants.NEW_BCAF_LINE, BCKeyConstants.ERROR_NO_SALARY_SETTING_PERMISSION, workingAppointmentFunding.getAppointmentFundingString());
            return mapping.findForward(KFSConstants.MAPPING_BASIC);
        }

        // acquire a lock for the new appointment funding line
        boolean gotLocks = salarySettingForm.acquirePositionAndFundingLocks(workingAppointmentFunding, errorMap);
        if (!gotLocks) {
            return mapping.findForward(KFSConstants.MAPPING_BASIC);
        }

        appointmentFundings.add(workingAppointmentFunding);
        salarySettingForm.setNewBCAFLine(salarySettingForm.createNewAppointmentFundingLine());

        return mapping.findForward(KFSConstants.MAPPING_BASIC);
    }

    // determine whether any active funding line is involved leave
    protected boolean hasFundingLineInvolvedLeave(List<PendingBudgetConstructionAppointmentFunding> activeAppointmentFundings) {
        for (PendingBudgetConstructionAppointmentFunding appointmentFunding : activeAppointmentFundings) {
            String leaveDurationCode = appointmentFunding.getAppointmentFundingDurationCode();

            if (!StringUtils.equals(leaveDurationCode, NONE.durationCode)) {
                return true;
            }
        }
        return false;
    }

    // determine whether any active funding line is involved in leave without pay
    protected boolean hasFundingLineInvolvedLeaveWithoutPay(List<PendingBudgetConstructionAppointmentFunding> activeAppointmentFundings) {
        for (PendingBudgetConstructionAppointmentFunding appointmentFunding : activeAppointmentFundings) {
            String leaveDurationCode = appointmentFunding.getAppointmentFundingDurationCode();

            if (StringUtils.equals(leaveDurationCode, LWPA.durationCode) || StringUtils.equals(leaveDurationCode, LWPF.durationCode)) {
                return true;
            }
        }
        return false;
    }

    // apply the default values to the certain fields when the fields are empty
    protected void applyDefaultValuesIfEmpty(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        if (StringUtils.isBlank(appointmentFunding.getSubAccountNumber())) {
            appointmentFunding.setSubAccountNumber(KFSConstants.getDashSubAccountNumber());
        }

        if (StringUtils.isBlank(appointmentFunding.getFinancialSubObjectCode())) {
            appointmentFunding.setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
        }

        if (appointmentFunding.getAppointmentTotalIntendedAmount() == null) {
            appointmentFunding.setAppointmentTotalIntendedAmount(KualiInteger.ZERO);
            appointmentFunding.setAppointmentTotalIntendedFteQuantity(BigDecimal.ZERO);
        }

        if (appointmentFunding.getAppointmentTotalIntendedFteQuantity() == null) {
            appointmentFunding.setAppointmentTotalIntendedFteQuantity(BigDecimal.ZERO);
        }
    }

    // clear the appointment funding lines that have been purged
    protected void clearPurgedAppointmentFundings(List<PendingBudgetConstructionAppointmentFunding> appointmentFundings) {
        List<PendingBudgetConstructionAppointmentFunding> purgedAppointmentFundings = new ArrayList<PendingBudgetConstructionAppointmentFunding>();
        for (PendingBudgetConstructionAppointmentFunding appointmentFunding : appointmentFundings) {
            if (appointmentFunding.isPurged()) {
                purgedAppointmentFundings.add(appointmentFunding);
            }
        }
        appointmentFundings.removeAll(purgedAppointmentFundings);
    }

    /**
     * unlock the position only, called as last action before a close or exit handling the case where there are no funding lines
     * attached yet.
     */
    protected void unlockPositionOnly(PositionSalarySettingForm positionSalarySettingForm) {

        Integer universityFiscalYear = positionSalarySettingForm.getBudgetConstructionPosition().getUniversityFiscalYear();
        String positionNumber = positionSalarySettingForm.getBudgetConstructionPosition().getPositionNumber();
        String principalId = GlobalVariables.getUserSession().getPerson().getPrincipalId();

        // unlock position
        SpringContext.getBean(LockService.class).unlockPosition(positionNumber, universityFiscalYear, principalId);

    }

    /**
     * This should return the SynchronizationCheckType based on the context. SynchronizationCheckType.POSN is used in
     * IncumbentSalarySetting and SynchronizationCheckType.EID is used in PositionSalarySetting
     *
     * @return
     */
    public abstract SynchronizationCheckType getSynchronizationCheckType();
}
