/*
 * 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.kns.lookup;

import org.apache.commons.codec.binary.Base64;
import org.kuali.kfs.kns.web.ui.ResultRow;
import org.kuali.kfs.krad.bo.LookupResults;
import org.kuali.kfs.krad.bo.MultipleValueLookupMetadata;
import org.kuali.kfs.krad.bo.SelectedObjectIds;
import org.kuali.kfs.krad.dao.PersistedLookupMetadataDao;
import org.kuali.kfs.krad.exception.AuthorizationException;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.krad.util.KRADConstants;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.rice.core.api.CoreApiServiceLocator;
import org.kuali.rice.krad.bo.BusinessObject;
import org.springframework.transaction.annotation.Transactional;

import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

@Transactional
public class LookupResultsServiceImpl implements org.kuali.kfs.kns.lookup.LookupResultsService {
    private BusinessObjectService businessObjectService;
    private PersistedLookupMetadataDao persistedLookupMetadataDao;
    private LookupResultsSupportStrategyService persistableBusinessObjectSupportStrategy;
    private LookupResultsSupportStrategyService dataDictionarySupportStrategy;

    /**
     * @see org.kuali.rice.krad.lookup.LookupResultsService#persistResultsTable(java.lang.String, java.util.List, java.lang.String)
     */
    public void persistResultsTable(String lookupResultsSequenceNumber, List<ResultRow> resultTable, String personId) throws Exception {
        String resultTableString = new String(Base64.encodeBase64(ObjectUtils.toByteArray(resultTable)));

        Timestamp now = CoreApiServiceLocator.getDateTimeService().getCurrentTimestamp();

        LookupResults lookupResults = retrieveLookupResults(lookupResultsSequenceNumber);
        if (lookupResults == null) {
            lookupResults = new LookupResults();
            lookupResults.setLookupResultsSequenceNumber(lookupResultsSequenceNumber);
        }
        lookupResults.setLookupResultsSequenceNumber(lookupResultsSequenceNumber);
        lookupResults.setLookupPersonId(personId);
        lookupResults.setSerializedLookupResults(resultTableString);
        lookupResults.setLookupDate(now);
        businessObjectService.save(lookupResults);
    }

    /**
     * @see org.kuali.rice.krad.lookup.LookupResultsService#persistSelectedObjectIds(java.lang.String, java.util.Set, java.lang.String)
     */
    public void persistSelectedObjectIds(String lookupResultsSequenceNumber, Set<String> selectedObjectIds, String personId) throws Exception {
        SelectedObjectIds selectedObjectIdsBO = retrieveSelectedObjectIds(lookupResultsSequenceNumber);
        if (selectedObjectIdsBO == null) {
            selectedObjectIdsBO = new SelectedObjectIds();
            selectedObjectIdsBO.setLookupResultsSequenceNumber(lookupResultsSequenceNumber);
        }
        selectedObjectIdsBO.setLookupResultsSequenceNumber(lookupResultsSequenceNumber);
        selectedObjectIdsBO.setLookupPersonId(personId);
        selectedObjectIdsBO.setSelectedObjectIds(
            org.kuali.kfs.kns.lookup.LookupUtils.convertSetOfObjectIdsToString(selectedObjectIds));
        selectedObjectIdsBO.setLookupDate(CoreApiServiceLocator.getDateTimeService().getCurrentTimestamp());
        businessObjectService.save(selectedObjectIdsBO);
    }

    /**
     * Retrieves the LookupResults BO with the given sequence number.  Does not check authentication.
     *
     * @param lookupResultsSequenceNumber
     * @return
     * @throws Exception
     */
    protected LookupResults retrieveLookupResults(String lookupResultsSequenceNumber) throws Exception {
        Map<String, String> queryCriteria = new HashMap<String, String>();
        queryCriteria.put(KRADConstants.LOOKUP_RESULTS_SEQUENCE_NUMBER, lookupResultsSequenceNumber);
        LookupResults lookupResults = (LookupResults) businessObjectService.findByPrimaryKey(LookupResults.class, queryCriteria);

        return lookupResults;
    }

    /**
     * Retrieves the SelectedObjectIds BO with the given sequence number.  Does not check authentication.
     *
     * @param lookupResultsSequenceNumber
     * @return
     * @throws Exception
     */
    protected SelectedObjectIds retrieveSelectedObjectIds(String lookupResultsSequenceNumber) throws Exception {
        Map<String, String> queryCriteria = new HashMap<String, String>();
        queryCriteria.put(KRADConstants.LOOKUP_RESULTS_SEQUENCE_NUMBER, lookupResultsSequenceNumber);
        SelectedObjectIds selectedObjectIds = (SelectedObjectIds) businessObjectService.findByPrimaryKey(SelectedObjectIds.class, queryCriteria);

        return selectedObjectIds;
    }

    /**
     * @see org.kuali.rice.krad.lookup.LookupResultsService#isAuthorizedToAccessLookupResults(java.lang.String, java.lang.String)
     */
    public boolean isAuthorizedToAccessLookupResults(String lookupResultsSequenceNumber, String personId) {
        try {
            LookupResults lookupResults = retrieveLookupResults(lookupResultsSequenceNumber);
            return isAuthorizedToAccessLookupResults(lookupResults, personId);
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * Returns whether the user ID parameter is allowed to view the results.
     *
     * @param lookupResults
     * @param personId
     * @return
     */
    protected boolean isAuthorizedToAccessLookupResults(LookupResults lookupResults, String personId) {
        return isAuthorizedToAccessMultipleValueLookupMetadata(lookupResults, personId);
    }

    /**
     * @see org.kuali.rice.krad.lookup.LookupResultsService#isAuthorizedToAccessSelectedObjectIds(java.lang.String, java.lang.String)
     */
    public boolean isAuthorizedToAccessSelectedObjectIds(String lookupResultsSequenceNumber, String personId) {
        try {
            SelectedObjectIds selectedObjectIds = retrieveSelectedObjectIds(lookupResultsSequenceNumber);
            return isAuthorizedToAccessSelectedObjectIds(selectedObjectIds, personId);
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * Returns whether the user ID parameter is allowed to view the selected object IDs
     *
     * @param selectedObjectIds
     * @param personId
     * @return
     */
    protected boolean isAuthorizedToAccessSelectedObjectIds(SelectedObjectIds selectedObjectIds, String personId) {
        return isAuthorizedToAccessMultipleValueLookupMetadata(selectedObjectIds, personId);
    }


    /**
     * @see org.kuali.rice.krad.lookup.LookupResultsService#retrieveResultsTable(java.lang.String, java.lang.String)
     */
    public List<ResultRow> retrieveResultsTable(String lookupResultsSequenceNumber, String personId) throws Exception {
        LookupResults lookupResults = retrieveLookupResults(lookupResultsSequenceNumber);
        if (!isAuthorizedToAccessLookupResults(lookupResults, personId)) {
            // TODO: use the other identifier
            throw new AuthorizationException(personId, "retrieve lookup results", "lookup sequence number " + lookupResultsSequenceNumber);
        }
        List<ResultRow> resultTable = (List<ResultRow>) ObjectUtils.fromByteArray(Base64.decodeBase64(lookupResults.getSerializedLookupResults().getBytes()));
        return resultTable;
    }

    /**
     * Figures out which support strategy to defer to and uses that service to retrieve the results; if the bo class doesn't qualify with any support strategy, an exception is thrown.  A nasty one, too.
     *
     * @see org.kuali.rice.krad.lookup.LookupResultsService#retrieveSelectedResultBOs(java.lang.String, java.lang.Class, java.lang.String)
     */
    public <T extends BusinessObject> Collection<T> retrieveSelectedResultBOs(String lookupResultsSequenceNumber, Class<T> boClass, String personId) throws Exception {
        final LookupResultsSupportStrategyService supportService = getQualifingSupportStrategy(boClass);
        if (supportService == null) {
            throw new RuntimeException("BusinessObject class " + boClass.getName() + " cannot be used within a multiple value lookup; it either needs to be a PersistableBusinessObject or have both its primary keys and a lookupable defined in its data dictionary entry");
        }

        SelectedObjectIds selectedObjectIds = retrieveSelectedObjectIds(lookupResultsSequenceNumber);

        if (!isAuthorizedToAccessSelectedObjectIds(selectedObjectIds, personId)) {
            // TODO: use the other identifier
            throw new AuthorizationException(personId, "retrieve lookup results", "lookup sequence number " + lookupResultsSequenceNumber);
        }

        Set<String> setOfSelectedObjIds = LookupUtils
            .convertStringOfObjectIdsToSet(selectedObjectIds.getSelectedObjectIds());

        if (setOfSelectedObjIds.isEmpty()) {
            // OJB throws exception if querying on empty set
            return new ArrayList<T>();
        }

        return supportService.retrieveSelectedResultBOs(boClass, setOfSelectedObjIds);
    }

    /**
     * Given the business object class, determines the best qualifying LookupResultsSupportStrategyService to use
     *
     * @param boClass a business object class
     * @return an LookupResultsSupportStrategyService implementation, or null if no qualifying strategies could be found
     */
    protected LookupResultsSupportStrategyService getQualifingSupportStrategy(Class boClass) {
        if (getPersistableBusinessObjectSupportStrategy().qualifiesForStrategy(boClass)) {
            return getPersistableBusinessObjectSupportStrategy();
        } else if (getDataDictionarySupportStrategy().qualifiesForStrategy(boClass)) {
            return getDataDictionarySupportStrategy();
        }
        return null;
    }

    /**
     * @see org.kuali.rice.krad.lookup.LookupResultsService#clearPersistedLookupResults(java.lang.String)
     */
    public void clearPersistedLookupResults(String lookupResultsSequenceNumber) throws Exception {
        LookupResults lookupResults = retrieveLookupResults(lookupResultsSequenceNumber);
        if (lookupResults != null) {
            businessObjectService.delete(lookupResults);
        }
    }

    /**
     * @see org.kuali.rice.krad.lookup.LookupResultsService#clearPersistedSelectedObjectIds(java.lang.String)
     */
    public void clearPersistedSelectedObjectIds(String lookupResultsSequenceNumber) throws Exception {
        SelectedObjectIds selectedObjectIds = retrieveSelectedObjectIds(lookupResultsSequenceNumber);
        if (selectedObjectIds != null) {
            businessObjectService.delete(selectedObjectIds);
        }
    }

    /**
     * Figures out which LookupResultsServiceSupportStrategy to defer to, and uses that to get the lookup id
     *
     * @see org.kuali.rice.krad.lookup.LookupResultsService#getLookupId(org.kuali.rice.krad.bo.BusinessObject)
     */
    public String getLookupId(BusinessObject businessObject) {
        final LookupResultsSupportStrategyService supportService = getQualifingSupportStrategy(businessObject.getClass());
        if (supportService == null) {
            return null; // this may happen quite often, so let's just return null - no exception here
        }
        return supportService.getLookupIdForBusinessObject(businessObject);
    }

    public BusinessObjectService getBusinessObjectService() {
        return businessObjectService;
    }

    public void setBusinessObjectService(BusinessObjectService businessObjectService) {
        this.businessObjectService = businessObjectService;
    }

    /**
     * Determines whether the passed in user ID is allowed to view the lookup metadata (object IDs or results table)
     *
     * @param mvlm
     * @param personId
     * @return
     */
    protected boolean isAuthorizedToAccessMultipleValueLookupMetadata(MultipleValueLookupMetadata mvlm, String personId) {
        return personId.equals(mvlm.getLookupPersonId());
    }


    public void deleteOldLookupResults(Timestamp expirationDate) {
        persistedLookupMetadataDao.deleteOldLookupResults(expirationDate);

    }

    public void deleteOldSelectedObjectIds(Timestamp expirationDate) {
        persistedLookupMetadataDao.deleteOldSelectedObjectIds(expirationDate);
    }

    public PersistedLookupMetadataDao getPersistedLookupMetadataDao() {
        return persistedLookupMetadataDao;
    }

    public void setPersistedLookupMetadataDao(PersistedLookupMetadataDao persistedLookupMetadataDao) {
        this.persistedLookupMetadataDao = persistedLookupMetadataDao;
    }

    /**
     * @return the persistableBusinessObjectSupportStrategy
     */
    public LookupResultsSupportStrategyService getPersistableBusinessObjectSupportStrategy() {
        return this.persistableBusinessObjectSupportStrategy;
    }

    /**
     * @return the dataDictionarySupportStrategy
     */
    public LookupResultsSupportStrategyService getDataDictionarySupportStrategy() {
        return this.dataDictionarySupportStrategy;
    }

    /**
     * @param persistableBusinessObjectSupportStrategy the persistableBusinessObjectSupportStrategy to set
     */
    public void setPersistableBusinessObjectSupportStrategy(
        LookupResultsSupportStrategyService persistableBusinessObjectSupportStrategy) {
        this.persistableBusinessObjectSupportStrategy = persistableBusinessObjectSupportStrategy;
    }

    /**
     * @param dataDictionarySupportStrategy the dataDictionarySupportStrategy to set
     */
    public void setDataDictionarySupportStrategy(
        LookupResultsSupportStrategyService dataDictionarySupportStrategy) {
        this.dataDictionarySupportStrategy = dataDictionarySupportStrategy;
    }

}

