
package gov.va.med.cds.transaction;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.Node;

import commonj.work.WorkException;
import commonj.work.WorkItem;
import gov.va.med.cds.clinicaldata.DomainClinicalRecordElementHelperInterface;
import gov.va.med.cds.clinicaldata.DomainEntryPoint;
import gov.va.med.cds.clinicaldata.Operation;
import gov.va.med.cds.common.person.correlation.PersonIdentifierInterface;
import gov.va.med.cds.exception.ErrorCodeEnum;
import gov.va.med.cds.exceptionframework.ExceptionHandler;
import gov.va.med.cds.filter.CdsFilterInterface;
import gov.va.med.cds.filter.EntryFilterInterface;
import gov.va.med.cds.persistence.PersistenceException;
import gov.va.med.cds.persistence.PersistenceLocatorInterface;
import gov.va.med.cds.persistence.ReadException;
import gov.va.med.cds.persistence.ReadPersistenceManagerInterface;
import gov.va.med.cds.persistence.WritePersistenceManagerInterface;
import gov.va.med.cds.request.vista.write.CdsWriteInterface;
import gov.va.med.cds.template.TemplateHelperInterface;

/**
 * 
 * Performs Create/update/append/delete/read in a Spring intiated transaction
 * context. See gov.va.med.cds.config.transactionContext.xml for Spring DI and
 * AOP/Tx details.
 * 
 */
public class DefaultTransactionManager implements TransactionManagerInterface, ValidNonVistaSitesMBeanInterface {
	protected PersistenceLocatorInterface persistenceLocator = null;
	protected WorkManagerInterface workManager = null;
	private Map<String, Set<DomainEntryPoint>> templateIdToDomainEntryPointListMap = null;
	private List<String> validNonVistaSites;
	private List<String> vistaCreateTemplateDomainEntryPointsList;
	private CdsWriteInterface cdsVistaWrite;

	private int createdRecordLimitIndex = 20;// initial default value -
												// configurable
	private TemplateHelperInterface templateHelper;
	private Map<String, DomainClinicalRecordElementHelperInterface> domainClinicalRecordOwnerMap;
	private Map<String, String> templateTimeoutMap = null;

	private static final String CDS_XPATH_OF_WARNING_MSG = "//errorSection/warnings/warning/displayMessage";
	private static final String CDS_XPATH_OF_WARNING_MSG_SINGLE = "//errorSection/warnings/displayMessage";
	private static final String CDS_XPATH_OF_ERROR_MSG = "//errorSection/errors/error/displayMessage";

	public void setTemplateTimeoutMap(Map<String, String> templateTimeoutMap) {
		this.templateTimeoutMap = templateTimeoutMap;
	}

	/**
	 * 
	 * @see gov.va.med.cds.transaction.TransactionManagerInterface#performReadOnClinicalData(gov.va.med.cds.filter.CdsPersistenceFilterInterface)
	 */
	public List<Document> performReadOnClinicalData(CdsFilterInterface filter) throws PersistenceException {
		return this.performReadOnClinicalData(null, null, filter);
	}

