package uk.nhs.careconnect.ri.stu3.dao;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.UriParam;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;

import org.hl7.fhir.dstu3.model.Identifier;
import org.hl7.fhir.dstu3.model.OperationOutcome;
import org.hl7.fhir.dstu3.model.StructureDefinition;
import org.hl7.fhir.dstu3.model.IdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import uk.nhs.careconnect.fhir.OperationOutcomeException;
import uk.nhs.careconnect.ri.stu3.dao.transforms.StructureDefinitionEntityToFHIRStructureDefinitionTransformer;
import uk.nhs.careconnect.ri.database.daointerface.StructureDefinitionRepository;
import uk.nhs.careconnect.ri.database.entity.structureDefinition.StructureDefinitionEntity;
import uk.nhs.careconnect.ri.database.entity.structureDefinition.StructureDefinitionIdentifier;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

@Repository
@Transactional
public class StructureDefinitionDao implements StructureDefinitionRepository{

	 @PersistenceContext
	    EntityManager em;


	@Autowired
	private LibDao libDao;
 
	 @Autowired
	 private StructureDefinitionEntityToFHIRStructureDefinitionTransformer structureDefinitionEntityToFHIRStructureDefinitionTransformer;

	//StructureDefinition structureDefinition;


	private static final Logger log = LoggerFactory.getLogger(StructureDefinitionDao.class);

	@Override
	public Long count() {
		CriteriaBuilder qb = em.getCriteriaBuilder();
		CriteriaQuery<Long> cq = qb.createQuery(Long.class);
		cq.select(qb.count(cq.from(StructureDefinitionEntity.class)));
		//cq.where(/*your stuff*/);
		return em.createQuery(cq).getSingleResult();
	}

	@Override
	public StructureDefinitionEntity readEntity(FhirContext ctx, IdType theId) {
		return null;
	}


	@Override
	public OperationOutcome delete(FhirContext ctx, IdType theId) {
		log.trace("Retrieving ValueSet = " + theId.getValue());

		StructureDefinitionEntity structureDefinitionEntity = findStructureDefinitionEntity(theId);

		if (structureDefinitionEntity == null) return null;

		for (StructureDefinitionIdentifier identifier : structureDefinitionEntity.getIdentifiers()) {
			em.remove(identifier);
		}
		em.remove(structureDefinitionEntity);
		return new OperationOutcome();

	}

	@Override
	public void save(FhirContext ctx, StructureDefinitionEntity resource) throws OperationOutcomeException {

	}



	    @Override
	    public StructureDefinition create(FhirContext ctx, StructureDefinition structureDefinition) throws OperationOutcomeException {

			log.info("Structured Definition Create");
	    	System.out.println("call came to save StructureDefinition : " + structureDefinition.getUrlElement().getValue() );
	       // this.structureDefinition = structureDefinition;
	        StructureDefinitionEntity structureDefinitionEntity = null;

	        System.out.println("id is" + structureDefinition.getIdElement());
	        long newStructureDefinitionId = -1;
	        if (structureDefinition.hasId()) {
	            structureDefinitionEntity = findStructureDefinitionEntity(structureDefinition.getIdElement());
	        }

			if (structureDefinition.hasUrl()) {
				List<StructureDefinitionEntity> entries = searchEntity(ctx, null, null, new UriParam().setValue(structureDefinition.getUrl()));
				for (StructureDefinitionEntity nameSys : entries) {
					if (structureDefinition.getId() == null) {
						throw new ResourceVersionConflictException("Url "+structureDefinition.getUrl()+ " is already present on the system "+ nameSys.getId());
					}

					if (!nameSys.getId().equals(structureDefinitionEntity.getId())) {
						throw new ResourceVersionConflictException("Url "+structureDefinition.getUrl()+ " is already present on the system "+ nameSys.getId());
					}
				}
			}

	        

			if (structureDefinitionEntity == null)
			{
				structureDefinitionEntity = new StructureDefinitionEntity();
			}
			structureDefinitionEntity.setResource(null);
			// Removed Id
			if (structureDefinition.hasUrl())
			{
				structureDefinitionEntity.setUrl(structureDefinition.getUrl());
			}
			if (structureDefinition.hasName())
			{
				structureDefinitionEntity.setName(structureDefinition.getName());
			}
			if (structureDefinition.hasStatus())
			{
				structureDefinitionEntity.setStatus(structureDefinition.getStatus());
			}
			if (structureDefinition.hasDescription())
			{
				structureDefinitionEntity.setDescription(structureDefinition.getDescription());
			}

			if (structureDefinition.hasPublisher())
			{
				structureDefinitionEntity.setPublisher(structureDefinition.getPublisher());
			}
			if (structureDefinition.hasCopyright())
			{
				structureDefinitionEntity.setCopyright(structureDefinition.getCopyright());
			}
			if (structureDefinition.hasVersion())
			{
				structureDefinitionEntity.setVersion(structureDefinition.getVersion());
			}

			structureDefinitionEntity.setResource(ctx.newJsonParser().encodeResourceToString(structureDefinition));
			        
			log.trace("Call em.persist StructureDefinitionEntity");
			em.persist(structureDefinitionEntity); // persisting Concept Maps structureDefinition

			log.info("Called PERSIST id="+structureDefinitionEntity.getId().toString());

			if (structureDefinition.hasIdentifier()) {
				for (StructureDefinitionIdentifier identifier : structureDefinitionEntity.getIdentifiers()) {
					em.remove(identifier);
				}
				for (Identifier identifier : structureDefinition.getIdentifier()) {
					StructureDefinitionIdentifier structureDefinitionIdentifier = new StructureDefinitionIdentifier();
					structureDefinitionIdentifier.setStructuredDefinition(structureDefinitionEntity);
					structureDefinitionIdentifier = (StructureDefinitionIdentifier) libDao.setIdentifier(identifier, structureDefinitionIdentifier );
					em.persist(structureDefinitionIdentifier);
				}
			}

			structureDefinition.setId(structureDefinitionEntity.getId().toString());


			return structureDefinitionEntityToFHIRStructureDefinitionTransformer.transform(structureDefinitionEntity,ctx);
	    }   
	    

	    
	    private StructureDefinitionEntity findStructureDefinitionEntity(IdType theId) {

	    	System.out.println("the id is " + theId.getIdPart());
	    	
	    	StructureDefinitionEntity structureDefinitionEntity = null;
	        // Only look up if the id is numeric else need to do a search
	/*        if (daoutils.isNumeric(theId.getIdPart())) {
	            structureDefinitionEntity =(StructureDefinitionEntity) em.find(StructureDefinitionEntity.class, theId.getIdPart());
	        } */
	        StructureDefinitionEntity.class.getName();
	        // if null try a search on strId
	        
	            CriteriaBuilder builder =  em.getCriteriaBuilder();

			if (daoutils.isNumeric(theId.getIdPart())) {

				CriteriaQuery<StructureDefinitionEntity> criteria = builder.createQuery(StructureDefinitionEntity.class);
				Root<StructureDefinitionEntity> root = criteria.from(StructureDefinitionEntity.class);
				List<Predicate> predList = new LinkedList<Predicate>();
				Predicate p = builder.equal(root.<String>get("id"), theId.getIdPart());
				predList.add(p);
				Predicate[] predArray = new Predicate[predList.size()];
				predList.toArray(predArray);
				if (predList.size() > 0) {
					criteria.select(root).where(predArray);

					List<StructureDefinitionEntity> qryResults = em.createQuery(criteria).getResultList();

					for (StructureDefinitionEntity cme : qryResults) {
						structureDefinitionEntity = cme;
						break;
					}
				}
			}
	       // }
	        return structureDefinitionEntity;
	    }
	    
