package gov.va.med.esr.common.persistent.person;

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

import gov.va.med.esr.UseCaseName;
import gov.va.med.esr.common.batchprocess.SSASSNVerificationData;
import gov.va.med.esr.common.infra.ImpreciseDate;
import gov.va.med.esr.common.model.CommonEntityKeyFactory;
import gov.va.med.esr.common.model.SiteYear;
import gov.va.med.esr.common.model.ee.Eligibility;
import gov.va.med.esr.common.model.ee.EnrollmentDetermination;
import gov.va.med.esr.common.model.ee.IneligibilityFactor;
import gov.va.med.esr.common.model.ee.MonetaryBenefitAward;
import gov.va.med.esr.common.model.financials.DependentFinancials;
import gov.va.med.esr.common.model.financials.FinancialStatement;
import gov.va.med.esr.common.model.financials.IncomeTest;
import gov.va.med.esr.common.model.financials.SpouseFinancials;
import gov.va.med.esr.common.model.ivmdm.IVMMigration;
import gov.va.med.esr.common.model.lookup.EnrollmentStatus;
import gov.va.med.esr.common.model.lookup.EnrollmentPriorityGroup;
import gov.va.med.esr.common.model.lookup.IncomeTestSource;
import gov.va.med.esr.common.model.lookup.MeansTestStatus;
import gov.va.med.esr.common.model.lookup.MessageStatus;
import gov.va.med.esr.common.model.lookup.SSAVerificationStatus;
import gov.va.med.esr.common.model.party.Address;
import gov.va.med.esr.common.model.person.DeathRecord;
import gov.va.med.esr.common.model.person.Person;
import gov.va.med.esr.common.model.person.PersonChangeLogEntry;
import gov.va.med.esr.common.model.person.Relation;
import gov.va.med.esr.common.model.person.SSN;
import gov.va.med.esr.common.model.person.SignatureImage;
import gov.va.med.esr.common.model.person.id.PersonEntityKey;
import gov.va.med.esr.service.IVMDMService;
import gov.va.med.esr.service.PersonHelperService;
import gov.va.med.esr.service.PersonIdentityTraits;
import gov.va.med.esr.service.PersonService;
import gov.va.med.fw.cache.EntityCacheManager;
import gov.va.med.fw.model.AbstractKeyedEntity;
import gov.va.med.fw.model.lookup.Lookup;
import gov.va.med.fw.persistent.AbstractKeyedEntitySubmittedAdvice;
import gov.va.med.fw.service.ServiceException;
import gov.va.med.fw.util.StringUtils;

import org.apache.commons.lang.Validate;

/**
 * After-advice that will log that this Person was submitted to be changed on
 * the current date.
 * 
 * <p>
 * Default submitterDescription will be the injected getSubmitter().
 * 
 * Created Mar 3, 2006 10:06:55 AM
 * 
 * @author DNS   BOHMEG
 * @author DNS   ruizc
 */
public class PersonSubmittedAdvice extends AbstractKeyedEntitySubmittedAdvice {
	public static final String SUBMITTER_ATTENDED = "UI";

	public static final String SUBMITTER_UNATTENDED = "Unattended_Process";

	public static final String SUBMITTER_IVM_DM = "IVM_DM";

	public static final String SUBMITTER_DATA_DESTINED_FOR_HECLEGACYDATASYNC = SUBMITTER_ATTENDED;
	/**
	 * CCR 12053 new selection criteria
	 * Means Test Status = MT Copay Exempt or GMT Copay Required, or Pending
	 * Adjudication or MT Copay Required
	 */
	public static final String[] mtStatusCodesForIVMCriteria = {
		MeansTestStatus.MT_STATUS_MT_COPAY_EXEMPT.getCode(), 
		MeansTestStatus.MT_STATUS_GMT_COPAY_REQUIRED.getCode(),
		MeansTestStatus.MT_STATUS_PENDING_ADJUDICATION.getCode(), 
		MeansTestStatus.MT_STATUS_MT_COPAY_REQUIRED.getCode()};	


	/**
	 * CCR 12053 new selection criteria
	 * Priority Group = 2 or 3 or 5 or 6 or 7 or 8
	 */	
	public static final String[] enrollmentPriorityCodesForIVMCriteria = {
		EnrollmentPriorityGroup.GROUP_2.getCode(), 
		EnrollmentPriorityGroup.GROUP_3.getCode(),
		EnrollmentPriorityGroup.GROUP_5.getCode(),
		EnrollmentPriorityGroup.GROUP_6.getCode(),
		EnrollmentPriorityGroup.GROUP_7.getCode(),		
		EnrollmentPriorityGroup.GROUP_8.getCode()};	

	
	private PersonService personService;

	private PersonHelperService personHelperService;

	private IVMDMService ivmDMService;

	private EntityCacheManager entityCacheManager;