	/**
	 * 
	 * @see gov.va.med.cds.transaction.TransactionManagerInterface#performReadOnClinicalData(gov.va.med.cds.filter.CdsPersistenceFilterInterface)
	 */
	@SuppressWarnings("rawtypes")
	@Override
	public List<Document> performReadOnClinicalData(String templateId, String requestId, CdsFilterInterface filter)
			throws PersistenceException {
		List<Document> readResults = new ArrayList<Document>();
		List<WorkItem> workQueue = new ArrayList<WorkItem>();

		List<PersonIdentifierInterface> filterPersonIdentifiers = null;
		boolean isPatientCentricFilter = filter.isPatientCentricFilter();
		if (isPatientCentricFilter) {
			filterPersonIdentifiers = filter.getPersonIdentifiers();
		}
		List<Document> resolvedIdsErrors = null;

		for (EntryFilterInterface entryFilter : filter.getEntryPointFilters()) {
			// the persistenceLocator should change to accept an EntryFilter
			// instead of CdsPersistenceFilter
			Collection<ReadPersistenceManagerInterface> readablePersistenceManagers = persistenceLocator
					.getReadPersistenceManagers(entryFilter, filterPersonIdentifiers);

			// if client sends only dfns in the filter
			if (isPatientCentricFilter && filter.getNationalId() == null) {
				resolvedIdsErrors = chkAssigningFacilities(readResults, filterPersonIdentifiers,
						entryFilter.getRequestId(), entryFilter.getTemplateId(), readablePersistenceManagers,
						templateHelper.getApplicationName(templateId));
			}

			for (ReadPersistenceManagerInterface readPersistenceManager : readablePersistenceManagers) {
				try {
					workQueue.add(workManager
							.schedule(new ReadWork(readPersistenceManager, entryFilter, filterPersonIdentifiers)));
				} catch (WorkException we) {
					ExceptionHandler.handleException(
							new ReadException(ErrorCodeEnum.READ_REQUEST_WORK_SCHEDULING_EXCEPTION, we),
							entryFilter.getTemplateId(), entryFilter.getRequestId(),
							templateHelper.getApplicationName(templateId));
				}
			}

			// wait for all of the scheduled work to complete before moving on.
			try {
				workManager.waitForAll(workQueue);
			} catch (WorkException we) {
				// log the fact that while this thread was waiting for all work
				// to complete,
				// it was interrupted.
				ExceptionHandler.handleException(
						new ReadException(ErrorCodeEnum.READ_REQUEST_WORK_WAIT_INTERRUPTED_EXCEPTION, we),
						entryFilter.getTemplateId(), entryFilter.getRequestId(),
						templateHelper.getApplicationName(templateId));
			}

			ReadWork doneWork = null;
			int failedCount = 0;

			for (WorkItem workItem : workQueue) {
				try {
					doneWork = ((ReadWork) workItem.getResult());

					if (doneWork != null) {
						failedCount = doneWork.isCompleteFailure() ? ++failedCount : failedCount;
						readResults.add(processEntryTypeQueries(doneWork.getResult(), entryFilter.getXpathQuery()));
					}

				} catch (WorkException e) {
					// this would represent an exception that occurred
					// while doing the work
					failedCount++;
				}
			}

			if ((workQueue.size() == failedCount) && (resolvedIdsErrors == null || resolvedIdsErrors.size() == 0)) {
				List<String> errMsgs = new ArrayList<String>();
				Element e = null;
				String msgContent = null;
				List content = null;
				String errorMsg = "No data found for the read request";

				for (Document readResult : readResults) {
					content = readResult.selectNodes(CDS_XPATH_OF_WARNING_MSG);
					if (content.size() == 0) {
						content = readResult.selectNodes(CDS_XPATH_OF_WARNING_MSG_SINGLE);
						if (content.size() == 0)
							content = readResult.selectNodes(CDS_XPATH_OF_ERROR_MSG);
					}

					e = (Element) content.get(0);
					msgContent = e.getText();
					errMsgs.add(msgContent);
				}

				if (errMsgs.size() > 0) {
					errorMsg = errMsgs.toString();
				}

				throw new PersistenceException(ErrorCodeEnum.READ_REQUEST_ALL_DATASOURCES_FAILED, errorMsg);
			}

			if (resolvedIdsErrors != null && resolvedIdsErrors.size() > 0) {
				for (Document resolvedIdsError : resolvedIdsErrors) {
					readResults.add(resolvedIdsError);
				}
			}
		}

		return readResults;
	}