	    public StructureDefinition read(FhirContext ctx, IdType theId) {

	        log.trace("Retrieving ValueSet = " + theId.getValue());

	        StructureDefinitionEntity structureDefinitionEntity = findStructureDefinitionEntity(theId);

	        if (structureDefinitionEntity == null) return null;

	        StructureDefinition structureDefinition = structureDefinitionEntityToFHIRStructureDefinitionTransformer.transform(structureDefinitionEntity, ctx);

	        if (structureDefinitionEntity.getResource() == null) {
	            String resource = ctx.newJsonParser().encodeResourceToString(structureDefinition);
	            if (resource.length() < 10000) {
	            	structureDefinitionEntity.setResource(resource);
	                em.persist(structureDefinitionEntity);
	            }
	        }
	        return structureDefinition;
	      

	    }

	public List<StructureDefinition> search (FhirContext ctx,
										  @OptionalParam(name = StructureDefinition.SP_NAME) StringParam name,
										  @OptionalParam(name = StructureDefinition.SP_PUBLISHER) StringParam publisher,
										  @OptionalParam(name = StructureDefinition.SP_URL) UriParam url
	) {
		List<StructureDefinitionEntity> qryResults = searchEntity(ctx,name,publisher, url);
		List<StructureDefinition> results = new ArrayList<>();

		for (StructureDefinitionEntity structureDefinitionEntity : qryResults)
		{

			StructureDefinition structureDefinition = structureDefinitionEntityToFHIRStructureDefinitionTransformer.transform(structureDefinitionEntity, ctx);

			results.add(structureDefinition);

		}
		return results;
	}

	    public List<StructureDefinitionEntity> searchEntity (FhirContext ctx,
	            @OptionalParam(name = StructureDefinition.SP_NAME) StringParam name,
	            @OptionalParam(name = StructureDefinition.SP_PUBLISHER) StringParam publisher,
	            @OptionalParam(name = StructureDefinition.SP_URL) UriParam url
	    )
	    {
	        List<StructureDefinitionEntity> qryResults = null;

	        CriteriaBuilder builder = em.getCriteriaBuilder();

	        CriteriaQuery<StructureDefinitionEntity> criteria = builder.createQuery(StructureDefinitionEntity.class);
	        Root<StructureDefinitionEntity> root = criteria.from(StructureDefinitionEntity.class);

	        List<Predicate> predList = new LinkedList<>();


	        if (name !=null)
	        {

	            Predicate p =
	                    builder.like(
	                            builder.upper(root.get("name").as(String.class)),
	                            builder.upper(builder.literal("%" + name.getValue() + "%"))
	                    );

	            predList.add(p);
	        }
	        if (publisher !=null)
	        {

	            Predicate p =
	                    builder.like(
	                            builder.upper(root.get("publisher").as(String.class)),
	                            builder.upper(builder.literal( publisher.getValue() + "%"))
	                    );

	            predList.add(p);
	        }
	        if (url !=null)
	        {

	            Predicate p =
	                    builder.like(
	                            builder.upper(root.get("url").as(String.class)),
	                            builder.upper(builder.literal( url.getValue()))
	                    );

	            predList.add(p);
	        }
	       

	        Predicate[] predArray = new Predicate[predList.size()];
	        predList.toArray(predArray);
	        if (predList.size()>0)
	        {
	            criteria.select(root).where(predArray);
	        }
	        else
	        {
	            criteria.select(root);
	        }

	        qryResults = em.createQuery(criteria).setMaxResults(100).getResultList();

	       return qryResults;
	    }
	    
}