/********************************************************************
 * Copyright © 2004-2005 EDS. All rights reserved
 ********************************************************************/
package gov.va.med.esr.service.impl;

// Java Classes
import gov.va.med.esr.common.batchprocess.PCMMImportData;
import gov.va.med.esr.common.model.CommonEntityKeyFactory;
import gov.va.med.esr.common.model.lookup.FunctionalGroup;
import gov.va.med.esr.common.model.lookup.SourceDesignation;
import gov.va.med.esr.common.model.lookup.VAFacility;
import gov.va.med.esr.common.model.lookup.WkfCaseType;
import gov.va.med.esr.common.model.person.HealthCareProvider;
import gov.va.med.esr.common.model.person.PatientProviderAssignmentLite;
import gov.va.med.esr.common.model.person.Person;
import gov.va.med.esr.common.model.person.PreferredFacility;
import gov.va.med.esr.common.model.person.PreferredFacilityLite;
import gov.va.med.esr.common.model.person.PreferredFacilityPerson;
import gov.va.med.esr.common.model.person.id.PersonEntityKey;
import gov.va.med.esr.common.model.person.id.PersonIdEntityKey;
import gov.va.med.esr.common.model.person.id.VPIDEntityKey;
import gov.va.med.esr.common.model.workload.WorkflowCaseInfo;
import gov.va.med.esr.common.persistent.lookup.VAFacilityDAO;
import gov.va.med.esr.common.persistent.person.HealthCareProviderDAO;
import gov.va.med.esr.common.persistent.person.PreferredFacilityDAO;
import gov.va.med.esr.service.CommsLogService;
import gov.va.med.esr.service.HandBookService;
import gov.va.med.esr.service.PersonIdentityTraits;
import gov.va.med.esr.service.PreferredFacilityService;
import gov.va.med.esr.service.UnknownLookupCodeException;
import gov.va.med.esr.service.UnknownLookupTypeException;
import gov.va.med.esr.service.trigger.MessageCaseTriggerEvent;
import gov.va.med.esr.service.trigger.PersonTrigger;
import gov.va.med.esr.service.trigger.PersonTriggerEvent;
import gov.va.med.fw.persistent.DAOException;
import gov.va.med.fw.rule.RuleValidationException;
import gov.va.med.fw.service.ServiceException;
import gov.va.med.fw.service.trigger.TriggerEvent;
import gov.va.med.fw.service.trigger.TriggerRouter;
import gov.va.med.fw.util.DateUtils;
import gov.va.med.fw.util.StopWatchLogger;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Comparator;
import java.util.Collections;

import org.apache.commons.lang.ClassUtils;

/**
 * Class that implements Preferred Facility services
 */
public class PreferredFacilityServiceImpl extends AbstractRuleAwareServiceImpl implements PreferredFacilityService {

	private PreferredFacilityDAO preferredFacilityDAO = null;
	private HealthCareProviderDAO providerDAO = null;
	private TriggerRouter triggerRouter;
	private boolean nightlyUpdate = false;  // By default the initial seeding job does not trigger Z11
	private static final String INPUT_TRANSACTION_TYPE = "D";
	private CommsLogService commsLogService;
	private HandBookService handBookService;
	
	public PreferredFacilityPerson findPreferredFacilityPerson(String dfn, String stationNumber) throws ServiceException {
		
		StopWatchLogger watch = null;
		if (logger.isDebugEnabled()) {
			watch = new StopWatchLogger(ClassUtils.getShortClassName(getClass())
					+ " findPreferredFacilityPerson");
			if (watch != null) {
				watch.start();
			}
		}

		//PersonIdentityTraits traits = getPsDelegateService().getIdentityTraits(dfn, stationNumber);	
		 //CR 11360- Replacing EJB calls
		PersonIdentityTraits traits = getPsDelegateService().getIdentityTraitsWithCompositeCall(dfn, stationNumber);		
		VPIDEntityKey vpid = traits.getVpid();
		PreferredFacilityPerson pfPerson = null;
		
		try {
			pfPerson = preferredFacilityDAO.getPreferredFacilityPersonByVPID(vpid);
		}  catch (DAOException e) {
			throw new ServiceException("Unable to retrieve PreferredFacilityPerson by VPID", e);
		} finally {
			if (logger.isDebugEnabled() && watch != null)
				watch.stopAndLog();
		}
		
		pfPerson.setIdentityTraits(traits);
		
		return pfPerson;
	}

	public PreferredFacilityPerson savePreferredFacilityPerson(PreferredFacilityPerson person) throws ServiceException {

		StopWatchLogger watch = null;
		if (logger.isDebugEnabled()) {
			watch = new StopWatchLogger(ClassUtils.getShortClassName(getClass())
					+ " savePreferredFacilityPerson");
			if (watch != null) {
				watch.start();
			}
		}
		
		try {
			preferredFacilityDAO.saveObject(person);
		} catch (DAOException e) {
			throw new ServiceException("Failed to persist a person", e);
		} finally {
			if (logger.isDebugEnabled() && watch != null)
				watch.stopAndLog();
		}
		return person;
	}
	