	@Override
	public List<Element> performCUADOnClinicalData(Operation operation, Document clinicalData, String templateId,
			String requestId) throws PersistenceException {
		List<Element> recordIdentifiers = new ArrayList<Element>();
		Set<DomainEntryPoint> domainEntryPoints = this.templateIdToDomainEntryPointListMap.get(templateId);

		if (domainEntryPoints == null) {
			throw new PersistenceException(ErrorCodeEnum.WRITE_REQUEST_SIMPLE_INVALID_TEMPLATE_ID);
		}

		Element documentRoot = clinicalData.getRootElement();
		Element patientElement = documentRoot.element("patient");

		if (null == patientElement) {
			patientElement = documentRoot.element("patients");
			for (DomainEntryPoint domainEntryPoint : domainEntryPoints) {
				String aresultId = null;
				WritePersistenceManagerInterface writePersistenceManager = persistenceLocator
						.getWritePersistenceManager(domainEntryPoint);
				aresultId = writePersistenceManager.performCUADOnPatientsData(templateId, requestId, operation,
						patientElement, domainEntryPoint);
				// String performCUADOnPatientsData( String templateId, String
				// requestId, Operation requestType, Object object,
				// DomainEntryPoint domainEntryPoint )
				recordIdentifiers.add(this.templateHelper.getWriteResponseGenerator(templateId)
						.generateIdentifierElement(aresultId, patientElement));
			}
		} else {
			for (DomainEntryPoint domainEntryPoint : domainEntryPoints) {
				List<Element> clinicalRecords = domainEntryPoint.getClinicalRecords(patientElement);
				if (clinicalRecords != null && clinicalRecords.size() > 0) {
					int i = 0;
					WritePersistenceManagerInterface writePersistenceManager = persistenceLocator
							.getWritePersistenceManager(domainEntryPoint);

					for (Element clinicalRecord : clinicalRecords) {
						String resultId = null;

						/*
						 * The requestId is needed for creation of the record -
						 * add the requestId from the clinicalDocument into the
						 * clinical record so that Hibernate can locate and
						 * store the value in the database. An ID element is
						 * needed in the document at the clinical record level
						 * in order for hibernate to construct the update /
						 * delete query against the main table.
						 */

						// first validate assigningFacility - if invalid a
						// fatal
						// error will be thrown and CUAD Operation will NOT
						// proceed
						if (operation.equals(Operation.Create) || operation.equals(Operation.Update)) {
							validateAssigningFacility(clinicalRecord, domainEntryPoint);
						}
						if (operation.equals(Operation.Create)) {
							addRequestIdToClinicalRecordElement(clinicalRecord, requestId);
							resultId = writePersistenceManager.performCUADOnClinicalData(templateId, operation,
									clinicalRecord, domainEntryPoint);

						}
						if (operation.equals(Operation.Update) || operation.equals(Operation.Delete)) {
							resultId = addIdtoClinicalElement(clinicalRecord, domainEntryPoint);
							resultId = writePersistenceManager.performCUADOnClinicalData(templateId, operation,
									clinicalRecord, domainEntryPoint);
						}

						recordIdentifiers.add(this.templateHelper.getWriteResponseGenerator(templateId)
								.generateIdentifierElement(resultId, clinicalRecord));
						i++;
					}
				}

			}
		}
		return recordIdentifiers;
	}

	public List<Document> performCUADVistAData(Operation operation, Document clinicalData, String templateId, String requestId)
			throws PersistenceException {
		List<Document> results = null;
		Set<DomainEntryPoint> domainEntryPoints = this.templateIdToDomainEntryPointListMap.get(templateId);

		if (domainEntryPoints == null) {
			throw new PersistenceException(ErrorCodeEnum.WRITE_REQUEST_SIMPLE_INVALID_TEMPLATE_ID);
		}

		Element documentRoot = clinicalData.getRootElement();
		Element patientElement = documentRoot.element("patient");

		if (null != patientElement) {
			for (DomainEntryPoint domainEntryPoint : domainEntryPoints) {
				List<Element> recordsParent = domainEntryPoint.getClinicalRecords(patientElement);
				cdsVistaWrite.setDataElement(recordsParent.get(0));
				cdsVistaWrite.setTemplateId(templateId);
				cdsVistaWrite.setRequestId(requestId);
				cdsVistaWrite.setDomainEntryPoint(domainEntryPoint);
				cdsVistaWrite.extractData();
																							
				results = performWriteOnPatientData(templateId, requestId, cdsVistaWrite);
				
			}
		}

		return results;
	}

