package gov.va.med.esr.common.batchprocess;

import gov.va.med.esr.common.model.CommonEntityKeyFactory;
import gov.va.med.esr.common.model.lookup.AddressType;
import gov.va.med.esr.common.model.lookup.BadAddressReason;
import gov.va.med.esr.common.model.lookup.PhoneType;
import gov.va.med.esr.common.model.party.Address;
import gov.va.med.esr.common.model.party.Phone;
import gov.va.med.esr.common.model.person.Person;
import gov.va.med.esr.common.model.person.SSN;
import gov.va.med.esr.common.model.person.id.PersonEntityKey;
import gov.va.med.esr.common.model.person.id.VPIDEntityKey;
import gov.va.med.esr.service.IdmServiceVO;
import gov.va.med.esr.service.LookupService;
import gov.va.med.esr.service.PersonIdentityTraits;
import gov.va.med.esr.service.PersonService;
import gov.va.med.esr.service.UnknownLookupCodeException;
import gov.va.med.esr.service.UnknownLookupTypeException;
import gov.va.med.fw.batchprocess.DataProcessExecutionContext;
import gov.va.med.fw.batchprocess.DataQueryProcessExecutionContext;
import gov.va.med.fw.persistent.DAOException;
import gov.va.med.fw.persistent.DAOOperations;
import gov.va.med.fw.persistent.hibernate.AbstractDAOAction;
import gov.va.med.fw.service.ServiceException;
import gov.va.med.fw.service.support.AbstractSpawnedThreadTask;
import gov.va.med.fw.util.StringUtils;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.math.BigDecimal;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.hibernate.Query;
import org.hibernate.Session;