	public void processPCMMData(PCMMImportData incoming)
			throws ServiceException {

		PreferredFacilityPerson onFilePerson = findPreferredFacilityPerson(
				incoming.getPatientIEN(), incoming.getPatientSta3n());

		if (onFilePerson == null) {
			throw new ServiceException("Person not found for stationNumber = "
					+ incoming.getPatientSta3n() + ", DFN = "
					+ incoming.getPatientIEN());
		}

		boolean pfUpdated = false;
		HealthCareProvider provider = null;

		try {
			// CCR11076 -- we'll need to test if parent facilities changed, so maintain
			// a copy of the pristine facilities now.
			// CCR11668 -- we'll need to test if any a new PF is added, or an existing PF is replaced by a different PF.
			// so maintain a copy of the pristine facilities now.
			//Set pristinePFs = new HashSet(onFilePerson.getPreferredFacilities());
			Set pristinePFs = new HashSet(getPreferredFacilityLiteSites(onFilePerson));
			VAFacility pristineMostRecentPF = onFilePerson.getMostRecentPreferredFacility();
				
			if (INPUT_TRANSACTION_TYPE.equals(incoming.getTransactionType())) {
				pfUpdated = deleteData(incoming, onFilePerson);
			} else {
				provider = addOrUpdateProvider(incoming);
				if (provider != null) {
					getProviderDAO().saveObject(provider);
				}

				addOrUpdateAssignment(incoming, onFilePerson, provider);
				pfUpdated = addOrUpdatePreferredFacility(incoming, onFilePerson);
			}

			// finally set the calculated most recent preferred facility
			// onto Person object
			setCalculatedMostRecentPreferredFacility(onFilePerson);

			savePreferredFacilityPerson(onFilePerson);

			// R-29: Modify ESR to trigger a message to be sent to VistA when a PF
			// has been assigned/modified.
	
			// Set a trigger to send a Z11 for this person ID.
			// Only the nightly job will trigger Z11 in a single process; the
			// initial seeding job will
			// require a separate process, followed by a stored procedure where null
			// PF's a checked and filled,
			// before the Z11 process is run, which is only for the pilot facilities
			// for handbook.
			if (isNightlyUpdate() && pfUpdated) {
				
				// CCR11076 -- test if parent facilities changed instead of the actual preferred facilities
				// CCR11668 -- test if any actual preferred facilities changed instead of the parent facilities
				//if (isAnyActiveParentPFChanged(pristinePFs, onFilePerson.getPreferredFacilities())) {
				if (isAnyActivePFChanged(pristinePFs, getPreferredFacilityLiteSites(onFilePerson))) {
					triggerHandBookInsert(onFilePerson);
				}
				
				if (isMostRecentParentPFChanged(pristineMostRecentPF, onFilePerson.getMostRecentPreferredFacility())) {
					triggerZ11(onFilePerson.getPersonEntityKey(), onFilePerson.getIdentityTraits());
				}
			}	
		} catch (RuleValidationException ex) {
			//
			createAEWorkloadCase(onFilePerson, ex);
		} catch (Exception ex1) {
			// for all other exceptions, rethrow
			if (ex1 instanceof ServiceException) {
				throw((ServiceException)ex1);
			} else {
				throw new ServiceException(ex1);
			}
		}
	}
	
	/**
	 * Returns true if any one of the active preferred facility sites changed  
	 * @param pristinePFs
	 * @param resultPFs
	 * @return
	 */
	private boolean isAnyActivePFChanged(Set pristinePFs, Set resultPFs) throws ServiceException {
		
		//Set activeParentPristinePFs = getParentActiveSites(pristinePFs);
		//Set activeParentResultPFs = getParentActiveSites(resultPFs);
		
		//return !activeParentPristinePFs.equals(activeParentResultPFs);
		boolean anyPFChanges = false;
		
		for (Iterator iter = resultPFs.iterator(); iter.hasNext(); ) {
			if (pristinePFs.contains (iter.next())) {
				continue;
			} else {
				return true;
			}
	 	}
		return anyPFChanges;
	}

	/**
	 * Checks to see if the parent sending site of the most recent calculated preferred facility had changed
	 * @param pristineMostRecentPF
	 * @param mostRecentPreferredFacility
	 * @return
	 */
	private boolean isMostRecentParentPFChanged(VAFacility pristineMostRecentPF, VAFacility mostRecentPF)
		throws ServiceException {
	
		VAFacility pristineParent = getPersonService().getParentSite(pristineMostRecentPF);
		VAFacility resultParent = getPersonService().getParentSite(mostRecentPF);
		
		return ! (pristineParent != null ? 
				(resultParent != null && pristineParent.getCode().equals(resultParent.getCode())) : 
					resultParent == null);
	}

	
	/**
	 * Provides a Set of parent objects who are the parent sending site of the active preferred facilities
	 * @param preferredFacilities a Set containing PreferredFacilityLite objects
	 * @return a Set containing VAFacility objects that are the parent sending sites
	 * @throws ServiceException
	 */
	private Set getParentActiveSites(Set preferredFacilities) throws ServiceException {
		
		// first get the list of active preferred facilities
		Set activePFs = getActivePreferredFacilityLites(preferredFacilities);
		
		Set parentSites = new HashSet();
		VAFacility facility = null;
		VAFacility parentFacility = null;
		
		// now create a list of the parent sites 
		for (Iterator iter = activePFs.iterator(); iter.hasNext(); ) {
			facility = ((PreferredFacilityLite)iter.next()).getFacility();
			parentFacility = getPersonService().getParentSite(facility);
			parentSites.add(parentFacility);
		}
		
		return parentSites;
	}
	