	@SuppressWarnings("rawtypes")
	private List<Document> performWriteOnPatientData(String templateId, String requestId, CdsWriteInterface cdsWrite)
			throws PersistenceException {
		List<Document> writeResults = new ArrayList<Document>();
		List<WorkItem> workQueue = new ArrayList<WorkItem>();
		List<PersonIdentifierInterface> personIds = null;
		List<Document> resolvedIdsErrors = null;
		List<PersonIdentifierInterface> personIdentifiers = new ArrayList<PersonIdentifierInterface>();
		List<EntryFilterInterface> recordsList = cdsWrite.getCreateData();

		for (EntryFilterInterface record : recordsList) {

			Collection<ReadPersistenceManagerInterface> readablePersistenceManagers = persistenceLocator
					.getReadPersistenceManagers(record, record.getPersonIdentifiers());

			resolvedIdsErrors = chkAssigningFacilities(writeResults, record.getPersonIdentifiers(),
						record.getRequestId(), record.getTemplateId(), readablePersistenceManagers,
						templateHelper.getApplicationName(templateId));
		

			for (ReadPersistenceManagerInterface readPersistenceManager : readablePersistenceManagers) {
				try {
					workQueue.add(workManager
							.schedule(new ReadWork(readPersistenceManager, record, record.getPersonIdentifiers())));
				} catch (WorkException we) {
					ExceptionHandler.handleException(
							new ReadException(ErrorCodeEnum.READ_REQUEST_WORK_SCHEDULING_EXCEPTION, we),
							record.getTemplateId(), record.getRequestId(),
							templateHelper.getApplicationName(templateId));
				}
			}

			// wait for all of the scheduled work to complete before moving on.
			try {
				workManager.waitForAll(workQueue);
			} catch (WorkException we) {
				// log the fact that while this thread was waiting for all work
				// to complete,
				// it was interrupted.
				ExceptionHandler.handleException(
						new ReadException(ErrorCodeEnum.READ_REQUEST_WORK_WAIT_INTERRUPTED_EXCEPTION, we),
						record.getTemplateId(), record.getRequestId(),
						templateHelper.getApplicationName(templateId));
			}

			ReadWork doneWork = null;
			int failedCount = 0;

			for (WorkItem workItem : workQueue) {
				try {
					doneWork = ((ReadWork) workItem.getResult());

					if (doneWork != null) {
						failedCount = doneWork.isCompleteFailure() ? ++failedCount : failedCount;
						writeResults.add(processEntryTypeQueries(doneWork.getResult(), record.getXpathQuery()));
					}

				} catch (WorkException e) {
					// this would represent an exception that occurred
					// while doing the work
					failedCount++;
				}
			}

			if ((workQueue.size() == failedCount) && (resolvedIdsErrors == null || resolvedIdsErrors.size() == 0)) {
				List<String> errMsgs = new ArrayList<String>();
				Element e = null;
				String msgContent = null;
				List content = null;
				String errorMsg = "Write request failure - No data response for the Write request";

				for (Document writeResult : writeResults) {
					content = writeResult.selectNodes(CDS_XPATH_OF_WARNING_MSG);
					if (content.size() == 0) {
						content = writeResult.selectNodes(CDS_XPATH_OF_WARNING_MSG_SINGLE);
						if (content.size() == 0)
							content = writeResult.selectNodes(CDS_XPATH_OF_ERROR_MSG);
					}

					e = (Element) content.get(0);
					msgContent = e.getText();
					errMsgs.add(msgContent);
				}

				if (errMsgs.size() > 0) {
					errorMsg = errMsgs.toString();
				}

				throw new PersistenceException(ErrorCodeEnum.READ_REQUEST_ALL_DATASOURCES_FAILED, errorMsg);
			}

			if (resolvedIdsErrors != null && resolvedIdsErrors.size() > 0) {
				for (Document resolvedIdsError : resolvedIdsErrors) {
					writeResults.add(resolvedIdsError);
				}
			}
		}
		
		return writeResults;
	}

	/**
	 * For all created records the request ID is required to store with the data
	 * - however the element comes inside the document in a place that isn't
	 * reachable for each clinical record being processed. This method
	 * manipulates the element and adds the request ID so that it is accessible
	 * to the Hibernate mapping layer.
	 * 
	 * @param clinicalRecord
	 * @param requestId
	 */
	protected void addRequestIdToClinicalRecordElement(Element clinicalRecord, String requestId) {
		clinicalRecord.addElement("requestId").addText(requestId);

	}