	/**
	 * @see gov.va.med.fw.persistent.AbstractEntitySubmittedAdvice#processEntitySubmitted(gov.va.med.fw.model.AbstractKeyedEntity)
	 */
	protected final void processEntitySubmitted(AbstractKeyedEntity entity,
			Object[] methodArguments) throws Exception {
		if (entity instanceof Person) {
			Person updatedPerson = (Person) entity;
			PersonEntityKey key = updatedPerson.getPersonEntityKey();
			if (logger.isDebugEnabled())
				logger.debug("Person [" + key.getKeyValue() + "] submitted by "
						+ getSubmitter());
			Person oldPerson = (Person) entityCacheManager
					.getItem(updatedPerson.getEntityKey());
			if (oldPerson == null)
				oldPerson = getPersonArgumentIfAny(methodArguments);

			/*
			 *  CCR 12053 ivm selection changes
			 *  Not all veterans will have income test, so use the current active IVM IY
			 *  as default for inserting into IVM_MIGRATE. The main point here is that
			 *  the income year in IVM_MIGRATE will be thought of as a time period
			 *  that does not necessarily imply an income test was present during the
			 *  time period.
			 */
			Integer currentActiveIVMIncomeYear = getIvmDMService().getCurrentActiveIVMIncomeYear();
			Integer nextIVMIncomeYear = getIvmDMService().getNextVMIncomeYear();
			Set<Integer> incomeYearsInserted =  new HashSet<Integer>();			
			Set changes = new HashSet();
			
			/* CCR 11892 new IVM Selection approach */
			IncomeTest currentIncomeTest = this.getPersonHelperService()
					.getCurrentIncomeTest(updatedPerson);
			Integer incomeYear = currentIncomeTest != null ? currentIncomeTest
					.getIncomeYear() : null;

			if (incomeYear == null && currentActiveIVMIncomeYear != null) {
				incomeYear = currentActiveIVMIncomeYear;	// Use default	
			}
			// Notice that the following three items are based on incomeYear
			// from the current income test of the updated person
			Set migrations = getIvmDMService().findIVMMigrationsWithCompleteOrPendingStatus(
					updatedPerson.getPersonEntityKey());
			boolean recordAwaitingTx = this.ivmMigrationsContainStatus(migrations, MessageStatus.AWAITING_TRANSMISSION, incomeYear);
			PersonIdentityTraits traits = this.getTraits(updatedPerson,	oldPerson);
			if (oldPerson == null) {
				// Check if from SSN job
				SSASSNVerificationData data = this
						.getSSASSNVerificationDataArgumentIfAny(methodArguments);
				if (data != null && !recordAwaitingTx) {
					boolean flag = satisfiesIVMSelectionCriteria(updatedPerson,
							incomeYear, traits);
					if (flag) {
						insertIVMMigrationEntry(updatedPerson, incomeYear, incomeYearsInserted, false);
					}
				}
				return; // No way to check for IVM changes
			}
			boolean recordPreviouslyCompleted = this.ivmMigrationsContainStatus(migrations, MessageStatus.COMPLETE, incomeYear);
			boolean veteranPreviouslyQualified = migrations != null ? migrations.size() > 0 : false;			
			checkIvmSyncChanges(changes, updatedPerson, oldPerson);

			/*
			 * CCR 12540 mark record for explicit delete
			 * Had to change order when this is called. 
			 * 
			 * Deletion of Means Test on ES When a means test has been deleted
			 * in ES and the Veteran was previously qualified for an ES to IVM
			 * update, ES will send to IVM an indication that the MT has been
			 * deleted. The indicator will include the IY without financial
			 * data.
			 */
			if ((recordPreviouslyCompleted || veteranPreviouslyQualified) && 
					updatedPerson.getChangeEvents().containsKey(UseCaseName.INCOMETEST_DELETED)	) {
				/*
				 * Notified that test was deleted so get the income year.
				 */
				Integer deletedYear = new Integer((String)updatedPerson.getChangeEvents().get(
						UseCaseName.INCOMETEST_DELETED));
				// THE TEST WAS DELETED. ESR must notify IVM. Insert a marked record
				// unless one already exists.
				if (deletedYear != null) {
					// Make sure a marked record does not exist for same IY
					boolean markedForDeleteRecordExists = this.ivmMigrationsContainMarkedForDelete(migrations, incomeYear);
					// This is rare case where we waive policy of not inserting a record 
					// if there is a record already pending TX for same IY. We have
					// to do so otherwise the delete signal to IVM will not occur.
					if (!markedForDeleteRecordExists) {
						// The insert below contains explicit delete signal.
						boolean deleteTheIncomeTestForThisIncomeYear = true;
						insertIVMMigrationEntry(updatedPerson, deletedYear, incomeYearsInserted, 
								deleteTheIncomeTestForThisIncomeYear);
					}
				}
			}
			
			/*
			 * SCENARIO: An update for a Veteran previously transferred.
			 * 
			 * IF record has been previously transferred for the income year
			 * THEN ES to IVM update is sent for the Veteran in this IY
			 * regardless of if meets IVM criteria ENDIF
			 */
			if (!recordAwaitingTx
					&& recordPreviouslyCompleted) {
				// We are here because ivmMigration was previously TX'd OK for
				// the income year and nothing is currently awaiting transmission
				if (changes.size() > 0 &&  !incomeYearsInserted.contains(incomeYear)) {				
					insertIVMMigrationEntry(updatedPerson, incomeYear, incomeYearsInserted, false);
				}
			}

			boolean meetsIVMSelectionCriteria = satisfiesIVMSelectionCriteria(
					updatedPerson, incomeYear, traits);

			// IF the Veteran has not been previously transferred for the IY
			// THEN
			// IF the IY is the current IVM active IY or the next IY THEN
			// the system checks to see if the Veteran meets the IVM selection
			// Criteria to be transferred for the IY.
			// IF the selection criteria are met THEN
			// the system will transfer the Veteran’s data for this IY.
			// ENDIF
			// IF the selection criteria are not met THEN
			// the system will not transfer the data.
			// ENDIF
			// ENDIF
			// ENDIF
			if (!recordAwaitingTx
					&& !recordPreviouslyCompleted
					&& (incomeYear != null)
					&& (incomeYear.compareTo(currentActiveIVMIncomeYear) == 0 || incomeYear
							.compareTo(nextIVMIncomeYear) == 0)) {
				// --------------------------------------------------------------------
				// Transfer of data for a new IY is only done for the current
				// IVM active IY or the next IY. New IY data is not transferred for
				// past IYs.
				// --------------------------------------------------------------------
				if (meetsIVMSelectionCriteria && changes.size() > 0) {
					if (!incomeYearsInserted.contains(incomeYear)) {
						insertIVMMigrationEntry(updatedPerson, incomeYear, incomeYearsInserted, false);						
					}
				}
			}

			// IF there is a change to Non IY specific data THEN
			// system checks to see if the Veteran meets the IVM Selection
			// Criteria to be transferred for
			// the current IVM active IY and the next IY.
			// IF Veteran meet IVM Selection Criteria for current IVM active IY
			// and/or the next IY, THEN
			// record is transferred for each of these years the Veteran meets
			// the IVM Selection Criteria.
			// ENDIF
			// ENDIF
			boolean meetsIVMSelectionForCurrentIVMActiveYear = satisfiesIVMSelectionCriteria(
					updatedPerson, currentActiveIVMIncomeYear, traits);
			boolean meetsIVMSelectionForNextVMIncomeYear = satisfiesIVMSelectionCriteria(
					updatedPerson, nextIVMIncomeYear, traits);
			boolean changeToNonIYSpecificData = checkNonIncomeYearSpecificChanges(
					updatedPerson, oldPerson);
			if (changeToNonIYSpecificData) {
				if (meetsIVMSelectionForCurrentIVMActiveYear) {
					if (!incomeYearsInserted.contains(currentActiveIVMIncomeYear)) {
						boolean awaitingTxForCurrActIY = this.ivmMigrationsContainStatus(migrations, 
								MessageStatus.AWAITING_TRANSMISSION, currentActiveIVMIncomeYear);
						// Don't insert if there is a record awaiting tx for the year						
						if (!awaitingTxForCurrActIY) {
							insertIVMMigrationEntry(updatedPerson,
									currentActiveIVMIncomeYear, incomeYearsInserted, false);
						}
					}
				}
				if (meetsIVMSelectionForNextVMIncomeYear) {
					if (!incomeYearsInserted.contains(nextIVMIncomeYear)) {
						boolean awaitingTxForNextIY = this.ivmMigrationsContainStatus(migrations, 
								MessageStatus.AWAITING_TRANSMISSION, nextIVMIncomeYear);
						if (!awaitingTxForNextIY) {
							insertIVMMigrationEntry(updatedPerson,
									nextIVMIncomeYear, incomeYearsInserted, false);							
						}
					}
				}
			}

			// IF the Veteran does not meet IVM Selection Criteria for Current
			// IVM active IY and/or next IY THEN
			// The most recent IY, for which the Veteran has been previously
			// transferred without an EDB Load Failure Reason,
			// if the Veteran has been previously transferred, will be sent as
			// an ES to IVM Update.
			// ENDIF
			Integer mostRecentlyTransferredIY = this.getMostRecentIVMMigratedIY(migrations);

			if (!meetsIVMSelectionForCurrentIVMActiveYear
					&& !meetsIVMSelectionForNextVMIncomeYear && changes.size() > 0) {
				// Check if we previously sent a succesful migration record to
				// IVM
				if (mostRecentlyTransferredIY != null && !incomeYearsInserted.contains(mostRecentlyTransferredIY)) {
					boolean awaitingTx = this.ivmMigrationsContainStatus(migrations, 
							MessageStatus.AWAITING_TRANSMISSION, mostRecentlyTransferredIY);
					
					if (!awaitingTx) {
						insertIVMMigrationEntry(updatedPerson, mostRecentlyTransferredIY, incomeYearsInserted, false);
					}
				}
			}
			// Legacy check for veterans transfered using old system 
			if (mostRecentlyTransferredIY == null && changes.size() > 0) {
				// CCR 12072
				// Handle legacy records who will may have an IVM migrate record
				// with a null status. 
				Integer latestMigratedYear = this.getLatestMigratedIYForLegacyRecord(migrations);
				if (latestMigratedYear != null && !incomeYearsInserted.contains(latestMigratedYear)) {
					insertIVMMigrationEntry(updatedPerson, latestMigratedYear, incomeYearsInserted, false);
				}
			}

		} else {
			if (logger.isWarnEnabled())
				logger
						.warn("Returned value is not an instance of Person (type: "
								+ entity.getClass().getName()
								+ "), not applying any after advice");
		}
	}
	// CCR 12540 identify if there is existing marked for delete record
	private boolean ivmMigrationsContainMarkedForDelete(Set data, Integer incomeYear) {
		if (incomeYear == null) return false;
		Iterator iter = data.iterator();
		while (iter.hasNext()) {
			IVMMigration migration = (IVMMigration)iter.next();
			if (migration.isStampped() && incomeYear.equals(migration.getIncomeYear()))
				return true;
		}
		return false;
	}