	/**
	 * Trigger outbound Z11 event
	 * @param key
	 */
	public void triggerZ11(PersonEntityKey key, PersonIdentityTraits traits) throws ServiceException {

		// 11593
		getTriggerRouter().processTriggerEvent(getTriggerEvent(key, traits));
	}

	/**
	 * Send handbook PreferredFacilityInsert
	 * @param person
	 */
	public void triggerHandBookInsert(PreferredFacilityPerson person) throws ServiceException {
		this.getHandBookService().triggerHandBookPFInsert(person.getPersonEntityKey().getKeyValueAsString());
	}

	/**
	 * 
	 * Look up the provider, and if the provider does not exist, insert a row in to 
	 *		health_care_provider table.  If it already exists, update the record using incoming data.
	 */
	private HealthCareProvider addOrUpdateProvider(PCMMImportData incoming) throws ServiceException {

		HealthCareProvider onFileProvider = null;

		HealthCareProvider incomingProvider = incoming.getProvider();

		try {
			onFileProvider = getProviderDAO().findByPractitionerID(
					incoming.getPractitionerIENSta3n());
		} catch (DAOException ex) {
			throw new ServiceException("Failed to find provider by practitionerId: " + incoming.getPractitionerIENSta3n(), ex);
		}
		// if the provider does not exist, insert a new row
		if (onFileProvider == null) {
			onFileProvider = incomingProvider;
		} else {
			// udpate
			getMergeRuleService().mergeHealthCareProvider(incomingProvider,
					onFileProvider);
		}
		return onFileProvider;
	}
	
	/**
	 * Add new reccord or update onFile PatientProviderAssigment data with the data from the incoming record.
	 * 
	 * @param incoming
	 * @param personLite
	 * @param provider
	 *
	 * @returns true if a new Assignment is created; false if it's an update
	 */
	private boolean addOrUpdateAssignment(PCMMImportData incoming, PreferredFacilityPerson personLite, HealthCareProvider provider) throws ServiceException {
		
		boolean newPCP = false;
		
		PatientProviderAssignmentLite onFileAssignment = findAssignment(personLite, incoming.getPractitionerIENSta3n(), incoming.getPatientIENSta3n());
		PatientProviderAssignmentLite incomingAssignment = incoming.getProviderAssignment();
		incomingAssignment.setProvider(provider);

		if (onFileAssignment == null) {
			// insert new record
			personLite.addProviderAssignment(incomingAssignment);
			newPCP = true;
		}
		else {
			// update existing record
			getMergeRuleService().mergeProviderAssignment(incomingAssignment, onFileAssignment);
			newPCP = false;
		}			
		
		return newPCP;
	}
	
	/**
	 * Add new or update existing preferred facility, and determine its source designation.
	 *  
	 * @param incoming
	 * @param personLite
	 * @param updatedAssignment
	 * @param isNewPCP
	 */
	private boolean addOrUpdatePreferredFacility(PCMMImportData incoming, 
			PreferredFacilityPerson personLite) throws ServiceException
	{
		boolean facilityUpdated = true;
		
		// Get existing active PFs for this person.  If not found, insert
		Set pfList = personLite.getPreferredFacilities();
		Set activePfs = getActivePreferredFacilityLites(pfList);
		if (activePfs.isEmpty()) {
			addPreferredFacility(personLite, incoming);
			return facilityUpdated;
		}
		
		// If any is "VistA" or "ESR" (note: multiple PFs can exist only for "PCP Active", so if no "PCP Active", the only active recod would be "ESR" or "VistA")
		// Inactivate the record, and insert the new record
		PreferredFacilityLite pf = (PreferredFacilityLite)activePfs.iterator().next();
		SourceDesignation sd = pf.getSourceDesignation();
		if (SourceDesignation.CODE_VISTA.getCode().equals(sd.getCode()) ||
			SourceDesignation.CODE_ESR.getCode().equals(sd.getCode())) {
			pf.setUnassignmentDate(new Date());
			addPreferredFacility(personLite, incoming);		
			return facilityUpdated;
		}
		
		// If any existing record is "PCP Active" -- look for one w/matching facility number, if found update;
		// otherwise insert a new record
		for( Iterator iter = activePfs.iterator(); iter.hasNext(); ) {
	    	pf = (PreferredFacilityLite) iter.next();
	    	if (SourceDesignation.CODE_PCP_ACTIVE.getCode().equals(pf.getSourceDesignation().getCode())) {
	    		if (pf.getFacility().getStationNumber().equals(incoming.getFacility().getStationNumber())) {
	    			// found matching record
	    			return updatePreferredFacility(pf, incoming);
	    		}
	    	}
		}
	    // no match found, insert new record
		addPreferredFacility(personLite, incoming);
		
    	return facilityUpdated;	    	
	    		
	}

