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

import gov.va.med.esr.common.infra.ImpreciseDate;
import gov.va.med.esr.common.infra.ImpreciseDateUtils;
import gov.va.med.esr.common.model.comms.AacLetterRequest;
import gov.va.med.esr.common.model.ee.CatastrophicDisability;
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.MedicaidFactor;
import gov.va.med.esr.common.model.ee.MilitaryService;
import gov.va.med.esr.common.model.ee.MonetaryBenefit;
import gov.va.med.esr.common.model.ee.MonetaryBenefitAward;
import gov.va.med.esr.common.model.ee.ServiceConnectionAward;
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.GMTThreshold;
import gov.va.med.esr.common.model.financials.Hardship;
import gov.va.med.esr.common.model.financials.IncomeTest;
import gov.va.med.esr.common.model.financials.IncomeTestStatus;
import gov.va.med.esr.common.model.financials.IncomeThreshold;
import gov.va.med.esr.common.model.financials.SpouseFinancials;
import gov.va.med.esr.common.model.lookup.EligibilityType;
import gov.va.med.esr.common.model.lookup.EmploymentStatus;
import gov.va.med.esr.common.model.lookup.IncomeTestSource;
import gov.va.med.esr.common.model.lookup.IncomeTestType;
import gov.va.med.esr.common.model.lookup.Indicator;
import gov.va.med.esr.common.model.lookup.MeansTestStatus;
import gov.va.med.esr.common.model.lookup.MonetaryBenefitType;
import gov.va.med.esr.common.model.lookup.PseudoSSNReason;
import gov.va.med.esr.common.model.lookup.SSAVerificationStatus;
import gov.va.med.esr.common.model.lookup.SSNType;
import gov.va.med.esr.common.model.person.Dependent;
import gov.va.med.esr.common.model.person.Employment;
import gov.va.med.esr.common.model.person.Name;
import gov.va.med.esr.common.model.person.Person;
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.Spouse;
import gov.va.med.esr.common.rule.parameter.EmploymentInputParameter;
import gov.va.med.esr.common.rule.service.impl.RuleValidationMessage;
import gov.va.med.esr.common.util.CommonDateUtils;
import gov.va.med.esr.service.FinancialsHelperService;
import gov.va.med.esr.service.LookupService;
import gov.va.med.esr.service.PersonHelperService;
import gov.va.med.esr.service.ScheduledTaskService;
import gov.va.med.esr.service.UnknownLookupCodeException;
import gov.va.med.esr.service.UnknownLookupTypeException;
import gov.va.med.fw.model.lookup.Lookup;
import gov.va.med.fw.service.AbstractComponent;
import gov.va.med.fw.service.ServiceException;
import gov.va.med.fw.util.DateUtils;
import gov.va.med.fw.util.StringUtils;
import gov.va.med.fw.validation.ValidationMessage;
import gov.va.med.fw.validation.ValidationMessages;
import gov.va.med.fw.validation.ValidationServiceException;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang.Validate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class FinancialsHelperServiceImpl extends AbstractComponent
        implements FinancialsHelperService {

    /**
     * A logger to log information
     */
    protected Log                   logger                                 = LogFactory.getLog(getClass());

    private static final String     DEFAULT_DAY_MONTH_YEAR_VALUE           = "00";
    private static final BigDecimal twentyPercent                          = new BigDecimal(0.20);
    private static final BigDecimal MEDICAL_DEDUCATION_PERCENT             = new BigDecimal(0.05);

    public static final String[]    mtNotRequiredPrimaryEligibilityCodes   = {
            EligibilityType.AID_AND_ATTENDANCE.getName(), EligibilityType.HOUSEBOUND.getName(),
            EligibilityType.NSC_VA_PENSION.getName(), EligibilityType.PRISONER_OF_WAR.getName(),
            EligibilityType.PURPLE_HEART_RECIPIENT.getName(), EligibilityType.WORLD_WAR_I.getName(),
            EligibilityType.MEXICAN_BORDER_WAR.getName(), EligibilityType.HUMANTARIAN_EMERGENCY.getName(),
            EligibilityType.TRICARE_CHAMPUS.getName(), EligibilityType.SHARING_AGREEMENT.getName(),
            EligibilityType.ALLIED_VETERAN.getName(), EligibilityType.CHAMPVA.getName(),
            EligibilityType.EMPLOYEE.getName(), EligibilityType.COLLATERAL_OF_VETERAN.getName(),
            EligibilityType.OTHER_FEDERAL_AGENCY.getName(), EligibilityType.REIMBURSABLE_INSURANCE.getName() };

    public static final String[]    mtNotRequiredSecondaryEligibilityCodes = {
            EligibilityType.AID_AND_ATTENDANCE.getName(), EligibilityType.HOUSEBOUND.getName(),
            EligibilityType.NSC_VA_PENSION.getName(), EligibilityType.PRISONER_OF_WAR.getName(),
            EligibilityType.PURPLE_HEART_RECIPIENT.getName(), EligibilityType.WORLD_WAR_I.getName(),
            EligibilityType.MEXICAN_BORDER_WAR.getName()                  };

    public static final String[]    pharmacyCopayApplicablePrimEligCodes   = {
            EligibilityType.SERVICE_CONNECTED_50_TO_100_PERCENT.getName(),
            EligibilityType.AID_AND_ATTENDANCE.getName(), EligibilityType.HOUSEBOUND.getName(),
            EligibilityType.NSC_VA_PENSION.getName(), EligibilityType.PRISONER_OF_WAR.getName(),
            EligibilityType.HUMANTARIAN_EMERGENCY.getName(), EligibilityType.TRICARE_CHAMPUS.getName(),
            EligibilityType.SHARING_AGREEMENT.getName(), EligibilityType.ALLIED_VETERAN.getName(),
            EligibilityType.CHAMPVA.getName(), EligibilityType.EMPLOYEE.getName(),
            EligibilityType.COLLATERAL_OF_VETERAN.getName(), EligibilityType.OTHER_FEDERAL_AGENCY.getName(),
            EligibilityType.REIMBURSABLE_INSURANCE.getName()              };

    public static final String[]    pharmacyCopayApplicableSecEligiCodes   = {
            EligibilityType.AID_AND_ATTENDANCE.getName(), EligibilityType.HOUSEBOUND.getName(),
            EligibilityType.NSC_VA_PENSION.getName(), EligibilityType.PRISONER_OF_WAR.getName() };

    private PersonHelperService     personHelperService;
    private LookupService           lookupService;
    private ScheduledTaskService    scheduledTaskService;

    public FinancialsHelperServiceImpl() {
        super();
    }


    /**
     * @see gov.va.med.fw.service.AbstractComponent#afterPropertiesSet()
     */
    public void afterPropertiesSet() throws Exception {
        super.afterPropertiesSet();
        Validate.notNull( lookupService, "Lookup Service is required.");
        Validate.notNull( personHelperService, "Person Helper Service is required.");
        Validate.notNull( scheduledTaskService, "Scheduled Task Service is required.");
    }

    /**
     * @see gov.va.med.esr.service.FinancialsHelperService#updatePseudoSsnClock(gov.va.med.esr.common.model.person.Person, gov.va.med.esr.common.model.financials.FinancialStatement)
     */
    public void updatePseudoSsnClock(Person person, FinancialStatement fStmt) throws ServiceException {
        Validate.notNull(person, "Person can not be null");

        //Cancel the clocks if there is no Financial Statement
        if (fStmt == null) {
            //cancel existing clock if any.
            handleClock(false, person, AacLetterRequest.SPOUSE_LETTER);
            handleClock(false, person, AacLetterRequest.DEPENDENT_LETTER);
            return;
        }

        //Start or Cancel SSN clock for Spouse
        boolean startClock = false;
        Set spouseFinancials = fStmt.getSpouseFinancials();
        for (Iterator i=spouseFinancials.iterator(); i.hasNext();){
            SpouseFinancials sf = (SpouseFinancials)i.next();
            if (shouldTriggerPseudoSSnClock(sf.getReportedOn())){
                startClock = true;
                break;
            }
        }
        handleClock(startClock, person, AacLetterRequest.SPOUSE_LETTER);

        //Start or Cancel SSN clock for Dependents
        startClock = false;
        for (Iterator iter = fStmt.getDependentFinancials().iterator(); iter.hasNext();) {
            DependentFinancials df = (DependentFinancials) iter.next();
            if (shouldTriggerPseudoSSnClock(df.getReportedOn())) {
                startClock = true;
                break;
            }
        }
        handleClock(startClock, person, AacLetterRequest.DEPENDENT_LETTER);
    }
public void updateSpouseEmployment(Employment incomingSpouseEmployment, Employment onFileSpouseEmployment){
    EmploymentInputParameter employmentInput = new EmploymentInputParameter();
    if( incomingSpouseEmployment !=null && onFileSpouseEmployment !=null
            && incomingSpouseEmployment.getEmploymentStatus()!=null  ){
        if(EmploymentStatus.CODE_UNKNOWN.getCode().equals(incomingSpouseEmployment.getEmploymentStatus().getCode()) ||
                EmploymentStatus.CODE_NOT_EMPLOYED.getCode().equals(incomingSpouseEmployment.getEmploymentStatus().getCode())){
            employmentInput.removeEmployment(onFileSpouseEmployment);
         }
        if(!EmploymentStatus.CODE_RETIRED.getCode().equals(incomingSpouseEmployment.getEmploymentStatus().getCode())){
         employmentInput.removeDateOfRetirement(onFileSpouseEmployment);
        }
    }
}
    private void handleClock(boolean clockReqd, Person person, String type) throws ServiceException {
        if (clockReqd) {
            getScheduledTaskService().startPseudoSSNReasonClock(person, type);
        }
        else {
            getScheduledTaskService().cancelPseudoSSNReasonClock(person, type);
        }
    }

    private boolean shouldTriggerPseudoSSnClock(Relation relation) {
        if (relation == null) {
            return false;
        }
        SSN ssn = relation.getOfficialSsn();
        SSN pseudoSsn = relation.getPseudoSsn();

        if (pseudoSsn != null
                && pseudoSsn.getSsnText() != null
                && PseudoSSNReason.CODE_FOLLOWUP_REQD.getCode().equals(getCode(pseudoSsn.getPseudoSSNReason()))
                && (ssn == null || StringUtils.isEmpty(ssn.getSsnText())))
        {
            return true;
        }
        return false;
    }

    /**
     * @see gov.va.med.esr.service.FinancialsHelperService#adjudicateIncomeTest(gov.va.med.esr.common.model.financials.IncomeTest, gov.va.med.esr.common.model.lookup.MeansTestStatus)
     */
    public void adjudicateIncomeTest(IncomeTest currentTest, MeansTestStatus status) throws ServiceException  {
        Validate.notNull(currentTest, "IncomeTest can not be null.");
        IncomeTestStatus currentITStatus = currentTest.getIncomeTestStatus();
        MeansTestStatus currentStatus = currentITStatus == null ? null : currentITStatus.getStatus();
        String currentStatusCode = currentStatus == null ? null : currentStatus.getCode();

        if (MeansTestStatus.MT_STATUS_PENDING_ADJUDICATION.getCode().equals(currentStatusCode)) {
        	this.validateAdjudicationUpdate(currentTest);
            currentITStatus.setStatus(status);
            currentITStatus.setDeterminedStatus(status);
            Date now = new Date();
            currentTest.setAdjudicationDate(now);
            currentITStatus.setLastEditedDate(now);
            currentTest.setSource(getLookupService().getIncomeTestSourceByCode(IncomeTestSource.CODE_HEC.getCode()));

            currentTest.setSiteConductingTest(null);
        }
    }
    private void validateAdjudicationUpdate(IncomeTest test) throws ServiceException {
        if (isTestOlderThan365Days(test)) {
            ValidationMessages validationMessages = new ValidationMessages();
            validationMessages.add(new ValidationMessage(RuleValidationMessage.INCOME_TEST_CANNOT_BE_EDITED.getCode()));
            throw new ValidationServiceException(validationMessages);
        }
    }
    /**
     * @see gov.va.med.esr.service.FinancialsHelperService#processSsn(gov.va.med.esr.common.model.financials.FinancialStatement)
     */
    public void processSsn(FinancialStatement stmt) throws ServiceException {
        Validate.notNull(stmt, "FinancialStatement must not be null");

        //Process spouse ssn
        for (Iterator iter = stmt.getSpouseFinancials().iterator(); iter.hasNext();) {
            SpouseFinancials df = (SpouseFinancials) iter.next();
            if (df != null && df.getReportedOn() != null) {
                processSsn(df.getReportedOn());
            }
        }
        //Process dependents ssn
        for (Iterator iter = stmt.getDependentFinancials().iterator(); iter.hasNext();) {
            DependentFinancials df = (DependentFinancials) iter.next();
            if (df != null && df.getReportedOn() != null) {
                processSsn(df.getReportedOn());
            }
        }
    }

    /**
     * @throws UnknownLookupCodeException
     * @throws UnknownLookupTypeException
     * @see gov.va.med.esr.service.FinancialsHelperService#processSsn(gov.va.med.esr.common.model.person.Relation)
     */
    public void processSsn(Relation relation) throws UnknownLookupTypeException, UnknownLookupCodeException {
        Validate.notNull(relation, "Relation must not be null");
        SSN ssn = SSN.getSSNOfType(relation.getSsns(), SSNType.CODE_ACTIVE.getCode());
        SSN pseudoSsn = SSN.getSSNOfType(relation.getSsns(), SSNType.CODE_PSEUDO.getCode());

        if (ssn == null || StringUtils.isEmpty(ssn.getSsnText()))
        {
            if (pseudoSsn == null) {
                pseudoSsn = new SSN();
                pseudoSsn.setType(getLookupService().getSSNTypeByCode(SSNType.CODE_PSEUDO.getCode()));
                relation.addSsn(pseudoSsn);
            }
            if (pseudoSsn.getPseudoSSNReason() == null) {
                //this is the case when pseudo ssn reason is not provided - from z07
                pseudoSsn.setPseudoSSNReason(getLookupService().getPseudoSSNReasonByCode(PseudoSSNReason.CODE_FOLLOWUP_REQD.getCode()));
            }
            if (StringUtils.isEmpty(pseudoSsn.getSsnText())) { //pseudo ssn is already created - no need to recalculate it
                pseudoSsn.setSsnText(generatePseudoSSn(relation.getName(), relation.getDob()));
            }
            return;
        }

        SSAVerificationStatus ssaStatus = lookupService.getSSAVerificationStatusByCode(
                SSAVerificationStatus.NEW_RECORD.getName());
        setSSAVerificationStatus(relation.getOfficialSsn(), ssaStatus);
        for (Iterator iter = relation.getOtherSsns().iterator(); iter.hasNext();) {
            setSSAVerificationStatus((SSN) iter.next(), ssaStatus);
        }

    }

    private void setSSAVerificationStatus(SSN ssn, SSAVerificationStatus ssaStatus) throws UnknownLookupTypeException, UnknownLookupCodeException {
        // If the SSN Verification Status has not yet been set, set it to new.
        if (ssn != null && ssn.getSsaVerificationStatus() == null)
        {
            ssn.setSsaVerificationStatus(ssaStatus);
        }
    }

    /**
     * Calculate the pseudo ssn given the name and date of birth.
     * Name is converted to uppercase to translate to number based on the character
     * @param name -
     *            The name of Person/Spouse/Dependent
     * @param dob -
     *            The date of birth
     * @return - Generated pseudo SSN
     */
    private String generatePseudoSSn(Name name, ImpreciseDate dob) {
        String firstName = name == null ? null : name.getGivenName();
        String middleName = name == null ? null : name.getMiddleName();
        String lastName = name == null ? null : name.getFamilyName();

        char firstInitial = firstName == null || firstName.length() < 1 ? ' ' : firstName.charAt(0);
        char middleInitial = middleName == null || middleName.length() < 1 ? ' ' : middleName.charAt(0);
        char lastInitial = lastName == null || lastName.length() < 1 ? ' ' : lastName.charAt(0);
        StringBuffer pseudoSsn = new StringBuffer();
        pseudoSsn.append(getNameInitialMapping(firstInitial));
        pseudoSsn.append(getNameInitialMapping(middleInitial));
        pseudoSsn.append(getNameInitialMapping(lastInitial));

        String dateStr = dob == null ? null : dob.getStandardFormat();
        String day = dateStr == null || dateStr.length() < 8 ? DEFAULT_DAY_MONTH_YEAR_VALUE : dateStr
                .substring(6, 8);
        String month = dateStr == null || dateStr.length() < 6 ? DEFAULT_DAY_MONTH_YEAR_VALUE : dateStr
                .substring(4, 6);
        String year = dateStr == null || dateStr.length() < 4 ? DEFAULT_DAY_MONTH_YEAR_VALUE : dateStr
                .substring(2, 4);
        pseudoSsn.append(month);
        pseudoSsn.append(day);
        pseudoSsn.append(year);

        return pseudoSsn.toString();
    }

    private String getNameInitialMapping(char firstInitial) {
        // char[] alphas = {'A', 'B' , 'C' , 'D' , 'E' , 'F' , 'G' , 'H' , 'I' ,
        // 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
        // 'X', 'Y', 'Z'};
        String value = "0";
        switch (String.valueOf(firstInitial).toUpperCase().charAt(0)) {
        case 'A':
        case 'B':
        case 'C':
            value = "1";
            break;
        case 'D':
        case 'E':
        case 'F':
            value = "2";
            break;
        case 'G':
        case 'H':
        case 'I':
            value = "3";
            break;
        case 'J':
        case 'K':
        case 'L':
            value = "4";
            break;
        case 'M':
        case 'N':
        case 'O':
            value = "5";
            break;
        case 'P':
        case 'Q':
        case 'R':
            value = "6";
            break;
        case 'S':
        case 'T':
        case 'U':
            value = "7";
            break;
        case 'V':
        case 'W':
        case 'X':
            value = "8";
            break;
        case 'Y':
        case 'Z':
            value = "9";
            break;
        default:
            value = "0";
        }
        return value;
    }

    /**
     * Check if the given relation is active for the whole calendar year or not.
     * A relation is considered active if the relation, if the relation active
     * date is on or before january 01 and inactive date is after december 31 of
     * the same year.
     *
     * @param relation
     * @return true if the relation is active, false otherwise.
     */
    public boolean isActiveRelation(Relation relation,  Integer incomeYear) {
        if (relation.getStartDate() == null) {
            return false; // must have start date to do calculation
        }

        Calendar yearStart = Calendar.getInstance();
        yearStart.set(incomeYear.intValue(), Calendar.JANUARY, 01, 00, 00, 00);

        Calendar yearEnd = Calendar.getInstance();
        yearEnd.set(incomeYear.intValue(), Calendar.DECEMBER, 31, 23, 59, 59);

        Date startDate = ImpreciseDateUtils.getDateWithDefault(relation.getStartDate());
        Date endDate = relation.getEndDate();

        return (startDate.compareTo(yearEnd.getTime()) <= 0 && (endDate == null || endDate.compareTo(yearStart.getTime()) >= 0));

        //Calendar activeDate = Calendar.getInstance();
        //activeDate.setTime(startDate.before(yearStart.getTime() )? yearStart.getTime() : startDate);

        //Calendar inactiveDate = Calendar.getInstance();
        //inactiveDate.setTime(endDate == null || endDate.after(yearEnd.getTime() )? yearEnd.getTime() : endDate);

        //return (incomeYear.intValue() == activeDate.get(Calendar.YEAR)
        //    && incomeYear.intValue() ==  inactiveDate.get(Calendar.YEAR));
    }

	/**
	 * The rules for considering if a Spouse is active while transmitting a message to Vista.
	 *
	 * If the inactive date of spouse is null OR inactive date of a spouse is within the IY and there is no other active spouse for that IY,
	 * then consider the spouse as ACTIVE
	 * If the inactive date of a spouse is within the IY and there IS another active spouse for that IY, then consider the
	 * spouse as INACTIVE.
	 * If the inactive date of a spouse is after the IY and there no other active spouse for the IY, then consider the spouse as
	 * ACTIVE.
	 * If the inactive date of a spouse is before the IY, then ALWAYS consider it as INACTIVE.
	 *
	 * @return boolean - spouse is active
	 */
	public boolean isActiveSpouseForVistaTransmission(SpouseFinancials spouseFinancials) {
		if (spouseFinancials == null)
			return false;

		// ESR assigns only one spouse per IY as active.
		if (spouseFinancials.isActive())
			return true;

		// If it is not active, then check other conditions to determine if it
		// needs to be considered as part of active spouse segments for Vista
		boolean isActive = false;

		FinancialStatement financialStatement = spouseFinancials
				.getFinancialStatement();

		SpouseFinancials activeSpouseFinancials = financialStatement
				.getActiveSpouseFinancials();

		Integer incomeYear = spouseFinancials.getIncomeYear();
		Spouse spouse = spouseFinancials.getReportedOn();

		Date endDate = spouse.getEndDate();
		Date incomeYearStartDate = DateUtils.getYearStartDate(incomeYear
				.intValue());
		Date incomeYearEndDate = DateUtils
				.getYearEndDate(incomeYear.intValue());

		//Inactive date of spouse is null OR inactive date of a spouse is within the IY and there is no other active spouse for that IY,
		//then consider the spouse as ACTIVE
		if (endDate == null
				|| (endDate.compareTo(incomeYearStartDate) >= 0
					&& endDate.compareTo(incomeYearEndDate) <= 0 && activeSpouseFinancials == null)){
			isActive = true;
		}else if (endDate.compareTo(incomeYearEndDate) >= 0 && activeSpouseFinancials == null){
			isActive = true;
		}

		return isActive;
	}

	/**
	 * The rules for considering if a Dependent is active while transmitting a message to Vista.
	 *
	 * If the inactive date is null OR inactive date of a dependent is within the IY, then consider as ACTIVE
	 * If inactive date is after IY, consider it ACTIVE
	 * If inactive date of a dependent is before IY, consider it as INACTIVE.
	 */
	public boolean isActiveDependentForVistaTransmission(DependentFinancials dependentFinancials) {
		if (dependentFinancials == null)
			return false;

		boolean isActive = false;

		Dependent dependent = dependentFinancials.getReportedOn();

		if (dependent != null) {
			Integer incomeYear = dependentFinancials.getIncomeYear();
			Date endDate = dependent.getEndDate();
			Date incomeYearStartDate = DateUtils.getYearStartDate(incomeYear
					.intValue());

			//As long as the end date > income year start date, is is active.
			isActive = (endDate == null || endDate
					.compareTo(incomeYearStartDate) >= 0);
		}

		return isActive;

	}

    /* (non-Javadoc)
     * @see gov.va.med.esr.service.FinancialsHelperService#getActiveSpouseFinancialsForVistaTransmission(gov.va.med.esr.common.model.person.Person, java.lang.Integer)
     */
    public SpouseFinancials getActiveSpouseFinancialsForVistaTransmission(Person person, Integer incomeYear)
    {
        FinancialStatement financialStatement = person.getFinancialStatement(incomeYear);

        if(financialStatement  != null)
        {
            Set allSpouseFinancials = financialStatement.getSpouseFinancials();

            if(allSpouseFinancials != null)
            {
            	for (Iterator iter = allSpouseFinancials.iterator(); iter.hasNext();) {
					SpouseFinancials spouseFinancials = (SpouseFinancials) iter.next();

					if (isActiveSpouseForVistaTransmission(spouseFinancials))
						return spouseFinancials;
				}
            }
        }

    	return null;
    }



    /**
     * Calculate the GMT Thresholds using income year, fips code and number of
     * extra dependents. If the extra dependents equals to zero, the
     * GMTThreshold object from the lookup service is returned. If there are
     * extra dependents, GMT threshold is calculated for each extra dependent
     * and returned.
     *
     * @param incomeYear
     * @param fipsCode
     * @param extraDependents
     * @return GMTThreshold
     * @throws ServiceException
     */
    public GMTThreshold calculateGMTThresholds(Integer incomeYear, String fipsCode, int extraDependents)
            throws ServiceException {
        List gmtThresholds = getLookupService().getGMTThreshold(incomeYear, fipsCode);

        // Get the first one from the list
        if (gmtThresholds != null && gmtThresholds.size() > 0) {
            GMTThreshold gmtThreshold = (GMTThreshold) gmtThresholds.get(0);

            calculateThresholds(gmtThreshold, extraDependents);
            return gmtThreshold;
        }

        return null;
    }

    /**
     * @see gov.va.med.esr.service.FinancialsHelperService#calculateIncomeThresholds(java.lang.Integer,
     *      int)
     */
    public IncomeThreshold calculateIncomeThresholds(Integer incomeYear, int extraDependents)
            throws ServiceException {
        int dependentCount = 7 + extraDependents;

        return calculateIncomeThresholds(incomeYear, new Integer(dependentCount));
    }

    /**
     * @see gov.va.med.esr.service.FinancialsHelperService#calculateIncomeThresholds(java.lang.Integer,
     *      java.lang.Integer)
     */
    public IncomeThreshold calculateIncomeThresholds(Integer incomeYear, Integer numberOfDependents)
            throws ServiceException {
        int dependentCount = numberOfDependents != null || numberOfDependents.intValue() < 7 ? numberOfDependents
                .intValue()
                : 7;

        IncomeThreshold it = getLookupService().getIncomeThreshold(incomeYear);

        if (it != null) {
            setMTThresholds(it, dependentCount);
            setPensionThresholds(it, dependentCount);
            setMedicalExpDeductibles(incomeYear, it, dependentCount);
            setGMTInpatientCopayments(it);
        }
        return it;
    }

    /**
     * @param it
     */
    private void setGMTInpatientCopayments(IncomeThreshold it) {
        it.setGmtInpatient90DayCopay(get20Percent(it.getNinetyDayHospitalCopay()));
        it.setGmtInpatientAdd90DayCopay(get20Percent(it.getAdd90DayHospitalDeductible()));
        it.setGmtInpatientPerDiem(get20Percent(it.getInpatientPerDiem()));
    }

    /**
     * @param twentyPercent
     * @param amount
     * @return
     */
    private BigDecimal get20Percent(BigDecimal amount) {
        return amount == null ? null : amount.multiply(twentyPercent);
    }

    /**
     * @param it
     * @param dependentCount
     */
    private void setMTThresholds(IncomeThreshold it, int dependentCount) {
        BigDecimal mtThreshold0Dependent = getNotNull(it.getExemptAmount());
        BigDecimal mtThreshold1Dependent = getNotNull(it.getDependent());
        it.setMeansTestThresholdValue(0, mtThreshold0Dependent);
        it.setMeansTestThresholdValue(1, mtThreshold1Dependent);
        // for each extra dependent in addition to 1,
        // add to MT threshold for 1 dep
        BigDecimal addDependentThreshold = getNotNull(it.getAddDependentThreshold());
        for (int i = 1; i < dependentCount; i++) {
            BigDecimal mtThreshold = mtThreshold1Dependent.add(addDependentThreshold.multiply(new BigDecimal(
                    i)));
            it.setMeansTestThresholdValue(i + 1, mtThreshold);
        }
    }

    /**
     * @param it
     * @param dependentCount
     */
    private void setPensionThresholds(IncomeThreshold it, int dependentCount) {
        BigDecimal pensionThreshold0Dependent = getNotNull(it.getPensionThreshold());
        BigDecimal pensionThreshold1Dependent = getNotNull(it.getPension1Dependent());
        it.setPensionThresholdValue(0, pensionThreshold0Dependent);
        it.setPensionThresholdValue(1, pensionThreshold1Dependent);
        // for each extra dependent in addition to 1,
        // add to Pension threshold for 1 dep
        BigDecimal addDependentPension = getNotNull(it.getAddDependentPension());
        for (int i = 1; i < dependentCount; i++) {
            BigDecimal pensionThreshold = pensionThreshold1Dependent.add(addDependentPension
                    .multiply(new BigDecimal(i)));
            it.setPensionThresholdValue(i + 1, pensionThreshold);
        }
    }

    /**
     * @param it
     * @param dependentCount
     * @throws ServiceException
     */
    private void setMedicalExpDeductibles(Integer incomeYear, IncomeThreshold it, int dependentCount)
            throws ServiceException {
        Integer prevYear = new Integer(incomeYear.intValue() - 1);
        IncomeThreshold prevYearIt = getLookupService().getIncomeThreshold(prevYear);
        if (prevYearIt != null) {
            setPensionThresholds(prevYearIt, dependentCount);
            for (int i = 0; i <= dependentCount; i++) {
                BigDecimal pensionThreshold = prevYearIt.getPensionThresholdValue(i);
                BigDecimal deductibleAmount = pensionThreshold.multiply(MEDICAL_DEDUCATION_PERCENT);
                it.setMedicalDeductiblesValue(i, deductibleAmount);
            }
        }
    }

    /**
     * Calculate and set the Thresholds for each extra dependent.
     *
     * @param gmtThreshold
     * @param extraDependents
     */
    private void calculateThresholds(GMTThreshold gmtThreshold, int extraDependents) {
        BigDecimal amountFor4 = getNotNull(gmtThreshold.getThresholdValue(3));
        BigDecimal amountFor8 = getNotNull(gmtThreshold.getThresholdValue(7));

        for (int d = 1; d <= extraDependents; d++) {
            double excessAmount = d * amountFor4.doubleValue() * 0.08;
            BigDecimal extraDependentAmount = amountFor8.add(new BigDecimal(excessAmount));
            Integer extraDependentThreshold = roundTo50(extraDependentAmount);
            gmtThreshold.setThresholdValue(7 + d, new BigDecimal(extraDependentThreshold.intValue()));
        }
    }

    private Integer roundTo50(BigDecimal amount) {
        return new Integer((int) (Math.floor((amount.doubleValue() + 25.0) / 50) * 50));
    }

    /**
     * Evaluates whether the person is subject to means test.
     * Used in rule processing that determines enrollment status and priority group.
     *
     * @param person - the person to evaluate
     * @return - boolean indicating whether person is subject to means test
     */
    public boolean isSubjectToMeansTest(Person person) throws ServiceException {
        boolean subjectToMeansTest = true;
        String primaryEligibilityCode = getPrimaryEligibilityCode(person);
        // Rule 1
        if (Boolean.TRUE.equals(getMilitaryDisabilityIndicator(person))) {
            subjectToMeansTest = false;
        }
        // Rule 2
        else if (Boolean.TRUE.equals(getDischargeDueToDisabilityIndicator(person))) {
            subjectToMeansTest = false;
        }
        // Rule 3
        else if (Boolean.TRUE.equals(isEligibleForMedicaid(person))) {
            subjectToMeansTest = false;
        }

        // CCR10669 public law if catastrophically disabled is YES, not subject to means test
        else if (Boolean.TRUE.equals(getCatastrophicallyDisabledIndicator(person))) {
            subjectToMeansTest = false;
        }

        // Rule 4
        else if (EligibilityType.SC_LESS_THAN_50_PERCENT.getName().equals(primaryEligibilityCode)
                && getServiceConnectionAwardPercentNonNull(person).intValue() == 0
                && getTotalCheckAmount(person).doubleValue() > 0) {
            subjectToMeansTest = false;
        }

        // Rule 5
        else if (EligibilityType.SERVICE_CONNECTED_50_TO_100_PERCENT.getName().equals(primaryEligibilityCode)) {
            subjectToMeansTest = false;
        }

        // Rule 6
        else if (EligibilityType.SC_LESS_THAN_50_PERCENT.getName().equals(primaryEligibilityCode)
                && getServiceConnectionAwardPercentNonNull(person).intValue() > 0) {
            subjectToMeansTest = false;
        }
        // Rules 7-22
        // get Primary eligibility code (check for MT not required codes
        else if (StringUtils.contains(mtNotRequiredPrimaryEligibilityCodes, primaryEligibilityCode)) {
            subjectToMeansTest = false;
        } else {
            // Rules 23-29
            // get Secondary eligibility codes that doesn't require MT
            subjectToMeansTest = isSubjectToMeansTestSecondaryEligibility(person);
        }

        return subjectToMeansTest;
    }

    /**
     * Evaluates whether the person is subject to means test within the context of
     * the Expire Means Test batch process.  The query which retrieves people to be
     * processed eliminates records based on other factors that affect whether they
     * are subject to means test.  This method is NOT intended for use in rule processing.
     *
     * @param person - the person to evaluate
     * @return - boolean indicating whether person is subject to means test
     */
    public boolean isSubjectToMeansTestForBatchProcess(Person person) throws ServiceException {
        // Primary Eligibility is no longer used here but this call is necessary to prevent Hibernate from
        // inserting a new record rather than updating the existing one
        getPrimaryEligibilityCode(person);
        return isSubjectToMeansTestSecondaryEligibility(person);
    }

    /**
     * Determines if the person is subject to means test based on their secondary
     * eligibility.
     *
     * @param person - the person to evaluate
     * @return - boolean indicating whether person is subject to means test
     */
    private boolean isSubjectToMeansTestSecondaryEligibility(Person person) {

        // Rules 23-29
        // get Secondary eligibility codes that doesn't require MT
        List secondaryEligCodes = getSecondaryEligibilityCodes(person);
        for (Iterator i = secondaryEligCodes.iterator(); i.hasNext();) {
            String code = (String) i.next();
            if (StringUtils.contains(mtNotRequiredSecondaryEligibilityCodes, code)) {
                return false;
            }
        }

        return true;
    }

    public boolean isPhramacyCoPayApplicable(Person person) {
        boolean primaryCopayApplicable = true;

        String primaryEligibilityCode = getPrimaryEligibilityCode(person);
        List secondaryEligCodes = getSecondaryEligibilityCodes(person);

        // CCR10669 public law if catastrophically disabled is YES, not subject to means test
        if (Boolean.TRUE.equals(getCatastrophicallyDisabledIndicator(person))) {
        	primaryCopayApplicable = false;
        }

        // Rule 3-6
        else if (EligibilityType.SC_LESS_THAN_50_PERCENT.getName().equals(primaryEligibilityCode)
                && getServiceConnectionAwardPercentNonNull(person).intValue() == 0) {
            for (Iterator i = secondaryEligCodes.iterator(); i.hasNext();) {
                String code = (String) i.next();
                if (StringUtils.contains(pharmacyCopayApplicableSecEligiCodes, code)) {
                    primaryCopayApplicable = false;
                }
            }
        }

        // Rule 7
        else if (EligibilityType.SERVICE_CONNECTED_50_TO_100_PERCENT.getName().equals(primaryEligibilityCode)
                && getServiceConnectionAwardPercentNonNull(person).intValue() == 0) {
            primaryCopayApplicable = false;
        }

        // Rules 8-20
        // get Primary eligibility code (check for MT not required codes
        else if (StringUtils.contains(pharmacyCopayApplicablePrimEligCodes, primaryEligibilityCode)) {
            primaryCopayApplicable = false;
        }

        // Rules 21
        else if (EligibilityType.SC_LESS_THAN_50_PERCENT.getName().equals(primaryEligibilityCode)
                && getServiceConnectionAwardPercentNonNull(person).intValue() > 0) {

            // And not AA,HB,VA Pension Indicators
            List benefitTypeCodes = getMonetaryBenefitTypeCodes(person);
            boolean contains = benefitTypeCodes.contains(MonetaryBenefitType.CODE_AID_AND_ATTENDANCE
                    .getName())
                    || benefitTypeCodes.contains(MonetaryBenefitType.CODE_HOUSEBOUND.getName())
                    || benefitTypeCodes.contains(MonetaryBenefitType.CODE_AID_AND_ATTENDANCE.getName());

            if (isUnemployable(person).booleanValue() && !contains) {
                primaryCopayApplicable = false;
            }
        }
        return primaryCopayApplicable;
    }

    public boolean isMeansTestPermitted(Person person) {

        List secondaryEligibilityCodes = getSecondaryEligibilityCodes(person);

        // Rule 1,3
        List benefitTypeCodes = getMonetaryBenefitTypeCodes(person);
        boolean containsAllBenefitTypes = benefitTypeCodes
                .contains(MonetaryBenefitType.CODE_AID_AND_ATTENDANCE.getName())
                || benefitTypeCodes.contains(MonetaryBenefitType.CODE_HOUSEBOUND.getName())
                || benefitTypeCodes.contains(MonetaryBenefitType.CODE_VA_PENSION.getName())
                || benefitTypeCodes.contains(MonetaryBenefitType.CODE_DISABILITY_COMPENSATION.getName());

        boolean containsSubBenefitTypes = benefitTypeCodes
                .contains(MonetaryBenefitType.CODE_AID_AND_ATTENDANCE.getName())
                || benefitTypeCodes.contains(MonetaryBenefitType.CODE_HOUSEBOUND.getName())
                || benefitTypeCodes.contains(MonetaryBenefitType.CODE_VA_PENSION.getName());

        // Rule 1, 3 & 4
        if (EligibilityType.SC_LESS_THAN_50_PERCENT.getName().equals(getPrimaryEligibilityCode(person))
                && getServiceConnectionAwardPercentNonNull(person).intValue() == 0) {

            // Rule 1
            if (!containsSubBenefitTypes && getTotalCheckAmount(person).intValue() > 0
                    && benefitTypeCodes.contains(MonetaryBenefitType.CODE_DISABILITY_COMPENSATION.getName())) {
                return true;
            }

            // Rule 3 //Null returns 0 value
            if (!containsAllBenefitTypes
                    && secondaryEligibilityCodes.contains(EligibilityType.WORLD_WAR_I.getName())
                    && getTotalCheckAmount(person).intValue() == 0) {
                return true;
            }

            // Rule 4
            if (getInEligibleDate(person) != null) {
                return true;
            }
        }

        // Rule 2
        if (EligibilityType.WORLD_WAR_I.getName().equals(getPrimaryEligibilityCode(person))
                && getServiceConnectionAwardPercent(person) == null
                && getTotalCheckAmount(person).intValue() == 0 && !containsAllBenefitTypes) {
            return true;
        }

        // Rule 5
        if (EligibilityType.NSC.getName().equals(getPrimaryEligibilityCode(person))
                && getInEligibleDate(person) != null) {
            return true;
        }
        return false;
    }

    /**
     * @param person
     * @return
     */
    private Date getInEligibleDate(Person person) {
        IneligibilityFactor ief = person.getIneligibilityFactor();
        return ief != null ? ief.getIneligibleDate() : null;
    }

    /**
     * @see gov.va.med.esr.service.FinancialsHelperService#isHardShipGranted(gov.va.med.esr.common.model.person.Person)
     */
    public boolean isHardShipGranted(Person person) {
        Hardship hardShip = getHardship(getCurrentIncomeTest(person));
        if (hardShip != null && hardShip.getHardshipGranted() != null) {
            return hardShip.getHardshipGranted().booleanValue();
        } else {
            return false;
        }
    }

    /**
     * @see gov.va.med.esr.service.FinancialsHelperService#isHardShipGranted(gov.va.med.esr.common.model.person.Person,
     *      java.lang.Integer)
     */
    public boolean isHardShipGranted(Person person, Integer incomeYear) {
        Hardship hardShip = getHardship(person.getIncomeTest(incomeYear));
        if (hardShip != null && hardShip.getHardshipGranted() != null) {
            return hardShip.getHardshipGranted().booleanValue();
        } else {
            return false;
        }
    }

    public boolean isAllowedtoEditTest(Person person, Integer incomeYear) throws ServiceException {
        if (person.isDeceased() || isHardShipGranted(person, incomeYear))
            return false;
        IncomeTest test = person.getIncomeTest(incomeYear);
        IncomeTestSource source = test == null ? null : test.getSource();
        String sourceCode = source == null ? null : source.getCode();
        boolean ivmTest = IncomeTestSource.CODE_IVM.getCode().equals(sourceCode);
        if (ivmTest || isTestOlderThan365Days(test)) {
            return false;
        }

        if (!isSubjectToMeansTest(person)
                && !isMeansTestPermitted(person)
                && !isPhramacyCoPayApplicable(person)) {
            return false;

        }
        return true;
    }

    // Rules 5,7
    public boolean isAllowedtoAddMeansTest(Person person, Integer incomeYear) throws ServiceException {
        if (person.isDeceased())
            return false;

        if (isSubjectToMeansTest(person))
            return true;

        return false;
    }

    // Rules 6,8
    public boolean isAllowedtoAddPharmacyCopayTest(Person person, Integer incomeYear) throws ServiceException {
        if (person.isDeceased())
            return false;
        IncomeTest test = getCurrentIncomeTest(person);

        boolean isRxCopayAllowed = !isSubjectToMeansTest(person) && isPhramacyCoPayApplicable(person);
        if (test != null && !isTestOlderThan365Days(test) && isRxCopayAllowed)
            return true;

        if (test == null && isRxCopayAllowed)
            return true;

        return false;
    }

    // Rule 9
    public boolean isAllowedtoAddTest(Person person, Integer incomeYear) throws ServiceException {
        if (person.isDeceased())
            return false;
        IncomeTest test = getCurrentIncomeTest(person);

        if (test == null && !isSubjectToMeansTest(person) && isMeansTestPermitted(person))
            return true;

        return false;
    }

    // CR9619 VFA-SP1 SUC1434.28.5 & 6
    public boolean isAllowedtoEditNetWorth(Person person, Integer incomeYear) throws ServiceException {
        if (CommonDateUtils.isIncomeYear2009OrLatter(incomeYear))
            return false;

        return true;
    }

    public boolean isNonPrimaryType(String type) {
        return IncomeTestType.CODE_LTC_CO_PAY_EXEMPTION_TEST.getName().equals(type)
                || IncomeTestType.CODE_LTC_CO_PAY_TEST.getName().equals(type);
    }

    private boolean isTestOlderThan365Days(IncomeTest test) {
        return test == null ? false : isBefore(addYearToDate(test.getEffectiveDate()), new Date());
    }

    public Date addYearToDate(Date currentDate) {
        if (currentDate == null) {
            return null;
        }
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(currentDate);
        calendar.add(Calendar.YEAR, 1);
        if (calendar.get(Calendar.MONTH) == Calendar.FEBRUARY && calendar.get(Calendar.DAY_OF_MONTH) == 29) {
            calendar.set(Calendar.DAY_OF_MONTH, 28);
        }
        return calendar.getTime();
    }

    private BigDecimal getTotalCheckAmount(Person person) {
        MonetaryBenefitAward mba = person.getMonetaryBenefitAward();
        BigDecimal checkAmt = mba == null ? null : mba.getCheckAmount();
        return getNotNull(checkAmt);
    }

    private String getPrimaryEligibilityCode(Person person) {
        EnrollmentDetermination enrollDet = person.getEnrollmentDetermination();
        if (enrollDet != null) {
            Eligibility eligibility = enrollDet.getPrimaryEligibility();
            if (eligibility != null)
                return eligibility.getType().getCode();
        }

        return null;
    }

    private List getSecondaryEligibilityCodes(Person person) {
        List codes = new ArrayList();
        EnrollmentDetermination enrollDet = person.getEnrollmentDetermination();
        if (enrollDet != null) {
            Set eligibilities = enrollDet.getSecondaryEligibilities();
            for (Iterator i = eligibilities.iterator(); i.hasNext();) {
                Eligibility eligibility = (Eligibility) i.next();
                codes.add(eligibility.getType().getCode());
            }
        }
        return codes;
    }

    private Boolean isUnemployable(Person person) {
        ServiceConnectionAward svcConnAwd = person.getServiceConnectionAward();
        if (svcConnAwd != null)
            return getNotNull(svcConnAwd.getUnemployable());
        else
            return Boolean.FALSE;
    }

    private Integer getServiceConnectionAwardPercentNonNull(Person person) {
        return getNotNull(getServiceConnectionAwardPercent(person));
    }

    private Integer getServiceConnectionAwardPercent(Person person) {
        ServiceConnectionAward svcConnAwd = person.getServiceConnectionAward();
        return svcConnAwd != null ? svcConnAwd.getServiceConnectedPercentage() : null;
    }

    private Boolean isEligibleForMedicaid(Person person) {
        MedicaidFactor ma = person.getMedicaidFactor();
        return ma != null ? getNotNull(ma.isEligibleForMedicaid()) : Boolean.FALSE;
    }

    private Boolean getDischargeDueToDisabilityIndicator(Person person) {
        MilitaryService ms = person.getMilitaryService();
        return ms != null ? getNotNull(ms.getDischargeDueToDisability()) : Boolean.FALSE;
    }

    private Boolean getMilitaryDisabilityIndicator(Person person) {
        MilitaryService ms = person.getMilitaryService();
        return ms != null ? getNotNull(ms.getDisabilityRetirementIndicator()) : Boolean.FALSE;
    }

    private Boolean getCatastrophicallyDisabledIndicator(Person person) {
    	CatastrophicDisability catdisability = person.getCatastrophicDisability();
        return catdisability != null ? getNotNull(catdisability.isCatastrophicallyDisabled()) : Boolean.FALSE;
    }

    /**
     * Get Monetary Benefit types from Person
     *
     * @return
     */
    private List getMonetaryBenefitTypeCodes(Person person) {
        List codes = new ArrayList();
        MonetaryBenefitAward award = person.getMonetaryBenefitAward();
        if (award != null) {
            Set benefits = award.getMonetaryBenefits();
            if (benefits != null) {
                for (Iterator i = benefits.iterator(); i.hasNext();) {
                    MonetaryBenefit benefit = (MonetaryBenefit) i.next();
                    MonetaryBenefitType type = benefit.getType();
                    //REEG 5807 - Use the indicator value to determine whether benefit type is applicable or not
                    Indicator ind = benefit.getMonetaryBenefitIndicator();
                    if (type != null
                            && ind != null && Indicator.YES.getCode().equals(ind.getCode())) {
                        codes.add(type.getCode());
                    }
                }
            }
        }
        return codes;
    }

    private static Hardship getHardship(IncomeTest incomeTest) {
        Hardship hardship = (incomeTest != null) ? incomeTest.getHardship() : null;
        return hardship;
    }

    // Need to move the following methods to some utility class,

    public boolean isBefore(Date value, Date upper) {

        boolean compare = false;
        if (value != null && upper != null) {
            compare = (value.before(upper) ? true : false);
        }
        if (logger.isDebugEnabled()) {
            logger.debug(" Upper bound: " + upper);
            logger.debug(" Returned value: " + compare);
        }
        return compare;
    }

    protected BigDecimal getNotNull(BigDecimal bd) {
        return bd == null ? new BigDecimal(0) : bd;
    }

    protected Integer getNotNull(Integer bd) {
        return bd == null ? new Integer(0) : bd;
    }

    protected BigDecimal convert(Integer intValue) {
        return intValue == null ? new BigDecimal(0) : new BigDecimal(intValue.intValue());
    }

    protected Boolean getNotNull(Boolean value) {
        return value == null ? Boolean.FALSE : value;
    }

    protected String getCode(Lookup lookup) {
        return lookup == null ? null : lookup.getCode();
    }

    public IncomeTest getCurrentIncomeTest(Person person) {
        return getPersonHelperService().getCurrentIncomeTest(person);
    }

    /**
     * @return Returns the personHelperService.
     */
    public PersonHelperService getPersonHelperService() {
        return personHelperService;
    }

    /**
     * @param personHelperService
     *            The personHelperService to set.
     */
    public void setPersonHelperService(PersonHelperService personHelperService) {
        this.personHelperService = personHelperService;
    }

    /**
     * @return Returns the lookupService.
     */
    public LookupService getLookupService() {
        return lookupService;
    }

    /**
     * @param lookupService
     *            The lookupService to set.
     */
    public void setLookupService(LookupService lookupService) {
        this.lookupService = lookupService;
    }

    /**
     * @return Returns the scheduledTaskService.
     */
    public ScheduledTaskService getScheduledTaskService() {
        return scheduledTaskService;
    }

    /**
     * @param scheduledTaskService The scheduledTaskService to set.
     */
    public void setScheduledTaskService(ScheduledTaskService scheduledTaskService) {
        this.scheduledTaskService = scheduledTaskService;
    }
}