	private boolean ivmMigrationsContainStatus(Set data, MessageStatus.Code code, Integer incomeYear) {
		if (incomeYear == null) return false;
		Iterator iter = data.iterator();
		while (iter.hasNext()) {
			IVMMigration migration = (IVMMigration)iter.next();
			if (incomeYear.compareTo(migration.getIncomeYear()) == 0 && 
					migration.getStatus() == null && code == null)
				return true;
			if (incomeYear.compareTo(migration.getIncomeYear()) == 0 && 
					migration.getStatus() != null && code != null) {
				if (migration.getStatus().getCode().equals(code.getCode())) {
					return true;
				}
			}
		}
		
		return false;
	}
	public Integer getLatestMigratedIYForLegacyRecord(Set data) {
		List sorted = sortIVMMigrationsByIY(data);		
		Iterator iter = sorted.iterator();
		while (iter.hasNext()) {
			IVMMigration migration = (IVMMigration)iter.next();
			if (	migration.getStatus() == null &&
					migration.getLoadFailureReason() == null &&
					migration.getMigrationDate() != null &&
					migration.getIncomeYear() != null)
				return migration.getIncomeYear();
		}
		
		return null;
	}
	public Integer getMostRecentIVMMigratedIY(Set data) {
		List sorted = sortIVMMigrationsByIY(data);
		Iterator iter = sorted.iterator();
		while (iter.hasNext()) {
			IVMMigration migration = (IVMMigration)iter.next();
			if (	migration.getStatus() != null && 
					MessageStatus.COMPLETE.getCode().equals(migration.getStatus().getCode()) &&
					migration.getMigrationDate() != null &&
					migration.getIncomeYear() != null)
				return migration.getIncomeYear();
		}
		
		return null;
	}