	private boolean updatePreferredFacility(PreferredFacilityLite pf, PCMMImportData incoming) throws ServiceException {

		boolean recordUpdated = false;
		
		if (!DateUtils.isSameInstant(pf.getAssignmentDate(), incoming.getAssignmentStartDate()) ||
			 !datesEqualIncludingNull(pf.getUnassignmentDate(), incoming.getAssignmentEndDate())) {

			// make update only if dates has been changed
			pf.setAssignmentDate(incoming.getAssignmentStartDate());
			pf.setUnassignmentDate(incoming.getAssignmentEndDate());
			if (isPFActive(pf)) {
				pf.setSourceDesignation(getLookupService().getSourceDesignationByCode(SourceDesignation.CODE_PCP_ACTIVE.getCode()));
			} else {
				pf.setSourceDesignation(getLookupService().getSourceDesignationByCode(SourceDesignation.CODE_PCP_INACTIVE.getCode()));
			}
			recordUpdated = true;
		}
		return recordUpdated;
	}

	/**
	 * Insert a new row into preferred_facility table.
	 * @param personLite
	 * @param incoming
	 */
	private void addPreferredFacility(PreferredFacilityPerson personLite, PCMMImportData incoming) throws ServiceException {
		PreferredFacilityLite pf = new PreferredFacilityLite();
		pf.setFacility(incoming.getFacility());	 
		pf.setAssignmentDate(incoming.getAssignmentStartDate());
		pf.setUnassignmentDate(incoming.getAssignmentEndDate());
	 
		 String sdCode = null; 
		 if (isPFActive(pf)) {
			 sdCode = SourceDesignation.CODE_PCP_ACTIVE.getCode();
		 } else {
			 sdCode = SourceDesignation.CODE_PCP_INACTIVE.getCode();
		 }
		 pf.setSourceDesignation(getLookupService().getSourceDesignationByCode(sdCode));
		 personLite.addPreferredFacility(pf); 
	}

	/**
	 * Delete incoming Provider Assignment Data as well as Prerferred Facility data
	 * 
	 * @param incoming
	 * @param personLite
	 * @return true if a Preferred Facility has been deleted (to signal trigger Z11)
	 * @throws ServiceException
	 */
	private boolean deleteData(PCMMImportData incoming, PreferredFacilityPerson personLite) throws RuleValidationException, ServiceException {
		// delete the provider assignment
		PatientProviderAssignmentLite assignment = findAssignment(personLite, incoming.getPractitionerIENSta3n(), incoming.getPatientIENSta3n());
		if (assignment != null) {
			personLite.removeProviderAssignment(assignment);
		}
		// delete the preferred facility if:
		// there are no other active PCPs for the same facility #
		return deletePreferredFacility(personLite, incoming);
	}


	/**
	 * Check if there are other PCPs (active or inactive) at the same facility
	 * @param personLite
	 * @param facility
	 * @return true if found
	 */
	private boolean hasOtherPCPsAtFacility(PreferredFacilityPerson personLite, VAFacility facilityToDelete) {
		Set assginmentList = personLite.getProviderAssignments();
		for (Iterator iter = assginmentList.iterator(); iter.hasNext(); ) {
			PatientProviderAssignmentLite assignment = (PatientProviderAssignmentLite)iter.next();
			
			// Check if there are any other PCPs (active/inactive) for the same facility # patient Id
			if (assignment.getFacility().getStationNumber().equals(facilityToDelete.getStationNumber())) {
				// if yes do not delete
				return true;
			}
		}
		// no other PCPs found for the same station number
		return false;
	}


	/**
	 * Delete the preferred facility with the input facility
	 * @param personLite
	 * @param facility
	 * @return if a facility has been deleted
	 */
	private boolean deletePreferredFacility(PreferredFacilityPerson personLite, PCMMImportData incoming) throws RuleValidationException {

		boolean pfDeleted = false;
		
		if (! hasOtherPCPsAtFacility(personLite, incoming.getFacility())) {
			// now find the PF with matching facility #, start date and end date to delete
			Set personPfList = personLite.getPreferredFacilities();
			PreferredFacilityLite pf = null;
			for (Iterator iter=personPfList.iterator(); iter.hasNext();) {
				pf = (PreferredFacilityLite)iter.next();
				if (pf.getFacility().getStationNumber().equals(incoming.getFacility().getStationNumber())) {
					
					// if this is the last PF for the person, we cannot delete.			
					// ESR needs to have at least one PF on file.  ESR cannot calculate a "NULL"
					if (personPfList.size() == 1) {
						// exception report/Workload item.  For performance purposes we are not using ILOG rules,
						// therefore directly creating a rule exception for the purpose of throwing an AE
						throw new RuleValidationException("Cannot delete preferred facility " + incoming.getFacility().getStationNumber() + 
								" for person " + personLite.getVPIDEntityKey().getKeyValueAsString() + ".  Person need to have at least one preferred facility.");
					} else {
						// do not delete PFs with source desgination being ERS or VistA
						if (SourceDesignation.CODE_PCP_ACTIVE.getCode().equals(pf.getSourceDesignation().getCode()) ||
							SourceDesignation.CODE_PCP_INACTIVE.getCode().equals(pf.getSourceDesignation().getCode())) {
							personLite.removePreferredFacility(pf);
							pfDeleted = true;
							break;
						}
					}
				}
			}
		}
		return pfDeleted;
	}

	/**
	 * Returns true if either date1 and date2 are both null, or if they equal in time
	 * @param date1
	 * @param date2
	 * @return
	 */
	private boolean datesEqualIncludingNull(Date date1, Date date2) {
		return (date1 == null && date2 == null) ||
			(date1 != null && date2 != null && DateUtils.isSameInstant(date1, date2));
	}
	