	private String addIdtoClinicalElement(Element clinicalRecord, DomainEntryPoint domainEntryPoint) {

		DomainClinicalRecordElementHelperInterface domainClinicalRecordHelper = getDomainClinicalRecordElementHelper(
				domainEntryPoint);
		return domainClinicalRecordHelper.addIdToClinicalRecordElement(clinicalRecord);
	}

	private DomainClinicalRecordElementHelperInterface getDomainClinicalRecordElementHelper(
			DomainEntryPoint domainEntryPoint) {

		DomainClinicalRecordElementHelperInterface domainClinicalRecordHelper = this.domainClinicalRecordOwnerMap
				.get(domainEntryPoint.getName());
		if (domainClinicalRecordHelper == null) {
			domainClinicalRecordHelper = this.domainClinicalRecordOwnerMap
					.get(DomainClinicalRecordElementHelperInterface.DEFAULT_DOMAIN_OWNER);
		}

		return domainClinicalRecordHelper;
	}

	private void validateAssigningFacility(Element clinicalRecord, DomainEntryPoint domainEntryPoint)
			throws PersistenceException {
		// if assigingFacility is invalid - throw fatal error and stop CUAD
		// operation
		DomainClinicalRecordElementHelperInterface domainClinicalRecordHelper = getDomainClinicalRecordElementHelper(
				domainEntryPoint);
		String assigningFacilityValue = domainClinicalRecordHelper.getAssigningFacilityValue(clinicalRecord);

		if (assigningFacilityValue != null && !this.validNonVistaSites.contains(assigningFacilityValue)) {
			throw new PersistenceException(ErrorCodeEnum.INVALID_ASSIGNING_FACILITY, assigningFacilityValue);
		}

	}

	/**
	 * Set the persistence locator to be used to locate the persistence managers
	 * for this transaction manager.
	 * 
	 * @param persistenceLocator
	 *            The persistence locator.
	 */
	/**
	 * @param persistenceLocator
	 */
	public void setPersistenceLocator(PersistenceLocatorInterface persistenceLocator) {
		this.persistenceLocator = persistenceLocator;
	}

	public void setWorkManager(WorkManagerInterface workManager) {
		this.workManager = workManager;
	}

	public void setTemplateIdToDomainEntryPointListMap(
			Map<String, Set<DomainEntryPoint>> templateIdToDomainEntryPointListMap) {
		this.templateIdToDomainEntryPointListMap = templateIdToDomainEntryPointListMap;
	}

	public void setValidNonVistaSites(List<String> validNonVistaSites) {
		this.validNonVistaSites = validNonVistaSites;
	}

	public void setTemplateHelper(TemplateHelperInterface templateHelper) {
		this.templateHelper = templateHelper;
	}

	/**
	 * Apply XPath query to filter out unwanted elements.
	 */
	@SuppressWarnings("unchecked")
	private Document processEntryTypeQueries(Document document, String xpath) {
		if (!StringUtils.isEmpty(xpath)) {
			// select all nodes using xpath with a wild card
			List<Node> allNodes = document.selectNodes(String.format("//%s", xpath.substring(0, xpath.indexOf('['))));

			// remove nodes that we want to keep and then detach the rest
			allNodes.removeAll(document.selectNodes("//" + xpath));
			for (Node node : allNodes) {
				node.detach();
			}
		}
		return document;
	}