	public List sortIVMMigrationsByIY(Set data) {
		if (data == null) 
			return new ArrayList();
		ArrayList d = new ArrayList(data);
        Comparator comparator = new Comparator() {
            public int compare(Object obj1, Object obj2) {
                Integer iy1 = (obj1 instanceof IVMMigration) ? ((IVMMigration)obj1).getIncomeYear() : null;
                Integer iy2 = (obj2 instanceof IVMMigration) ? ((IVMMigration)obj2).getIncomeYear() : null;
                return (iy1 != null && iy2 != null) ? (-iy1.compareTo(iy2)) : 0;
            }
        };
        Collections.sort(d,comparator);
        return d;
 	}
	
	private PersonIdentityTraits getTraits(Person updatedPerson,
			Person oldPerson) {
		PersonIdentityTraits pit = updatedPerson.getIdentityTraits();
		if (pit != null && pit.getSsn() != null) {
			if (pit.getSsn().getSsaVerificationStatus() == null) {
				if (oldPerson != null
						&& oldPerson.getIdentityTraits() != null
						&& oldPerson.getIdentityTraits().getSsn() != null
						&& oldPerson.getIdentityTraits().getSsn()
								.getSsaVerificationStatus() != null) {
					pit = oldPerson.getIdentityTraits(); // missing from
															// updated Person on
															// Z07
				}
			}
		}
		return pit;
	}

	protected final Person getPersonArgumentIfAny(Object[] methodArguments)
			throws ServiceException {
		Person incomingPerson = null;
		for (int i = 0; i < methodArguments.length; i++) {
			Object arg = methodArguments[i];
			if (arg instanceof Person || arg instanceof PersonEntityKey) {
				if (arg instanceof Person && incomingPerson == null) {
					incomingPerson = (Person) arg;
				} else if (arg instanceof PersonEntityKey
						&& incomingPerson == null) {
					incomingPerson = personService
							.getPerson((PersonEntityKey) arg);
				} else {
					// more than one, no way to know
					if (logger.isWarnEnabled())
						logger
								.warn("More than one incoming Person object, no way to know which is intended incoming");
					incomingPerson = null;
					break;
				}
			}
		}
		return incomingPerson;
	}

	private final SSASSNVerificationData getSSASSNVerificationDataArgumentIfAny(
			Object[] methodArguments) throws ServiceException {
		SSASSNVerificationData data = null;
		for (int i = 0; i < methodArguments.length; i++) {
			Object arg = methodArguments[i];
			if (arg instanceof SSASSNVerificationData && data == null) {
				data = (SSASSNVerificationData) arg;
			}
		}
		return data;
	}

	private final void handleUpdate(Person person, Set logs)
			throws ServiceException {
		personService.logChanges(logs);
	}

	/*
	 * Check for changes that are of interest to ESR->IVM data migration/sync
	 * process. The information about these changes are to be persisted,
	 * including the person id, the income year if the change is income year
	 * specific
	 */
	private void checkIvmSyncChanges(Set logs, Person updatedPerson,
			Person oldPerson) {
		if (oldPerson == null) {
			logger
					.error("There is no incoming person to compare IVM changes. Returning without adding any entry for IVM.");
			return;
		}

		if (checkNonIncomeYearSpecificChanges(updatedPerson, oldPerson)) {
			logs.add(createPersonChangeLogEntry(updatedPerson, null));
		}

		checkIncomeYearSpecificChanges(logs, updatedPerson, oldPerson);
	}

	private PersonChangeLogEntry createPersonChangeLogEntry(Person person,
			Integer year) {
		PersonChangeLogEntry logEntry = new PersonChangeLogEntry();
		logEntry.setPersonEntityKey(CommonEntityKeyFactory
				.createPersonIdEntityKey(person.getPersonEntityKey()
						.getKeyValueAsString()));
		logEntry.setSubmitterDescription(SUBMITTER_IVM_DM);
		logEntry.setSubmittedDate(new Date());
		logEntry.setIncomeYear(year);
		return logEntry;
	}