	private PatientProviderAssignmentLite findAssignment(PreferredFacilityPerson personLite, String practionerId, String patientId) 
		throws ServiceException {
		PatientProviderAssignmentLite assignment = null;
		
		for (Iterator iter = personLite.getProviderAssignments().iterator(); iter.hasNext(); ) {
			assignment = (PatientProviderAssignmentLite)iter.next();
			if (assignment.getProvider().getPractitionerId().equals(practionerId)) {
				// found match, but if patient DFN doesn't match, throw exception
				if (assignment.getPatientId() != null && ! assignment.getPatientId().equals(patientId)) {
					throw new ServiceException("Found assignment with matching practitionerId, but patient Id mismatches.");
				}
				return assignment;
			}
		}
		return null;		
	}


	/**
	 * Lightweight object used only for the initial seeding job. 
	 * 
	 * @param pfList
	 * @return
	 * @throws ServiceException
	 */
	private Set getActivePreferredFacilityLites(Set pfList) throws ServiceException {
		Set activePfs = new HashSet();
		PreferredFacilityLite pf = null;
		
		for (Iterator iter = pfList.iterator(); iter.hasNext(); ) {
			pf = (PreferredFacilityLite)iter.next();
			if (isPFActive(pf)) {
				activePfs.add(pf);
			}
		}
		return activePfs;
	}
	

