/*
 * 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.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.kuali.kfs.kns.util.KNSGlobalVariables;
import org.kuali.kfs.kns.util.MessageList;
import org.kuali.kfs.krad.service.BusinessObjectService;
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.LockStatus;
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.BudgetConstructionLockStatus;
import org.kuali.kfs.module.bc.businessobject.BudgetConstructionPosition;
import org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding;
import org.kuali.kfs.module.bc.document.service.LockService;
import org.kuali.kfs.module.bc.document.service.SalarySettingService;
import org.kuali.kfs.module.bc.service.BudgetConstructionPositionService;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.context.SpringContext;

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

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

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

    private SalarySettingService salarySettingService = SpringContext.getBean(SalarySettingService.class);
    private BusinessObjectService businessObjectService = SpringContext.getBean(BusinessObjectService.class);
    private BudgetConstructionPositionService budgetConstructionPositionService = SpringContext.getBean(BudgetConstructionPositionService.class);

    /**
     * @see org.kuali.kfs.module.bc.document.web.struts.SalarySettingBaseAction#loadExpansionScreen(org.apache.struts.action.ActionMapping,
     * org.apache.struts.action.ActionForm, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
     */
    @Override
    public ActionForward loadExpansionScreen(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
        PositionSalarySettingForm positionSalarySettingForm = (PositionSalarySettingForm) form;
        MessageMap errorMap;
        if (positionSalarySettingForm.isBudgetByAccountMode()) {
            errorMap = positionSalarySettingForm.getCallBackErrors();
        } else {
            errorMap = GlobalVariables.getMessageMap();
        }

        // update the position record if required
        ActionForward resyncAction = this.resyncPositionBeforeSalarySetting(mapping, form, request, response);
        if (resyncAction != null) {
            return resyncAction;
        }

        Map<String, Object> fieldValues = positionSalarySettingForm.getKeyMapOfSalarySettingItem();
        BudgetConstructionPosition budgetConstructionPosition = businessObjectService.findByPrimaryKey(BudgetConstructionPosition.class, fieldValues);
        if (budgetConstructionPosition == null) {
            String positionNumber = (String) fieldValues.get(KFSPropertyConstants.POSITION_NUMBER);
            String fiscalYear = (String) fieldValues.get(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR);
            errorMap.putError(KFSConstants.GLOBAL_MESSAGES, BCKeyConstants.ERROR_POSITION_NOT_FOUND, positionNumber, fiscalYear);

            if (positionSalarySettingForm.isBudgetByAccountMode()) {
                return this.returnToCaller(mapping, form, request, response);
            } else {
                this.cleanupAnySessionForm(mapping, request);
                return mapping.findForward(BCConstants.MAPPING_ORGANIZATION_SALARY_SETTING_RETURNING);
            }
        }

        if (!positionSalarySettingForm.isViewOnlyEntry()) {

            // Lock the position even if there are no current funding lines attached
            Integer universityFiscalYear = budgetConstructionPosition.getUniversityFiscalYear();
            String positionNumber = budgetConstructionPosition.getPositionNumber();
            String principalId = GlobalVariables.getUserSession().getPerson().getPrincipalId();

            // attempt to lock position
            BudgetConstructionLockStatus bcLockStatus = SpringContext.getBean(LockService.class).lockPosition(positionNumber, universityFiscalYear, principalId);
            if (!bcLockStatus.getLockStatus().equals(BCConstants.LockStatus.SUCCESS)) {
                errorMap.putError(KFSConstants.GLOBAL_ERRORS, BCKeyConstants.ERROR_FAIL_TO_LOCK_POSITION, "Position Number:" + budgetConstructionPosition.getPositionNumber() + ", Fiscal Year:" + budgetConstructionPosition.getUniversityFiscalYear().toString() + ", Desc:" + budgetConstructionPosition.getPositionDescription() + ", Locked By:" + budgetConstructionPosition.getPositionLockUser().getPrincipalName());
                if (positionSalarySettingForm.isBudgetByAccountMode()) {
                    return this.returnToCaller(mapping, form, request, response);
                } else {
                    this.cleanupAnySessionForm(mapping, request);
                    return mapping.findForward(BCConstants.MAPPING_ORGANIZATION_SALARY_SETTING_RETURNING);
                }
            }
        }

        positionSalarySettingForm.setBudgetConstructionPosition(budgetConstructionPosition);
        if (positionSalarySettingForm.isSingleAccountMode()) {
            positionSalarySettingForm.pickAppointmentFundingsForSingleAccount();
        }

        // acquire position and funding locks for the associated funding lines
        if (!positionSalarySettingForm.isViewOnlyEntry()) {
            positionSalarySettingForm.postProcessBCAFLines();
            positionSalarySettingForm.setNewBCAFLine(positionSalarySettingForm.createNewAppointmentFundingLine());

            boolean accessModeUpdated = positionSalarySettingForm.updateAccessMode(errorMap);
            if (!accessModeUpdated) {
                this.unlockPositionOnly(positionSalarySettingForm);
                if (positionSalarySettingForm.isBudgetByAccountMode()) {
                    return this.returnToCaller(mapping, form, request, response);
                } else {
                    this.cleanupAnySessionForm(mapping, request);
                    return mapping.findForward(BCConstants.MAPPING_ORGANIZATION_SALARY_SETTING_RETURNING);
                }
            }

            boolean gotLocks = positionSalarySettingForm.acquirePositionAndFundingLocks(errorMap);
            if (!gotLocks) {
                this.unlockPositionOnly(positionSalarySettingForm);
                if (positionSalarySettingForm.isBudgetByAccountMode()) {
                    return this.returnToCaller(mapping, form, request, response);
                } else {
                    this.cleanupAnySessionForm(mapping, request);
                    return mapping.findForward(BCConstants.MAPPING_ORGANIZATION_SALARY_SETTING_RETURNING);
                }
            }
        }

        return mapping.findForward(KFSConstants.MAPPING_BASIC);
    }

    /**
     * enable to send warning after saving
     *
     * @see org.kuali.kfs.module.bc.document.web.struts.DetailSalarySettingAction#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 {
        ActionForward saveAction = super.save(mapping, form, request, response);

        PositionSalarySettingForm positionSalarySettingForm = (PositionSalarySettingForm) form;
        this.sendWarnings(positionSalarySettingForm, KNSGlobalVariables.getMessageList());

        return saveAction;
    }

    /**
     * send warning messsages back to the caller
     *
     * @param positionSalarySettingForm the given position salary setting form
     * @param warnings                  the warning list that can hold the warning messages if any
     */
    public void sendWarnings(PositionSalarySettingForm positionSalarySettingForm, MessageList warnings) {
        List<PendingBudgetConstructionAppointmentFunding> activeAppointmentFundings = positionSalarySettingForm.getActiveFundingLines();
        if (activeAppointmentFundings == null || activeAppointmentFundings.isEmpty()) {
            return;
        }

        boolean hasFundingLineInvolveLeave = this.hasFundingLineInvolvedLeave(activeAppointmentFundings);
        BudgetConstructionPosition budgetConstructionPosition = positionSalarySettingForm.getBudgetConstructionPosition();

        BigDecimal positionFte = budgetConstructionPosition.getPositionFullTimeEquivalency().setScale(2, BigDecimal.ROUND_HALF_UP);
        BigDecimal requestedFteQuantityTotal = positionSalarySettingForm.getAppointmentRequestedFteQuantityTotal().setScale(2, BigDecimal.ROUND_HALF_UP);
        if (!hasFundingLineInvolveLeave && requestedFteQuantityTotal.compareTo(positionFte) != 0) {
            warnings.add(BCKeyConstants.WARNING_FTE_NOT_EQUAL);
        }

        BigDecimal positionStandardHours = budgetConstructionPosition.getPositionStandardHoursDefault().setScale(2, BigDecimal.ROUND_HALF_UP);
        BigDecimal requestedStandardHoursTotal = positionSalarySettingForm.getAppointmentRequestedStandardHoursTotal().setScale(2, BigDecimal.ROUND_HALF_UP);
        if (!hasFundingLineInvolveLeave && requestedStandardHoursTotal.compareTo(positionStandardHours) != 0) {
            warnings.add(BCKeyConstants.WARNING_WORKING_HOUR_NOT_EQUAL);
        }

        if (positionSalarySettingForm.isPendingPositionSalaryChange()) {
            warnings.add(BCKeyConstants.WARNING_RECALCULATE_NEEDED);
        }
    }

    /**
     * Recalculates all rows where the position change flags are set and the row is edit-able and active. Sets funding months, FTE,
     * CSF FTE, and normalizes biweekly request amounts, where appropriate This action is called from the global calculate button.
     *
     * @param mapping
     * @param form
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    public ActionForward recalculateAllSalarySettingLines(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
        PositionSalarySettingForm positionSalarySettingForm = (PositionSalarySettingForm) form;

        List<PendingBudgetConstructionAppointmentFunding> appointmentFundings = positionSalarySettingForm.getActiveFundingLines();
        for (PendingBudgetConstructionAppointmentFunding appointmentFunding : appointmentFundings) {
            if (!appointmentFunding.isDisplayOnlyMode()) {
                this.recalculateSalarySettingLine(appointmentFunding);
            }
        }
        return mapping.findForward(KFSConstants.MAPPING_BASIC);
    }

    /**
     * Recalculates a single row where the position change flags are set and the row is edit-able and active. Sets funding months,
     * FTE, CSF FTE, and normalizes biweekly request amounts, where appropriate This action is called from the row action calculate
     * button.
     *
     * @param mapping
     * @param form
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    public ActionForward recalculateSalarySettingLine(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
        PositionSalarySettingForm positionSalarySettingForm = (PositionSalarySettingForm) form;
        PendingBudgetConstructionAppointmentFunding appointmentFunding = this.getSelectedFundingLine(request, positionSalarySettingForm);

        this.recalculateSalarySettingLine(appointmentFunding);

        return mapping.findForward(KFSConstants.MAPPING_BASIC);
    }

    /**
     * Recalculates a PendingBudgetConstructionAppointmentFunding. Sets funding months, FTE, CSF FTE, and normalizes biweekly
     * request amounts, where appropriate
     *
     * @param appointmentFunding
     */
    protected void recalculateSalarySettingLine(PendingBudgetConstructionAppointmentFunding appointmentFunding) {

        // do the recalc on the passed line and reset the indicator
        // there are no rule checks involved in this operation
        if (appointmentFunding.isPositionSalaryChangeIndicator()) {
            appointmentFunding.setPositionSalaryChangeIndicator(Boolean.FALSE);

            // check if months needs reset
            if (appointmentFunding.getAppointmentFundingDurationCode().equals(NONE.durationCode)) {
                if (!appointmentFunding.getAppointmentFundingMonth().equals(appointmentFunding.getBudgetConstructionPosition().getIuNormalWorkMonths())) {
                    appointmentFunding.setAppointmentFundingMonth(appointmentFunding.getBudgetConstructionPosition().getIuNormalWorkMonths());
                }
            }

            // recalc request fte and if hourly, normalize request amount and hourly rate
            salarySettingService.recalculateDerivedInformation(appointmentFunding);

        }


        // doing this just to do cleanup of the default object change flag
        // the rules will force the user to mark the line delete
        // if the default object doesn't match with the line object when saving
        if (appointmentFunding.isPositionObjectChangeIndicator()) {
            appointmentFunding.setPositionObjectChangeIndicator(Boolean.FALSE);
        }

    }

    /**
     * @see org.kuali.kfs.module.bc.document.web.struts.SalarySettingBaseAction#getFundingAwareObjectName()
     */
    @Override
    protected String getFundingAwareObjectName() {
        return BCPropertyConstants.BUDGET_CONSTRUCTION_POSITION;
    }

    /**
     * resyn position brefore performing salary setting
     */
    private ActionForward resyncPositionBeforeSalarySetting(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
        PositionSalarySettingForm positionSalarySettingForm = (PositionSalarySettingForm) form;

        if (!positionSalarySettingForm.isRefreshPositionBeforeSalarySetting()) {
            return null;
        }

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

        // attempt to lock position and associated funding
        BudgetConstructionLockStatus bcLockStatus = SpringContext.getBean(LockService.class).lockPositionAndActiveFunding(universityFiscalYear, positionNumber, principalId);
        if (!bcLockStatus.getLockStatus().equals(BCConstants.LockStatus.SUCCESS)) {
            GlobalVariables.getMessageMap().putError(KFSConstants.GLOBAL_ERRORS, BCKeyConstants.ERROR_POSITION_LOCK_NOT_OBTAINED, new String[]{universityFiscalYear.toString(), positionNumber});
            return this.returnToCaller(mapping, form, request, response);
        }

        try {
            budgetConstructionPositionService.refreshPositionFromExternal(universityFiscalYear, positionNumber);
        } finally {
            // release locks
            LockStatus lockStatus = SpringContext.getBean(LockService.class).unlockPositionAndActiveFunding(universityFiscalYear, positionNumber, principalId);
            if (!lockStatus.equals(BCConstants.LockStatus.SUCCESS)) {
                LOG.error(String.format("unable to unlock position and active funding records: %s, %s, %s", universityFiscalYear, positionNumber, principalId));
                throw new RuntimeException(String.format("unable to unlock position and active funding records: %s, %s, %s", universityFiscalYear, positionNumber, principalId));
            }
        }

        return null;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.web.struts.DetailSalarySettingAction#getSynchronizationCheckType()
     */
    @Override
    public SynchronizationCheckType getSynchronizationCheckType() {
        return SynchronizationCheckType.EID;
    }
}