	/**
	 * 
	 * @param updatedPerson
	 * @param oldPerson
	 * @return True if there is non income year specific changes
	 */
	private boolean checkNonIncomeYearSpecificChanges(Person updatedPerson,
			Person oldPerson) {
		// ES 4.3_CodeCR13127_CR_BT Eligible Indicator Trigger 
		if (updatedPerson.getChangeEvents().containsKey(UseCaseName.BT_IND_CHANGE)) {
			return true;
		}
		
		// VET LINE
		// CCR 11892 IVM
		// The original check looked for a change anywhere in person, which is
		// too broad.
		// Limited the check to just traits
		PersonIdentityTraits traitsUpdated = updatedPerson.getIdentityTraits() != null ? updatedPerson
				.getIdentityTraits()
				: null;
		PersonIdentityTraits traitsOld = oldPerson.getIdentityTraits() != null ? oldPerson
				.getIdentityTraits()
				: null;
		if (traitsUpdated != null && traitsOld != null
				&& !traitsUpdated.equals(traitsOld))
			return true;
		// CCR 12062 restored the check of person in order to detect changes to
		// claim info.
		if (updatedPerson.getChangeEvents().containsKey(UseCaseName.CLAIMNUM)
				|| updatedPerson.getChangeEvents().containsKey(
						UseCaseName.CLAIMLOC)
				|| updatedPerson.getChangeEvents().containsKey(
						UseCaseName.EMPLOYMENT)
				|| updatedPerson.getChangeEvents().containsKey(
						UseCaseName.INCOMETEST_DELETED)			
				|| updatedPerson.getChangeEvents().containsKey(
						UseCaseName.IDENTITY_TRAITS)						
				|| updatedPerson.getChangeEvents().containsKey(
						UseCaseName.INSURANCE))
			return true;
		// CCR 12062
		/**
		 * CCR 12062 1. Claim Folder Location 2. Claim Folder Number 3.
		 * Employment Status (Spouse & Veteran) 4. Insurance Company Name 5.
		 * Insurance Company address (Line1, 2 and 3, city, state, Zip Code) 6.
		 * Insurance Company Phone Number 7. Group Name 8. Group Number 9. Date
		 * of Death 10. Ineligible Date & Reason
		 */
		DeathRecord updatedDeathRecord = updatedPerson.getDeathRecord();
		DeathRecord oldDeathRecord = oldPerson.getDeathRecord();
		if (updatedDeathRecord != null && oldDeathRecord == null)
			return true;
		if (updatedDeathRecord == null && oldDeathRecord != null)
			return true;
		if (updatedDeathRecord != null && oldDeathRecord != null
				&& !updatedDeathRecord.equals(oldDeathRecord))
			return true;

		IneligibilityFactor updatedIneligibilityFactor = updatedPerson
				.getIneligibilityFactor();
		IneligibilityFactor oldIneligibilityFactor = oldPerson
				.getIneligibilityFactor();
		if (updatedIneligibilityFactor != null
				&& oldIneligibilityFactor == null)
			return true;
		if (updatedIneligibilityFactor == null
				&& oldIneligibilityFactor != null)
			return true;
		if (updatedIneligibilityFactor != null
				&& oldIneligibilityFactor != null
				&& compareAbstractKeyedEntity(updatedIneligibilityFactor,
						oldIneligibilityFactor))
			return true;

		// ADD LINE, CMD LINE
		Address permAddressUpdated = updatedPerson.getPermanentAddress();
		Address permAddressOld = oldPerson.getPermanentAddress();
		
// RTC Defect # 234937  (Confidential address changes triggering IVM updates against expectation (REEG_00012313))	
// check updated person address is confidential type and if true then bypass address change logic
// confidential address should not trigger a IVM migration
//
		boolean confidential = false;
		if (permAddressUpdated != null) {
			confidential = permAddressUpdated.isConfidential();
		}
			
		if (!confidential) {		
			if ((permAddressUpdated != null && permAddressUpdated.getBadAddressReason() == null) 
					&& permAddressOld == null)
				return true;
			if (permAddressUpdated == null
					&& (permAddressOld != null && permAddressOld.getBadAddressReason() == null))
				return true;
			// they could have removed a bad reason so only look at updated		
			if ((permAddressUpdated != null && permAddressUpdated.getBadAddressReason() == null)
					&& permAddressOld != null
					&& compareAbstractKeyedEntity(permAddressUpdated, permAddressOld))
				return true;
			
			Address tempAddressUpdated = updatedPerson.getTemporaryCorrespondenceAddress();
			Address tempAddressOld = oldPerson.getTemporaryCorrespondenceAddress();		
			if ((tempAddressUpdated != null && tempAddressUpdated.getBadAddressReason() == null)
					&& tempAddressOld == null)
				return true;
			if (tempAddressUpdated == null
					&& (tempAddressOld != null && tempAddressOld.getBadAddressReason() == null))
				return true;
			// they could have removed a bad reason so only look at updated
			if ((tempAddressUpdated != null && tempAddressUpdated.getBadAddressReason() == null)
					&& tempAddressOld != null
					&& compareAbstractKeyedEntity(tempAddressUpdated, tempAddressOld))
				return true;
		}
		
		// TEL LINE
		if (compareAbstractKeyedEntitySets(updatedPerson.getPhones(), oldPerson
				.getPhones()))
			return true;

		// AWD, SCN LINE
		if (compareAbstractKeyedEntity(updatedPerson
				.getEligibilityVerification(), oldPerson
				.getEligibilityVerification())
				|| compareAbstractKeyedEntity(updatedPerson
						.getServiceConnectionAward(), oldPerson
						.getServiceConnectionAward())
				|| compareAbstractKeyedEntity(updatedPerson
						.getIncompetenceRuling(), oldPerson
						.getIncompetenceRuling()))
			return true;

		// ELI LINE
		EnrollmentDetermination newEnroll = updatedPerson
				.getEnrollmentDetermination();
		Eligibility newElig = newEnroll != null ? newEnroll
				.getPrimaryEligibility() : null;
		EnrollmentDetermination oldEnroll = oldPerson
				.getEnrollmentDetermination();
		Eligibility oldElig = oldEnroll != null ? oldEnroll
				.getPrimaryEligibility() : null;
		if (compareAbstractKeyedEntity(newElig, oldElig)
				|| compareAbstractKeyedEntity(updatedPerson
						.getIneligibilityFactor(), oldPerson
						.getIneligibilityFactor()))
			return true;

		// MON LINE
		MonetaryBenefitAward newAward = updatedPerson.getMonetaryBenefitAward();
		MonetaryBenefitAward oldAward = oldPerson.getMonetaryBenefitAward();
		if (compareAbstractKeyedEntity(newAward, oldAward)
				|| (newAward != null && oldAward != null && compareAbstractKeyedEntitySets(
						newAward.getMonetaryBenefits(), oldAward
								.getMonetaryBenefits())))
			return true;

		// INS, POL, INP LINE
		if (compareAbstractKeyedEntity(updatedPerson.getMedicaidFactor(),
				oldPerson.getMedicaidFactor())
				|| compareAbstractKeyedEntitySets(
						updatedPerson.getInsurances(), oldPerson
								.getInsurances()))
			return true;

		return false;
	}