	public boolean hasActivePreferredFacility(Set pfList) throws ServiceException {
		for (Iterator itor = pfList.iterator(); itor.hasNext(); ) {
			PreferredFacilityLite pf = (PreferredFacilityLite)itor.next();
			if (isPFActive(pf)) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Checks if a preferred facility is active.  
	 * Returns true if pf has unassignment date being null or future dated.
	 * @param pf
	 * @return
	 */
	public boolean isPFActive(PreferredFacilityLite pf) {
		
		Date currentDate = DateUtils.getCurrentDateTime();
		return ((pf.getUnassignmentDate() == null || pf.getUnassignmentDate().after(currentDate)));
	}

	private TriggerEvent getTriggerEvent(PersonEntityKey key, PersonIdentityTraits traits)
	{
		PersonTriggerEvent triggerEvent = new PersonTriggerEvent(PersonTrigger.DestinationType.MESSAGING,
                PersonTrigger.TargetType.VISTA, PersonTrigger.DispatchType.NOTIFY,  PersonTrigger.DataType.ELIGIBILITY);
		triggerEvent.setPersonId(key);
	    triggerEvent.setIdentityTraits(traits);

	    return triggerEvent;
	}

	public PreferredFacilityDAO getPreferredFacilityDAO() {
		return preferredFacilityDAO;
	}

	public void setPreferredFacilityDAO(PreferredFacilityDAO preferredFacilityDAO) {
		this.preferredFacilityDAO = preferredFacilityDAO;
	}

	public HealthCareProviderDAO getProviderDAO() {
		return providerDAO;
	}

	public void setProviderDAO(HealthCareProviderDAO providerDAO) {
		this.providerDAO = providerDAO;
	}

	public TriggerRouter getTriggerRouter() {
		return triggerRouter;
	}

	public void setTriggerRouter(TriggerRouter triggerRouter) {
		this.triggerRouter = triggerRouter;
	}

	public boolean isNightlyUpdate() {
		return nightlyUpdate;
	}

	public void setNightlyUpdate(boolean nightlyUpdate) {
		this.nightlyUpdate = nightlyUpdate;
	}

	
	/**
	 * Used by outbound Z11 -- where a person is used.  DO NOT used for PCMMImport process (needs lightweight person)
	 * @param person
	 * @return
	 */
	public PreferredFacility getMostRecentPreferredFacility(Person person) {
    	PreferredFacility mostRecent = null;
    	
    	PreferredFacility next;
    	
    	Set pfList = person.getPreferredFacilities();
    	for (Iterator iter = pfList.iterator(); iter.hasNext(); ) {
			next = (PreferredFacility)iter.next();
			if (isMoreRecent(next, mostRecent)) {
				mostRecent = next;
			}
		}
    	return mostRecent;
	}

    /**
     * Checks to see if the next preferred facility is more recent than the existing most recent encountered so far
     * @param next
     * @param mostRecent
     * @return
     */
    private boolean isMoreRecent(PreferredFacilityLite next, PreferredFacilityLite mostRecent) {
    	if (mostRecent == null) {
    		return true;
    	}  
    	
    	boolean isMoreRecent = false;
		
		if (!isPFActive(mostRecent)) {
			if (isPFActive(next)) {
		    	// existing record is inactive and next record is active
				isMoreRecent = true;
			} else if (next.getUnassignmentDate().after(mostRecent.getUnassignmentDate())){
				// both records are inactive but the next record has more recent date unassigned
				isMoreRecent = true;
			} 
		}
		else { // existing record is active
			if (isPFActive(next) && 
				next.getAssignmentDate().after(mostRecent.getAssignmentDate()))
				// next record is also active AND has more recent assignment date
			{
				isMoreRecent = true;
			}
		}
		
		return isMoreRecent;
	}
  
    /**
     * CCR12879_AJ_VOA - Apply PF processing logic when VOA submitted PF
     * Checks if the person has any active preferred facility with source designation as "PCP Active" or "ESR".
     * Note: A regular heavyweight person is used.  DO NOT used for PCMMImport process (needs lightweight person)
     * @return 
     */
    public boolean hasPCPActiveOrESRPF(Person person) {
		Set pfList = person.getPreferredFacilities();
		for (Iterator itor = pfList.iterator(); itor.hasNext(); ) {
			PreferredFacility pf = (PreferredFacility)itor.next();
			if (isPFActive(pf) && 
					(pf.getSourceDesignation().getCode().equals(SourceDesignation.CODE_PCP_ACTIVE.getCode()) ||
					 pf.getSourceDesignation().getCode().equals(SourceDesignation.CODE_ESR.getCode()))) {
				return true;
			}
		}
		return false;
    }

    /**
     * CCR12879_AJ_VOA - Apply PF processing logic when VOA submitted PF
     * Checks if the person has any active preferred facility with source designation as "PCP Active."
     * Note: A regular heavyweight person is used.  DO NOT used for PCMMImport process (needs lightweight person)
     * @return 
     */
    public boolean hasPCPActivePF(Person person) {
		Set pfList = person.getPreferredFacilities();
		for (Iterator itor = pfList.iterator(); itor.hasNext(); ) {
			PreferredFacility pf = (PreferredFacility)itor.next();
			if (isPFActive(pf) && 
					(pf.getSourceDesignation().getCode().equals(SourceDesignation.CODE_PCP_ACTIVE.getCode()))) {
				return true;
			}
		}
		return false;
    }
    
    /**
     * Checks if the person has any active preferred facility with source designation as "PCP Inactive" or "Vista".
     * Note: A regular heavyweight person is used.  DO NOT used for PCMMImport process (needs lightweight person)
     * @return 
     */
    public boolean hasVistaOrPCPInactivePF(Person person) {
		Set pfList = person.getPreferredFacilities();
		for (Iterator itor = pfList.iterator(); itor.hasNext(); ) {
			PreferredFacility pf = (PreferredFacility)itor.next();
			if (isPFActive(pf) && 
					(pf.getSourceDesignation().getCode().equals(SourceDesignation.CODE_PCP_INACTIVE.getCode()) ||
					 pf.getSourceDesignation().getCode().equals(SourceDesignation.CODE_VISTA.getCode()))) {
				return true;
			}
		}
		return false;
    }
    
	/**
	 * Get the person's active preferred facility record that is from vistA
     * Note: A regular heavyweight person is used.  DO NOT used for PCMMImport process (needs lightweight person)
	 * @return
	 */
	public PreferredFacility getPreferredFacilityFromVista(Person person) 
	{
		Set pfList = person.getPreferredFacilities();
		for (Iterator itor = pfList.iterator(); itor.hasNext(); ) {
			PreferredFacility pf = (PreferredFacility)itor.next();
			if (isPFActive(pf) &&			
				pf.getSourceDesignation().getCode().equals(SourceDesignation.CODE_VISTA.getCode())) {
				return pf;
			}
		}
		// not found
		return null;
	}
	
	/**
	 * Get the person's active preferred facility record that is from ESR
	 * CCR12879_AJ_VOA - Apply PF processing logic when VOA submitted PF
     * Note: A regular heavyweight person is used.  DO NOT used for PCMMImport process (needs lightweight person)
	 * @return
	 */
	
	public PreferredFacility getPreferredFacilityFromES(Person person) 
	{
		Set pfList = person.getPreferredFacilities();
		for (Iterator itor = pfList.iterator(); itor.hasNext(); ) {
			PreferredFacility pf = (PreferredFacility)itor.next();
			if (isPFActive(pf) &&			
				pf.getSourceDesignation().getCode().equals(SourceDesignation.CODE_ESR.getCode())) {
				return pf;
			}
		}
		// not found
		return null;
	}
	
	/**
	* Returns list of preferred facilities to be displayed by the UI
	* @return
	*/
	public List getDisplayPreferredFacilities(Person person)
	{
		List displayList = new ArrayList();
		
		Set activePCPPfs = new HashSet();
		Set activeOtherPfs = new HashSet();
		PreferredFacility next = null;
		
		for (Iterator iter = person.getPreferredFacilities().iterator(); iter.hasNext(); ) {
			next = (PreferredFacility)iter.next();
			if (isPFActive(next)) {
				if (next.getSourceDesignation().getCode().equals(SourceDesignation.CODE_PCP_ACTIVE.getCode())) {
					activePCPPfs.add(next);
				} else {
					activeOtherPfs.add(next);
				}
			}
		}
		// if there are PCP Active preferred facilities, return those
		if (!activePCPPfs.isEmpty()) {
			return new ArrayList(activePCPPfs);
		}
		
		// no SD="PCP Active" records, there should only be one other active SD="ESR" or SD="VistA" preferred facility		
		PreferredFacility pf = null;

		if (!activeOtherPfs.isEmpty()) {
			for (Iterator iter = activeOtherPfs.iterator(); iter.hasNext(); ) {
				next = (PreferredFacility)iter.next();
				if (next.getSourceDesignation().getCode().equals(SourceDesignation.CODE_ESR.getCode())) {
					pf = next;
				} else if (next.getSourceDesignation().getCode().equals(SourceDesignation.CODE_VISTA.getCode()) &&
						pf == null) {
					pf = next;
				}
			}
		}
		
		// CCR11491 -- fixed logic error that causes null pointer exception
		if (pf != null) {
			displayList.add(pf);
		}
		else {
			// there are no active preferred facilities, just return the most recent one
			PreferredFacility mostRecent = getMostRecentPreferredFacility(person);
			if (mostRecent != null) {
				displayList.add(mostRecent);
			}
		}
		return displayList;
	}

	/**
	 * Add a new preferred facility from VistA or ESR. Trigger Z11.
	 */
	public void addPreferredFacility(PreferredFacility incoming, Person onfilePerson, String sdCode) throws ServiceException
	{
		if (incoming != null) {
			
			PreferredFacility newPf = new PreferredFacility();
			newPf.setFacility(incoming.getFacility());
			newPf.setAssignmentDate(new Date());
			newPf.setUnassignmentDate(null);
			newPf.setSourceDesignation(getLookupService().getSourceDesignationByCode(sdCode));
			onfilePerson.addPreferredFacility(newPf);
			
			// CCR11540, suppressed triggering Z11 from within the service layer, since this method
			// is called for VistA and UI changes, and that should already been part of an ILOG flow,
			// which already triggers Z11.  We should not send duplicate Z11s.
			// Trigger Z11
			// triggerZ11(onfilePerson.getPersonEntityKey());
		}
	}
    
		
	/**
	 * update the calculated most recent prefered facility result onto person object.  Uses a regular heavy weight person.
	 * @param person
	 */
	public void setCalculatedMostRecentPreferredFacility(Person person) {
		PreferredFacility mostRecentPF = getMostRecentPreferredFacility(person);
		person.setMostRecentPreferredFacility(mostRecentPF == null ? null : mostRecentPF.getFacility());
	}
	
	/**
	 * Same function as above logic but using lightweight person object, used only for the PCMM batch job.
	 * @param personLite
	 */
	private void setCalculatedMostRecentPreferredFacility(PreferredFacilityPerson personLite){
		// now set the calculated result of most recent preferred facility onto person object. Not using
		// available method, as that method uses the heavy weight person.  So unfortunately duplicating logic
	   	Set pfList = personLite.getPreferredFacilities();
	   	PreferredFacilityLite next = null, mostRecent = null;
    	for (Iterator iter = pfList.iterator(); iter.hasNext(); ) {
			next = (PreferredFacilityLite)iter.next();
			if (isMoreRecent(next, mostRecent)) {
				mostRecent = next;
			}
		}
		personLite.setMostRecentPreferredFacility(mostRecent == null ? null : mostRecent.getFacility());
	}
	
	/**
	 * Returns regular object used only for general processing. 
	 * 
	 * @param pfList
	 * @return
	 * @throws ServiceException
	 */
	public Set getActivePreferredFacilities(Set pfList) throws ServiceException {
		Set activePfs = new HashSet();
		PreferredFacility pf = null;
		
		for (Iterator iter = pfList.iterator(); iter.hasNext(); ) {
			pf = (PreferredFacility)iter.next();
			if (isPFActive(pf)) {
				activePfs.add(pf);
			}
		}
		return activePfs;
	}
	/**
	 * Returns regular object used only for general processing. 
	 * We cannot use the method above decause it is for PreferredFacility object. So unfortunately duplicating logic.
	 * @param pfList
	 * @return
	 * @throws ServiceException
	 */
	public Set getActivePreferredFacilitiesLite(Set pfList) throws ServiceException {
		Set activePfs = new HashSet();
		PreferredFacilityLite pf = null;
		
		for (Iterator iter = pfList.iterator(); iter.hasNext(); ) {
			pf = (PreferredFacilityLite)iter.next();
			if (isPFActive(pf)) {
				activePfs.add(pf);
			}
		}
		return activePfs;
	}
	
	private void createAEWorkloadCase(PreferredFacilityPerson p, RuleValidationException ex) throws ServiceException {
		// Create a DQ Workload case
		WorkflowCaseInfo caseInfo = new WorkflowCaseInfo();
	    // set the group type
    	FunctionalGroup caseGroup = getLookupService().getFunctionalGroupByCode(FunctionalGroup.DQ.getCode());
		caseInfo.setGroupType(caseGroup);

		caseInfo.setCaseType(getLookupService().getWkfCaseTypeByCode(WkfCaseType.CODE_APPLICATION_EXCEPTION.getCode()));
		caseInfo.setErrorMessage("PF assignment error, ESR user needs to determine appropriate PF. " + ex.getMessage());
		PersonIdEntityKey personKey = CommonEntityKeyFactory.createPersonIdEntityKey(p
	            .getPersonEntityKey().getKeyValueAsString());
		caseInfo.setPersonEntityKey(personKey);

		createWorkloadCase(caseInfo, personKey);
	}
	
	private void createWorkloadCase(WorkflowCaseInfo caseInfo, PersonIdEntityKey personKey) throws ServiceException {
		// create async trigger event to create a case
		MessageCaseTriggerEvent messageCaseTriggerEvent = new MessageCaseTriggerEvent();
		messageCaseTriggerEvent.setWorkflowCaseInfo(caseInfo);
		messageCaseTriggerEvent.setPersonId(personKey);
		triggerRouter.processTriggerEvent(messageCaseTriggerEvent);
	}
	/**
	 * @return Returns the commsLogService.
	 */
	public CommsLogService getCommsLogService() {
		return commsLogService;
	}

	/**
	 * @param commsLogService The commsLogService to set.
	 */
	public void setCommsLogService(CommsLogService commsLogService) {
		this.commsLogService = commsLogService;
	}

	// 3.6 CCR 10960
	public HandBookService getHandBookService() {
		return handBookService;
	}

	public void setHandBookService(HandBookService handBookService) {
		this.handBookService = handBookService;
	}
	
	/**
	 * Get list of Child VaFacility 
	 * 
	 * @return VaFacility A corresponding Child VaFacilities
	 */
	public  List getChildVAFacility(List parentIds)
	throws UnknownLookupTypeException, UnknownLookupCodeException {
		try {
			
			return getPreferredFacilityDAO().getChildVaFacility(parentIds);
		} catch (DAOException e) {
			throw new RuntimeException(e);
		}
	}
	
	/**
	 * Returns set of active preferred facility sites for person, if no acdtive PFs are found, return the most recent inactive PF
	 * @param person
	 * @return
	 * @throws ServiceException
	 */
	public Set<VAFacility> getPreferredFacilitySites(Person person) throws ServiceException {
		
	   	Set pfList = person.getPreferredFacilities();
	   	
	   	Set activePfs = this.getActivePreferredFacilities(pfList);
	   	
	   	Set<VAFacility> pfSites = new HashSet<VAFacility>();

	   	PreferredFacility pf;
	   	List<PreferredFacility> pfs = new ArrayList<PreferredFacility>();
	   	if (!activePfs.isEmpty()) {
	   		pfs.addAll(activePfs);
	   		// Comparator specific to PreferredFacility defined as in inner class
	        Comparator comparator = new Comparator()
	        {
	          //public int compare(PreferredFacility pf1, PreferredFacility  pf2)
	          public int compare(Object o1, Object o2)
	            {      
	        	  PreferredFacility pf1 = (PreferredFacility)o1;   
	        	  PreferredFacility pf2 = (PreferredFacility)o2; 
	        	  if (pf1 == null || pf1.getAssignmentDate() == null) return -1;
	        	  if (pf2 == null || pf2.getAssignmentDate() == null) return 1;
	        	  return (pf2.getAssignmentDate().compareTo(pf1. getAssignmentDate()));
	            }
	        };
			Collections.sort(pfs, comparator);
			//Get the first 3 PFs from the pfs by assigmentDate.
			List first3pfsList = pfs.subList(0, Math.min(3, pfs.size()));
			for (Iterator iter = first3pfsList.iterator(); iter.hasNext();) {
				pf = (PreferredFacility)iter.next();
				pfSites.add(pf.getFacility());
			}		
	   	} else {
	   		// no active PFs found, return just the most recent (inactive) PF
	   		if (person.getMostRecentPreferredFacility()!=null) {
	   			pfSites.add(person.getMostRecentPreferredFacility());
	   		}
	   	}
		return pfSites;
	}
	
	/**
	 * Returns set of active preferred facility sites for PreferredFacilityPerson, if no acdtive PFs are found, return the most recent inactive PF. 
	 * We cannot use the method above decause it is for heavyweight person object. So unfortunately duplicating logic.
	 * @param person
	 * @return
	 * @throws ServiceException
	 */
	public Set<VAFacility> getPreferredFacilityLiteSites(PreferredFacilityPerson person) throws ServiceException {
		
	   	Set pfList = person.getPreferredFacilities();
	   	
	   	Set activePfs = this.getActivePreferredFacilitiesLite(pfList);
	   	
	   	Set<VAFacility> pfSites = new HashSet<VAFacility>();

	   	PreferredFacilityLite pf;
	   	if (!activePfs.isEmpty()) {
	   		for (Iterator iter = activePfs.iterator(); iter.hasNext(); ) {
	   			pf = (PreferredFacilityLite)iter.next();
	   			pfSites.add(pf.getFacility());
	   		}
	   	} else {
	   		// no active PFs found, return just the most recent (inactive) PF
	   		if (person.getMostRecentPreferredFacility()!=null) {
	   			pfSites.add(person.getMostRecentPreferredFacility());
	   		}
	   	}
		return pfSites;
	}
	
	/**
	 * Returns set of active preferred facility sites for person, if no acdtive PFs are found, return the most recent inactive PF
	 * We cannot use the method above (getPreferredFacilitySites) decause it returns three PFs by assignment date. So unfortunately 
	 * duplicating logic.
	 * @param person
	 * @return
	 * @throws ServiceException
	 */
	public Set<VAFacility> getPFSites(Person person) throws ServiceException {
		
	   	Set pfList = person.getPreferredFacilities();
	   	
	   	Set activePfs = this.getActivePreferredFacilities(pfList);
	   	
	   	Set<VAFacility> pfSites = new HashSet<VAFacility>();

	   	PreferredFacility pf;
	   	if (!activePfs.isEmpty()) {
	   		for (Iterator iter = activePfs.iterator(); iter.hasNext(); ) {
	   			pf = (PreferredFacility)iter.next();
	   			pfSites.add(pf.getFacility());
	   		}
	   	} else {
	   		// no active PFs found, return just the most recent (inactive) PF
	   		if (person.getMostRecentPreferredFacility()!=null) {
	   			pfSites.add(person.getMostRecentPreferredFacility());
	   		}
	   	}
		return pfSites;
	}
}