public class MVIAddressExportProcessSpawnedThreadTask extends
	AbstractSpawnedThreadTask{

	private LookupService lookupService;
	private PersonService personService;
	private Object[] acquiredData;
	private DataQueryProcessExecutionContext context = null;
	private DAOOperations dao;

	private AddressType permAddressType = null;
	private PhoneType homePhoneType = null;

	private static final String ADDRESS_UPDATE_DATE_QUERY = "MVIAddressExportProcess.latestDateQuery";
	private static final String UPDATE_SEEDING_STATUS = "MVIAddressExportProcess.updateSeeding";
	private static final String PARAM_PERSONID = "person_id";
	private static final String PARAM_SEEDING_ID = "seedingId";
	private static final String PARAM_SEEDINGSTATUS_ID = "statusId";

	private static final BigDecimal SEEDING_STATUS_COMPLETE_ID = new BigDecimal("1758848");
	private static final BigDecimal SEEDING_STATUS_ERROR_ID = new BigDecimal("1758849");

	/*
	 * (non-Javadoc)
	 *
	 * @see gov.va.med.fw.service.support.SpawnedThreadTask#execute()
	 */
	public void executeTask() throws Throwable {

		BigDecimal personId = null;
		BigDecimal seedingRecordId = null;
		Date batchEndDate = null;

		try {
			seedingRecordId = (BigDecimal) acquiredData[0];
	        personId = (BigDecimal) acquiredData[1];
	        batchEndDate = (Date) acquiredData[4];

	        // check to see if the address/phone data has already been updated since the batch job ended
	        // otherwise ignore the record, mark it complete, and continue to the next
	        if (! isAddressPhoneUpdated(personId, batchEndDate)) {
				String vpidValue = (String) acquiredData[2];

				// get ESR Correlation first
				VPIDEntityKey key = CommonEntityKeyFactory.createVPIDEntityKey(vpidValue);
				PersonEntityKey personKey = CommonEntityKeyFactory.createPersonIdEntityKey(personId);
				PersonIdentityTraits traits = personService.getESRCorrelation(key);
				Person incoming = personService.getPersonWithoutIdentityTraits(personKey);

				// overlay address data and call update person
				IdmServiceVO idmServiceVO = convertTraitsToIdmServiceVO(traits);

				//CCR13856, add death data in case this job is ever run again
				idmServiceVO.setDeathRecord(incoming.getDeathRecord());
				idmServiceVO.setDeathEvent(false);

			    // use long vpid form for 200ESR correlation
				idmServiceVO.setVpid(key);

				Address address = populateAddressData();
				overlayAddress(idmServiceVO, address);

				Phone phone = populatePhoneData();
				overlayPhone(idmServiceVO, phone);

				personService.updateProfileForESRCorrelation(idmServiceVO);
				context.getProcessStatistics().incrementNumberOfSuccessfulRecords();
	        }
	        else {
	        	// Record already updated, so skipping it.  Incrementing total record count
				((MVIAddressExportProcessStatistics)context.getProcessStatistics()).incrementNumberOfIgnoredRecords();
	        }

	        updateSeedingStatusToComplete(seedingRecordId);

		} catch (Exception ex) {
			//ERROR situation
			handleFailure(context, seedingRecordId, personId, ex);
		}

        finally {

			MVIAddressExportProcess.adjustTaskCount(
					context, -1);
			Object threadCreator = getThreadCreator(context);
			synchronized (threadCreator) {
				threadCreator.notifyAll();
			}

			/*
			 * force release of context as thay will hold the ThreadPool(->Threads)
			 * this is necessary to GC ThreadPool threads at end
			 */
			context = null;
		}
	}

	private boolean isAddressPhoneUpdated(BigDecimal personId, Date batchEndDate) throws DAOException
	{
		// execute query to retrieve the most recently modified_date for the person's address and phone
        Map contextData = new HashMap();
        contextData.put("personId", personId);

        // check to see if person is alive
        AbstractDAOAction callback = new AbstractDAOAction(contextData) {
            public Object execute(Session session) {
                Query query = session.getNamedQuery(ADDRESS_UPDATE_DATE_QUERY);
                query.setParameter(PARAM_PERSONID, (BigDecimal)getContextData().get("personId"));
                return query.uniqueResult();
            }
        };

        Date addressUpdateDate = (Date) getDao().execute(callback);

        return (addressUpdateDate != null && addressUpdateDate.after(batchEndDate));
	}

    private IdmServiceVO convertTraitsToIdmServiceVO(PersonIdentityTraits traits) throws ServiceException
	{
		IdmServiceVO idmServiceVO = new IdmServiceVO();

		idmServiceVO.setNames(traits.getNames());
	    SSN ssnOfficial = traits.getSsn();
	    if( ssnOfficial != null ) {
	    	idmServiceVO.setSsn(ssnOfficial);
	    }
	    idmServiceVO.setGender(traits.getGender());
	    idmServiceVO.setBirthRecord(traits.getBirthRecord());
	    idmServiceVO.setDeathEvent(false);
	    idmServiceVO.setVpid(traits.getVpid());
	    idmServiceVO.setPatientType(traits.getPatientType());
	    idmServiceVO.setVeteran(traits.isVeteran());
	    idmServiceVO.setServiceConnected(traits.isServiceConnected());
	    idmServiceVO.setPreferredFacilty(traits.getPreferredFacilty());
	    idmServiceVO.setMothersMaidenName(traits.getMothersMaidenName());
	    //WI 305033 Backout - Sigi not ready on MVI side yet
	    //idmServiceVO.setSigi(traits.getSigi());

	    return idmServiceVO;
	}

	private Address populateAddressData() throws UnknownLookupTypeException, UnknownLookupCodeException
	{
		Address address = new Address();
		address.setType(getPermAddressType());

		String badAddressReasonCd = (String)acquiredData[5];
		if (! StringUtils.isEmpty(badAddressReasonCd)) {
			BadAddressReason badAddrReason = getLookupService().getBadAddressReasonByCode(badAddressReasonCd);
			address.setBadAddressReason(badAddrReason);
		}

		address.setLine1((String)acquiredData[6]);
		address.setLine2((String)acquiredData[7]);
		address.setLine3((String)acquiredData[8]);
		address.setCity((String)acquiredData[9]);
		address.setState((String)acquiredData[10]);
		address.setZipCode((String)acquiredData[11]);
		address.setZipPlus4((String)acquiredData[12]);
		address.setCountry((String)acquiredData[13]);
		return address;
	}

	private Phone populatePhoneData() throws UnknownLookupTypeException, UnknownLookupCodeException
	{
		Phone phone = new Phone();
		phone.setType(getHomePhoneType());
		phone.setPhoneNumber((String)acquiredData[14]);
		return phone;
	}

    private void overlayAddress(IdmServiceVO idmServiceVO, Address address) {
    	idmServiceVO.setAddresses(null);
    	idmServiceVO.addAddress(address);
     }

    private void overlayPhone(IdmServiceVO idmServiceVO, Phone phone) {
    	idmServiceVO.setPhones(null);
    	idmServiceVO.addPhone(phone);
     }

	private Object getThreadCreator(DataProcessExecutionContext context) {
		return context.getContextData().get(MVIAddressExportProcess.CONTEXT_THREAD_CREATOR);
	}

	private void handleFailure(DataProcessExecutionContext context,
			BigDecimal seedingRecordId, BigDecimal personId, Exception e) {

		try {
			updateSeedingStatusToError(seedingRecordId);
		} catch(DAOException daoe)  {
			if (logger.isErrorEnabled()) {
				logger.error("Error: unable to set seeding status to ERROR for seedingRecordId = " + seedingRecordId);
			}
		}

		String exceptionText = null;
		if (e == null) {
			exceptionText = "Error: Unable to process person: " + personId;
		}
		else {
				exceptionText = "Error: Unable to process person: "
						+ personId + " because of exception: " + e;
		}
		if (logger.isErrorEnabled()) {
			logger.error(exceptionText);
		}
		context.getProcessStatistics().incrementNumberOfErrorRecords();
		context.getExceptionData().add(personId);
		context.getExceptionData().add(exceptionText + "\n\n");

        if ( e != null )
        {
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw));
            String trackTrace =  sw.toString();
            context.getExceptionData().add(trackTrace);
        }
	}

	private AddressType getPermAddressType() throws UnknownLookupTypeException, UnknownLookupCodeException
	{
		if (permAddressType == null) {
			permAddressType = getLookupService().getAddressTypeByCode(AddressType.CODE_PERMANENT_ADDRESS.getCode());
		}
		return permAddressType;
	}

	private PhoneType getHomePhoneType() throws UnknownLookupTypeException, UnknownLookupCodeException
	{
		if (homePhoneType == null) {
			homePhoneType = getLookupService().getPhoneTypeByCode(PhoneType.CODE_HOME.getCode());
		}
		return homePhoneType;
	}

	private void updateSeedingStatusToComplete(BigDecimal seedingRecordId) throws DAOException {
		updateSeedingStatus(seedingRecordId, SEEDING_STATUS_COMPLETE_ID);
	}

	private void updateSeedingStatusToError(BigDecimal seedingRecordId) throws DAOException {
		updateSeedingStatus(seedingRecordId, SEEDING_STATUS_ERROR_ID);
	}

	/**
	 * Updates the seeding status in the database record; also update the processed date of the seeding record
	 *
	 * @param seedingRecordId
	 * @param seedingStatusId
	 * @throws DAOException
	 * @throws ServiceException
	 */
	private void updateSeedingStatus(BigDecimal seedingRecordId, BigDecimal seedingStatusId) throws DAOException {
		Map contextData = new HashMap();
		contextData.put("seedingRecordId", seedingRecordId);
		contextData.put("seedingStatusId", seedingStatusId);

		/**
		 * Note -- due to the temporary nature of this batch job and the related data table (one time use), it was decided
		 * Hibernate mapping not be used for this table. Therefore direct SQL query and SQL updates were used.
		 * However since HibernateTemplate Query statements does not support SQL update, PreparedStatements were
		 * used.  This is not the general practice though.
		 */
		AbstractDAOAction callback = new AbstractDAOAction(contextData) {
			public Object execute(Session session) throws DAOException {
				Integer returnCount = null;

				Query query = session.getNamedQuery(UPDATE_SEEDING_STATUS);
				String prepareStmt = query.getQueryString();

	           	if (prepareStmt != null && prepareStmt.length() > 0) {
	           		try {
						PreparedStatement ps = session.connection().prepareStatement(prepareStmt);
						ps.setBigDecimal(1, (BigDecimal)getContextData().get("seedingStatusId"));
						ps.setBigDecimal(2, (BigDecimal)getContextData().get("seedingRecordId"));
						returnCount = new Integer(ps.executeUpdate());
	           		} catch (SQLException ex) {
						throw new DAOException(
								"Failed to execute prepare statement: " + prepareStmt, ex);
					}
	           	}
	           	return returnCount;
			}
 		};

     	Integer updateCount = (Integer) getDao().execute(callback);
     	if (updateCount.intValue() < 1) {
	       throw new DAOException("SeedingStatus not updated for esr_addr_phne_batch_id = " + seedingRecordId);
     	}
	}

	/**
	 * @return Returns the acquiredData.
	 */
	public Object[] getAcquiredData() {
		return acquiredData;
	}

	/**
	 * @param acquiredData
	 *            The acquiredData to set.
	 */
	public void setAcquiredData(Object[] acquiredData) {
		this.acquiredData = acquiredData;
	}


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

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

	/**
     * @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 dao.
	 */
	public DAOOperations getDao() {
		return dao;
	}

	/**
	 * @param dao
	 *            The dao to set.
	 */
	public void setDao(DAOOperations dao) {
		this.dao = dao;
	}

	/*
	 * all getter/setter for static variables
	 */
	public DataQueryProcessExecutionContext getContext() {
		return context;
	}

	public void setContext(DataQueryProcessExecutionContext context) {
		this.context = context;
	}
}