	private IVMMigration insertIVMMigrationEntry(Person person,
			Integer incomeYear, Set<Integer> insertedIncomeYears, boolean deleteIncomeTest) throws ServiceException {
		IVMMigration ivmMigration = new IVMMigration();
		ivmMigration.setPersonEntityKey(CommonEntityKeyFactory
				.createPersonIdEntityKey(person.getPersonEntityKey()
						.getKeyValueAsString()));
		ivmMigration.setIncomeYear(incomeYear);
		// CCR 12540 explicit delete signal
		if (deleteIncomeTest) {
			ivmMigration.setStampped(true);
		}			
		ivmMigration.setRemigrate(false);			

		this.getIvmDMService().saveIVMMigration(ivmMigration,
				MessageStatus.AWAITING_TRANSMISSION);
		insertedIncomeYears.add(incomeYear);
		return ivmMigration;
	}

	public boolean satisfiesIVMSelectionCriteria(Person person,
			Integer incomeYear, PersonIdentityTraits traits) {
		/*
		 * 
		 * NEW CRITERIA for CCR 12053:
		 * IVM Selection Criteria The IVM Selection Criteria will be used in the
		 * ES to IVM Updates Triggering Section below. When checking to see if a
		 * Veteran should be transferred for a new IY, the following IVM
		 * Selection Criteria must be met:
		 * 1. SSN Verification Status = Verified and
		 * 2. Date of Death Not Populated and
		 * 3. Any one of the following is true: 
		 * 
		 * a. Primary Income Test for the Income Year or 
         * the latest primary income test not expired as of VFA
		 * start date
		 * 
		 * b. Means Test Status = MT Copay Exempt or GMT Copay Required, or
		 * Pending Adjudication or MT Copay Required
		 * 
		 * c. Priority Group = 2 or 3 or 5 or 6 or 7 or 8
		 * 
		 * d. Enrollment Status= Pending; MT Required and at least one self-reported Means Test is on file
		 */

		// Get the SSN from identity traits
		SSN ssn = traits != null ? traits.getSsn() : null;
		SSAVerificationStatus status = (ssn != null) ? ssn.getSsaVerificationStatus() : null;
		boolean meetsSSNVerificationStatusCriteria = (status != null) ? SSAVerificationStatus.VERIFIED.getCode().equals(status.getCode()) : false;

		ImpreciseDate deathDate = (person.getDeathRecord() != null) ? person.getDeathRecord().getDeathDate() : null;
		
		IncomeTest incomeTest = person.getIncomeTest(incomeYear);
		boolean futureTest = (incomeTest != null && incomeTest.isFutureTest() != null) ?
				incomeTest.isFutureTest().booleanValue() : false; 

		MeansTestStatus mtStatus = (incomeTest != null && !futureTest) ? incomeTest.getStatus(): null;
		boolean meetsIncomeTestCriteria = incomeTest != null && !futureTest &&
		(incomeTest.isPrimaryIncomeTest() != null && incomeTest.isPrimaryIncomeTest().booleanValue()) 
		&& (mtStatus != null)
		&&
		StringUtils.contains(mtStatusCodesForIVMCriteria, mtStatus.getCode());
				
		EnrollmentStatus enrollmentStatus = person.getEnrollmentDetermination() != null ? 
				person.getEnrollmentDetermination().getEnrollmentStatus() : null;
		boolean meetsEnrollmentStatusCriteria = (enrollmentStatus != null) ? 
				EnrollmentStatus.CODE_PENDING_MEANS_TEST_REQUIRED.getCode().equals(enrollmentStatus.getCode()) : false;			
		
		EnrollmentPriorityGroup priority =  person.getEnrollmentDetermination() != null ? 
				person.getEnrollmentDetermination().getPriorityGroup() : null;
		boolean meetsPriorityGroupCriteria = priority != null && 
		StringUtils.contains(enrollmentPriorityCodesForIVMCriteria, priority.getCode());
		
		if (meetsSSNVerificationStatusCriteria && deathDate == null &&
				(meetsIncomeTestCriteria || meetsPriorityGroupCriteria || 
						(meetsEnrollmentStatusCriteria && hasSelfReportedTest(person))) ) {
			return true;
		}
		
		return false;
	}

