package ca.uhn.fhir.jpa.dao.dstu3;

/*
 * #%L
 * HAPI FHIR JPA Server
 * %%
 * Copyright (C) 2014 - 2017 University Health Network
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import static org.apache.commons.lang3.StringUtils.isNotBlank;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;

import org.hl7.fhir.dstu3.hapi.validation.IValidationSupport.CodeValidationResult;
import org.hl7.fhir.dstu3.hapi.validation.ValidationSupportChain;
import org.hl7.fhir.dstu3.model.CodeSystem;
import org.hl7.fhir.dstu3.model.CodeSystem.CodeSystemContentMode;
import org.hl7.fhir.dstu3.model.CodeSystem.ConceptDefinitionComponent;
import org.hl7.fhir.dstu3.model.CodeableConcept;
import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.springframework.beans.factory.annotation.Autowired;

import ca.uhn.fhir.jpa.dao.IFhirResourceDaoCodeSystem;
import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemVersionDao;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion;
import ca.uhn.fhir.jpa.entity.TermConcept;
import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum;
import ca.uhn.fhir.jpa.term.IHapiTerminologySvc;
import ca.uhn.fhir.jpa.util.LogicUtil;
import ca.uhn.fhir.rest.method.RequestDetails;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;

public class FhirResourceDaoCodeSystemDstu3 extends FhirResourceDaoDstu3<CodeSystem> implements IFhirResourceDaoCodeSystem<CodeSystem, Coding, CodeableConcept> {

	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoCodeSystemDstu3.class);

	@Autowired
	private IHapiTerminologySvc myTerminologySvc;

	@Autowired
	private ValidationSupportChain myValidationSupport;

	@Override
	public List<IIdType> findCodeSystemIdsContainingSystemAndCode(String theCode, String theSystem) {
		List<IIdType> valueSetIds;
		Set<Long> ids = searchForIds(CodeSystem.SP_CODE, new TokenParam(theSystem, theCode));
		valueSetIds = new ArrayList<IIdType>();
		for (Long next : ids) {
			valueSetIds.add(new IdType("CodeSystem", next));
		}
		return valueSetIds;
	}

//	private LookupCodeResult lookup(List<ValueSetExpansionContainsComponent> theContains, String theSystem, String theCode) {
//		for (ValueSetExpansionContainsComponent nextCode : theContains) {
//
//			String system = nextCode.getSystem();
//			String code = nextCode.getCode();
//			if (theSystem.equals(system) && theCode.equals(code)) {
//				LookupCodeResult retVal = new LookupCodeResult();
//				retVal.setSearchedForCode(code);
//				retVal.setSearchedForSystem(system);
//				retVal.setFound(true);
//				if (nextCode.getAbstractElement().getValue() != null) {
//					retVal.setCodeIsAbstract(nextCode.getAbstractElement().booleanValue());
//				}
//				retVal.setCodeDisplay(nextCode.getDisplay());
//				retVal.setCodeSystemVersion(nextCode.getVersion());
//				retVal.setCodeSystemDisplayName("Unknown"); // TODO: implement
//				return retVal;
//			}
//
//		}
//
//		return null;
//	}

	@Override
	public LookupCodeResult lookupCode(IPrimitiveType<String> theCode, IPrimitiveType<String> theSystem, Coding theCoding, RequestDetails theRequestDetails) {
		boolean haveCoding = theCoding != null && isNotBlank(theCoding.getSystem()) && isNotBlank(theCoding.getCode());
		boolean haveCode = theCode != null && theCode.isEmpty() == false;
		boolean haveSystem = theSystem != null && theSystem.isEmpty() == false;

		if (!haveCoding && !(haveSystem && haveCode)) {
			throw new InvalidRequestException("No code, coding, or codeableConcept provided to validate");
		}
		if (!LogicUtil.multiXor(haveCoding, (haveSystem && haveCode)) || (haveSystem != haveCode)) {
			throw new InvalidRequestException("$lookup can only validate (system AND code) OR (coding.system AND coding.code)");
		}

		String code;
		String system;
		if (haveCoding) {
			code = theCoding.getCode();
			system = theCoding.getSystem();
		} else {
			code = theCode.getValue();
			system = theSystem.getValue();
		}

		ourLog.info("Looking up {} / {}", system, code);
		
		if (myValidationSupport.isCodeSystemSupported(getContext(), system)) {

			ourLog.info("Code system {} is supported", system);

			CodeValidationResult result = myValidationSupport.validateCode(getContext(), system, code, null);
			if (result != null) {
				if (result.isOk()) {
					LookupCodeResult retVal = new LookupCodeResult();
					retVal.setFound(true);
					retVal.setSearchedForCode(code);
					retVal.setSearchedForSystem(system);
					retVal.setCodeDisplay(result.asConceptDefinition().getDisplay());
					retVal.setCodeSystemDisplayName("Unknown");
					retVal.setCodeSystemVersion("");
					return retVal;
				}
			}
			
//			HapiWorkerContext ctx = new HapiWorkerContext(getContext(), myValidationSupport);
//			ValueSetExpander expander = ctx.getExpander();
//			ValueSet source = new ValueSet();
//			source.getCompose().addInclude().setSystem(system).addConcept().setCode(code);
//
//			ValueSetExpansionOutcome expansion;
//			try {
//				expansion = expander.expand(source);
//			} catch (Exception e) {
//				throw new InternalErrorException(e);
//			}
//
//			if (expansion.getValueset() != null) {
//				List<ValueSetExpansionContainsComponent> contains = expansion.getValueset().getExpansion().getContains();
//				LookupCodeResult result = lookup(contains, system, code);
//				if (result != null) {
//					return result;
//				}
//			}

		}

		// We didn't find it..
		LookupCodeResult retVal = new LookupCodeResult();
		retVal.setFound(false);
		retVal.setSearchedForCode(code);
		retVal.setSearchedForSystem(system);
		return retVal;

	}

	private List<TermConcept> toPersistedConcepts(List<ConceptDefinitionComponent> theConcept, TermCodeSystemVersion theCodeSystemVersion) {
		ArrayList<TermConcept> retVal = new ArrayList<TermConcept>();

		for (ConceptDefinitionComponent next : theConcept) {
			if (isNotBlank(next.getCode())) {
				TermConcept termConcept = new TermConcept();
				termConcept.setCode(next.getCode());
				termConcept.setCodeSystem(theCodeSystemVersion);
				termConcept.setDisplay(next.getDisplay());
				termConcept.addChildren(toPersistedConcepts(next.getConcept(), theCodeSystemVersion), RelationshipTypeEnum.ISA);
				retVal.add(termConcept);
			}
		}

		return retVal;
	}

	@Autowired
	private ITermCodeSystemVersionDao myCsvDao;
	
	@Override
	protected ResourceTable updateEntity(IBaseResource theResource, ResourceTable theEntity, Date theDeletedTimestampOrNull, boolean thePerformIndexing,
			boolean theUpdateVersion, Date theUpdateTime) {
		ResourceTable retVal = super.updateEntity(theResource, theEntity, theDeletedTimestampOrNull, thePerformIndexing, theUpdateVersion, theUpdateTime);

		CodeSystem cs = (CodeSystem) theResource;

		if (cs != null && isNotBlank(cs.getUrl())) {
			String codeSystemUrl = cs.getUrl();
			if (cs.getContent() == CodeSystemContentMode.COMPLETE || cs.getContent() == null) {
				ourLog.info("CodeSystem {} has a status of {}, going to store concepts in terminology tables", retVal.getIdDt().getValue(), cs.getContentElement().getValueAsString());
				
				Long codeSystemResourcePid = retVal.getId();
				TermCodeSystemVersion persCs = myCsvDao.findByCodeSystemResourceAndVersion(codeSystemResourcePid, retVal.getVersion());
				if (persCs != null) {
					ourLog.info("Code system version already exists in database");
				} else {
					
					persCs = new TermCodeSystemVersion();
					persCs.setResource(retVal);
					persCs.setResourceVersionId(retVal.getVersion());
					persCs.getConcepts().addAll(toPersistedConcepts(cs.getConcept(), persCs));
					ourLog.info("Code system has {} concepts", persCs.getConcepts().size());
					myTerminologySvc.storeNewCodeSystemVersion(codeSystemResourcePid, codeSystemUrl, persCs);
					
				}

			}
		}

		return retVal;
	}

}