	/**
	 * checks if the resolvedids sent by client in the filter have valid
	 * assigning facilities else throws exceptions that is handled and added to
	 * readresults list to become part of cds response xml
	 * 
	 * @param filter
	 * @param readResults
	 * @param filterPersonIdentifiers
	 * @param entryFilter
	 * @param readablePersistenceManagers
	 * @return
	 */
	private List<Document> chkAssigningFacilities(List<Document> readResults,
			List<PersonIdentifierInterface> filterPersonIdentifiers, String requestId, String templateId,
			Collection readablePersistenceManagers, String appName) {
		List<Document> errorResponseList = new ArrayList<Document>();
		List<String> invalidAssigningFacilitiesInFilter = new ArrayList<String>();
		String assigningFacility = null;
		List<String> configuredSites = new ArrayList<String>();
		String configuredSiteId = null;
		ReadPersistenceManagerInterface readPersistenceManager = null;

		for (Object rpm : readablePersistenceManagers) {
			if (rpm instanceof ReadPersistenceManagerInterface) {
				readPersistenceManager = (ReadPersistenceManagerInterface) rpm;
				configuredSiteId = readPersistenceManager.getSiteIdentifier();
			}

			if (configuredSiteId != null) {
				configuredSites.add(configuredSiteId.trim());
			}
		}

		for (PersonIdentifierInterface resolvedPersonIdentifier : filterPersonIdentifiers) {
			assigningFacility = resolvedPersonIdentifier.getAssigningFacility().trim();
			if (!(validNonVistaSites.contains(assigningFacility)) && !(configuredSites.contains(assigningFacility))) {
				invalidAssigningFacilitiesInFilter.add(assigningFacility);
			}
		}

		if (invalidAssigningFacilitiesInFilter.size() > 0) {
			if (configuredSites.size() > 0) {
				errorResponseList
						.add(ExceptionHandler.handleException(
								new ReadException(ErrorCodeEnum.INVALID_ASSIGNING_FACILITIES_IN_FILTER_XML_REQUEST,
										invalidAssigningFacilitiesInFilter.toString()),
								templateId, requestId, appName));
			} else {
				errorResponseList
						.add(ExceptionHandler.handleException(
								new ReadException(ErrorCodeEnum.ALL_ASSIGNING_FACILITIES_INVALID_IN_FILTER_XML_REQUEST,
										invalidAssigningFacilitiesInFilter.toString()),
								templateId, requestId, appName));
			}
		}

		return errorResponseList;
	}

	@Override
	public List<String> getValidNonVistaSites() {
		return validNonVistaSites;
	}

	@Override
	public void addValidNonVistaSite(String validNonVistaSite) {
		if (null != validNonVistaSite && validNonVistaSite.length() > 0
				&& !validNonVistaSites.contains(validNonVistaSite))
			this.validNonVistaSites.add(validNonVistaSite);
	}

	@Override
	public void removeValidNonVistaSite(String validNonVistaSite) {
		this.validNonVistaSites.remove(validNonVistaSite);
	}

	public void setDomainClinicalRecordOwnerMap(
			Map<String, DomainClinicalRecordElementHelperInterface> domainClinicalRecordOwnerMap) {
		this.domainClinicalRecordOwnerMap = domainClinicalRecordOwnerMap;
	}

	public void setCreatedRecordLimitIndex(int createdRecordLimitIndex) {
		this.createdRecordLimitIndex = createdRecordLimitIndex;
	}

	public void setVistaCreateDomainEntryPointsList(List<String> vistaCreateDomainEntryPointsList) {
		this.vistaCreateTemplateDomainEntryPointsList = vistaCreateDomainEntryPointsList;
	}
	

	public void setCdsVistaWrite(CdsWriteInterface cdsVistaWrite) {
		this.cdsVistaWrite = cdsVistaWrite;
	}

	/*
	 * @Override public List<CensusMasterIngest>
	 * performReadOnCensusMetaData(CensusMasterIngest censusMasterIngest) throws
	 * CensusException { // if bad segment was found, throw PersistenceException
	 * to caller so that caller can register Exception // need to add new
	 * message to ErrorCodeEnum WritePersistenceManagerInterface
	 * writePersistenceManager = persistenceLocator.getWritePersistenceManager(
	 * DomainEntryPoint.CensusSurveyResponse); List<CensusMasterIngest> results
	 * =
	 * writePersistenceManager.performReadOnCensusMetaData(censusMasterIngest);
	 * return results; }
	 * 
	 * 
	 * @Override public String performCUADOnCensusMetaData(Operation operation,
	 * CensusMasterIngest censusMasterIngest) throws PersistenceException {
	 * WritePersistenceManagerInterface writePersistenceManager =
	 * persistenceLocator.getWritePersistenceManager(
	 * DomainEntryPoint.CensusSurveyResponse);
	 * writePersistenceManager.performCUADOnCensusMetaData(operation,
	 * censusMasterIngest); return
	 * String.valueOf(censusMasterIngest.getCurrentSegmentNo());
	 * 
	 * }
	 */

}