	private boolean hasSelfReportedTest(Person person) {
		if (person.getIncomeTests() != null && 
				person.getIncomeTests().isEmpty()) {
			return false;
		}
        Map incomeTestMap = person.getIncomeTests();
        Collection incomeTests = incomeTestMap.values();
        Iterator iter = incomeTests.iterator();
        while (iter.hasNext()) {
            IncomeTest incomeTest = (IncomeTest) iter.next();
            String code = incomeTest.getSource() != null ? incomeTest.getSource().getCode() : null; 
            if (!IncomeTestSource.CODE_IVM.getCode().equals(code)) {
                return true;
            }               
        }       
        return false;
	}
	private void checkIncomeYearSpecificChanges(Set logs, Person updatedPerson,
			Person oldPerson) {
		Map map = new HashMap();
		// VAM LINE
		Map newSummariesMap = updatedPerson.getPatientVisitSummaries();
		Collection keys = newSummariesMap != null ? newSummariesMap.keySet()
				: null;
		if (keys != null) {
			Iterator keyIter = keys.iterator();
			while (keyIter.hasNext()) {
				SiteYear siteYear = (SiteYear) keyIter.next();
				Integer year = siteYear.getYear();
				if (year != null) {
					if (compareAbstractKeyedEntitySets(updatedPerson
							.getPatientVisitSummaries(year), oldPerson
							.getPatientVisitSummaries(year))) {
						map.put(year, createPersonChangeLogEntry(updatedPerson,
								year));
					}

				}
			}
		}

		// CON LINE
		Set newSignatures = updatedPerson.getSignatureImages();
		if (newSignatures != null) {
			Iterator iter = newSignatures.iterator();
			while (iter.hasNext()) {
				SignatureImage newSignature = (SignatureImage) iter.next();
				Integer year = newSignature.getIncomeYear();
				if (year != null) {
					if (compareAbstractKeyedEntity(newSignature, oldPerson
							.getSignatureImage(year))) {
						map.put(year, createPersonChangeLogEntry(updatedPerson,
								year));
					}
				}
			}
		}

		// INT, IST LINE
		if (updatedPerson.getChangeEvents().containsKey(UseCaseName.FIN_INCOMETEST_CHANGE)) {
			Integer iy = new Integer((String)updatedPerson.getChangeEvents().get(
					UseCaseName.FIN_INCOMETEST_CHANGE));
			map.put(iy, createPersonChangeLogEntry(updatedPerson,iy));
		}			
		if (updatedPerson.getChangeEvents().containsKey(UseCaseName.FIN_INCTSTSTATUS_CHANGE)) {
			Integer iy = new Integer((String)updatedPerson.getChangeEvents().get(
					UseCaseName.FIN_INCTSTSTATUS_CHANGE));
			map.put(iy, createPersonChangeLogEntry(updatedPerson,iy));
		}			

		// FIN, AST, INC, EXP, SPO, DEP LINE
		Map newFinancialStatements = updatedPerson.getFinancialStatements();
		keys = newFinancialStatements != null ? newFinancialStatements.keySet()
				: null;
		if (keys != null) {
			Iterator keyIter = keys.iterator();
			while (keyIter.hasNext()) {
				Integer year = (Integer) keyIter.next();
				if (year != null) {
					FinancialStatement newStatement = updatedPerson
							.getFinancialStatement(year);
					FinancialStatement oldStatement = oldPerson
							.getFinancialStatement(year);
					if (compareAbstractKeyedEntity(newStatement, oldStatement)) {
						map.put(year, createPersonChangeLogEntry(updatedPerson,
								year));
					}
					if ((newStatement != null && oldStatement != null && compareAbstractKeyedEntitySets(
							new HashSet(newStatement.getIncome().values()),
							new HashSet(oldStatement.getIncome().values())))
							|| (newStatement != null && oldStatement != null && compareAbstractKeyedEntitySets(
									new HashSet(newStatement.getExpenses()
											.values()),
									new HashSet(oldStatement.getExpenses()
											.values())))
							|| (newStatement != null && oldStatement != null && compareAbstractKeyedEntitySets(
									new HashSet(newStatement.getAssets()
											.values()), new HashSet(
											oldStatement.getAssets().values())))
							|| (newStatement != null && oldStatement != null && compareAbstractKeyedEntity(
									newStatement.getDebt(), oldStatement
											.getDebt()))) {
						map.put(year, createPersonChangeLogEntry(updatedPerson,
								year));
						continue;
					}
					if (newStatement != null && oldStatement != null) {
						Set newSpouseFinancial = newStatement
								.getSpouseFinancials();
						Set oldSpouseFinancial = oldStatement
								.getSpouseFinancials();
						if (compareSpouseFinancialsSets(newSpouseFinancial,
								oldSpouseFinancial)) {
							map.put(year, createPersonChangeLogEntry(
									updatedPerson, year));
							continue;
						}
					}
				}
			}
		}
		if (updatedPerson.getChangeEvents().containsKey(UseCaseName.FIN_DEPENDENT_CHANGE)) {
			Integer iy = new Integer((String)updatedPerson.getChangeEvents().get(
					UseCaseName.FIN_DEPENDENT_CHANGE));
			map.put(iy, createPersonChangeLogEntry(updatedPerson, iy));
		}
		
		logs.addAll(map.values());
	}

	/*
	 * private boolean compareSpouseFinancials(SpouseFinancials
	 * newSpouseFinancial, SpouseFinancials oldSpouseFinancial) { Spouse
	 * newSpouse = newSpouseFinancial == null ? null :
	 * newSpouseFinancial.getReportedOn(); Spouse oldSpouse = oldSpouseFinancial ==
	 * null ? null : oldSpouseFinancial.getReportedOn();
	 * 
	 * return compareAbstractKeyedEntity(newSpouseFinancial,oldSpouseFinancial) ||
	 * (newSpouseFinancial!=null && oldSpouseFinancial!=null &&
	 * compareRelation(newSpouse,oldSpouse)) ||
	 * compareLookup(getSSAVerificationStatus(newSpouse),
	 * getSSAVerificationStatus(oldSpouse)); }
	 */

	/**
	 * 
	 * @param newSet
	 *            Set of AbstractKeyedEntity
	 * @param oldSet
	 *            Set of AbstractKeyedEntity
	 * @return True if the newSet has an object that is not in oldSet or newSet
	 *         has an object that has a higher version than the object with the
	 *         same identifier in oldSet
	 */
	private boolean compareAbstractKeyedEntitySets(Set newSet, Set oldSet) {
		if (newSet == null || newSet.isEmpty())
			return false;
		if (oldSet == null || oldSet.isEmpty())
			return true;
		Iterator newIter = newSet.iterator();
		// CodeCR7348 commented for defect fix. This oldIter has to be initiated
		// with in the newIter loop.
		// Iterator oldIter = oldSet.iterator();
		while (newIter.hasNext()) {
			AbstractKeyedEntity newObject = (AbstractKeyedEntity) newIter
					.next();
			boolean foundOldObjectWithSameKey = false;

			Iterator oldIter = oldSet.iterator();
			while (oldIter.hasNext()) {
				AbstractKeyedEntity oldObject = (AbstractKeyedEntity) oldIter
						.next();
				if (newObject.getEntityKey() != null
						&& newObject.getEntityKey().equals(
								oldObject.getEntityKey())) {
					foundOldObjectWithSameKey = true;
					if (newObject.getVersion() == null)
						break;
					if (oldObject.getVersion() == null)
						return true;
					if (newObject.getVersion().intValue() > oldObject
							.getVersion().intValue())
						return true;

					// if foundOldObjectWithSameKey and version number is not
					// changed then we can break
					// we do not have to loop through the oldIter any more. Can
					// can continue and check the
					// next item from the newiter.
					if (foundOldObjectWithSameKey) {
						break;
					}
				}
			}
			if (!foundOldObjectWithSameKey)
				return true;
		}
		return false;
	}

	private boolean compareRelation(Relation newRelation, Relation oldRelation) {
		if (compareAbstractKeyedEntity(newRelation, oldRelation))
			return true;
		if (newRelation != null && oldRelation != null) {
			return compareAbstractKeyedEntitySets(newRelation.getSsns(),
					oldRelation.getSsns());
		}
		return false;
	}

	private SSAVerificationStatus getSSAVerificationStatus(Relation relation) {
		SSN ssn = relation == null ? null : relation.getOfficialSsn();
		SSAVerificationStatus status = ssn == null ? null : ssn
				.getSsaVerificationStatus();
		return status;
	}

	private boolean compareSpouseFinancialsSets(Set newSet, Set oldSet) {
		if (newSet == null || newSet.isEmpty())
			return false;
		if (oldSet == null || oldSet.isEmpty())
			return true;
		Iterator newIter = newSet.iterator();

		// CodeCR7348 commented for defect fix. This oldIter has to be initiated
		// with in the newIter loop.
		// Iterator oldIter = oldSet.iterator();
		while (newIter.hasNext()) {
			SpouseFinancials newFinancials = (SpouseFinancials) newIter.next();
			boolean foundOldObjectWithSameKey = false;
			Iterator oldIter = oldSet.iterator();
			while (oldIter.hasNext()) {
				SpouseFinancials oldFinancials = (SpouseFinancials) oldIter
						.next();
				if (newFinancials.getEntityKey() != null
						&& newFinancials.getEntityKey().equals(
								oldFinancials.getEntityKey())) {
					foundOldObjectWithSameKey = true;
					if (newFinancials.getVersion() == null)
						break;
					if (oldFinancials.getVersion() == null)
						return true;
					if (newFinancials.getVersion().intValue() > oldFinancials
							.getVersion().intValue())
						return true;
					if (compareRelation(newFinancials.getReportedOn(),
							oldFinancials.getReportedOn()))
						return true;

					// if foundOldObjectWithSameKey and version number is not
					// changed then we can break
					// we do not have to loop through the oldIter any more. Can
					// can continue and check the
					// next item from the newiter.
					break;
				}
			}
			if (!foundOldObjectWithSameKey)
				return true;
		}
		return false;
	}

	private boolean compareDependentFinancialsSets(Set newSet, Set oldSet) {
		if (newSet == null || newSet.isEmpty())
			return false;
		if (oldSet == null || oldSet.isEmpty())
			return true;
		Iterator newIter = newSet.iterator();

		// CodeCR7348 commented for defect fix. This oldIter has to be initiated
		// with in the newIter loop.
		// Iterator oldIter = oldSet.iterator();
		while (newIter.hasNext()) {
			DependentFinancials newFinancials = (DependentFinancials) newIter
					.next();
			boolean foundOldObjectWithSameKey = false;

			Iterator oldIter = oldSet.iterator();
			while (oldIter.hasNext()) {
				DependentFinancials oldFinancials = (DependentFinancials) oldIter
						.next();
				if (newFinancials.getEntityKey() != null
						&& newFinancials.getEntityKey().equals(
								oldFinancials.getEntityKey())) {
					foundOldObjectWithSameKey = true;
					if (newFinancials.getVersion() == null)
						break;
					if (oldFinancials.getVersion() == null)
						return true;
					if (newFinancials.getVersion().intValue() > oldFinancials
							.getVersion().intValue())
						return true;
					if (compareRelation(newFinancials.getReportedOn(),
							oldFinancials.getReportedOn()))
						return true;

					// if foundOldObjectWithSameKey and version number is not
					// changed then we can break
					// we do not have to loop through the oldIter any more. Can
					// can continue and check the
					// next item from the newiter.
					break;
				}
			}
			if (!foundOldObjectWithSameKey)
				return true;
		}
		return false;
	}

	private boolean compareLookup(Lookup newLookup, Lookup oldLookup) {
		String newCode = newLookup == null ? null : newLookup.getCode();
		String oldCode = oldLookup == null ? null : oldLookup.getCode();

		if (newCode == null && oldCode == null) {
			return false;
		} else if ((newCode != null && oldCode == null)
				|| (newCode == null && oldCode != null)
				|| (newCode != null && oldCode != null && !newCode
						.equals(oldCode))) {
			return true;
		}
		return false;
	}

	/**
	 * 
	 * @param newObject
	 * @param oldObject
	 * @return
	 */
	private boolean compareAbstractKeyedEntity(AbstractKeyedEntity newObject,
			AbstractKeyedEntity oldObject) {
		Integer newVersion = newObject != null ? newObject.getVersion() : null;
		if (newVersion == null) {
			return false;
		}

		Integer oldVersion = oldObject != null ? oldObject.getVersion() : null;
		if (oldVersion == null) {
			return true;
		}
		return newVersion.intValue() > oldVersion.intValue();
	}

	public void afterPropertiesSet() {
		super.afterPropertiesSet();
		Validate.notNull(personService, "A PersonService is required");
	}

	/**
	 * @return Returns the personService.
	 */
	public PersonService getPersonService() {
		return personService;
	}

	/**
	 * @param personService
	 *            The personService to set.
	 */
	public void setPersonService(PersonService personService) {
		this.personService = personService;
	}

	public EntityCacheManager getEntityCacheManager() {
		return entityCacheManager;
	}

	public void setEntityCacheManager(EntityCacheManager entityCacheManager) {
		this.entityCacheManager = entityCacheManager;
	}

	public PersonHelperService getPersonHelperService() {
		return personHelperService;
	}

	public void setPersonHelperService(PersonHelperService personHelperService) {
		this.personHelperService = personHelperService;
	}

	public IVMDMService getIvmDMService() {
		return ivmDMService;
	}

	public void setIvmDMService(IVMDMService ivmDMService) {
		this.ivmDMService = ivmDMService;
	}
}
