package org.brapi.test.BrAPITestServer.service.geno;

import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerDbIdNotFoundException;
import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException;
import org.brapi.test.BrAPITestServer.model.dto.geno.VariantWriteRequest;
import org.brapi.test.BrAPITestServer.model.entity.geno.CallEntity;
import org.brapi.test.BrAPITestServer.model.entity.geno.MarkerPositionEntity;
import org.brapi.test.BrAPITestServer.model.entity.geno.ReferenceSetEntity;
import org.brapi.test.BrAPITestServer.model.entity.geno.VariantEntity;
import org.brapi.test.BrAPITestServer.model.entity.geno.VariantSetEntity;
import org.brapi.test.BrAPITestServer.repository.geno.CallRepository;
import org.brapi.test.BrAPITestServer.repository.geno.MarkerPositionRepository;
import org.brapi.test.BrAPITestServer.repository.geno.VariantRepository;
import org.brapi.test.BrAPITestServer.repository.geno.VariantSetRepository;
import org.brapi.test.BrAPITestServer.service.DateUtility;
import org.brapi.test.BrAPITestServer.service.PagingUtility;
import org.brapi.test.BrAPITestServer.service.SearchQueryBuilder;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

import io.swagger.model.Metadata;
import io.swagger.model.geno.Variant;
import io.swagger.model.geno.VariantsSearchRequest;

@Service
public class VariantService {

	private final VariantRepository variantRepository;
	private final CallRepository callRepository;
	private final MarkerPositionRepository markerPositionRepository;
	private final VariantSetRepository variantSetRepository;
	private final ReferenceSetService referenceSetService;

	public VariantService(VariantRepository variantRepository, CallRepository callRepository,
			MarkerPositionRepository markerPositionRepository, VariantSetRepository variantSetRepository,
			ReferenceSetService referenceSetService) {
		this.variantRepository = variantRepository;
		this.callRepository = callRepository;
		this.markerPositionRepository = markerPositionRepository;
		this.variantSetRepository = variantSetRepository;
		this.referenceSetService = referenceSetService;
	}

	public List<Variant> findVariants(String variantDbId, String variantSetDbId, String referenceDbId,
			String referenceSetDbId, String externalReferenceId, String externalReferenceSource, Metadata metadata) {

		VariantsSearchRequest request = new VariantsSearchRequest();
		if (variantSetDbId != null)
			request.addVariantSetDbIdsItem(variantSetDbId);
		if (variantDbId != null)
			request.addVariantDbIdsItem(variantDbId);
		if (referenceDbId != null)
			request.addReferenceDbIdsItem(referenceDbId);
		if (referenceSetDbId != null)
			request.addReferenceSetDbIdsItem(referenceSetDbId);

		request.addExternalReferenceItem(externalReferenceId, null, externalReferenceSource);

		return findVariants(request, metadata);
	}

	public List<Variant> findVariants(VariantsSearchRequest request, Metadata metadata) {
		return findVariantEntities(request, metadata).stream().map(this::convertFromEntity)
				.collect(Collectors.toList());
	}

	public List<VariantEntity> findVariantEntities(VariantsSearchRequest request, Metadata metadata) {
		Pageable pageReq = PagingUtility.getPageRequest(metadata);
		SearchQueryBuilder<VariantEntity> searchQuery = new SearchQueryBuilder<VariantEntity>(VariantEntity.class);
		if (request.getCallSetDbIds() != null && !request.getCallSetDbIds().isEmpty()) {
			searchQuery = searchQuery.join("variantSet.callSets", "callSet").appendList(request.getCallSetDbIds(),
					"*callSet.id");
		}
		searchQuery = searchQuery.appendList(request.getVariantSetDbIds(), "variantSet.id")
				.appendList(request.getVariantDbIds(), "id")
				.appendList(request.getReferenceSetDbIds(), "referenceSet.id");

		Page<VariantEntity> page = variantRepository.findAllBySearch(searchQuery, pageReq);
		PagingUtility.calculateMetaData(metadata, page);
		return page.getContent();
	}

	public Variant getVariant(String variantDbId) throws BrAPIServerException {
		return convertFromEntity(getVariantEntity(variantDbId, HttpStatus.NOT_FOUND));
	}

	public VariantEntity getVariantEntity(String variantDbId) throws BrAPIServerException {
		return getVariantEntity(variantDbId, HttpStatus.BAD_REQUEST);
	}

	public VariantEntity getVariantEntity(String variantDbId, HttpStatus errorStatus) throws BrAPIServerException {
		VariantEntity variant = null;
		Optional<VariantEntity> entityOpt = variantRepository.findById(variantDbId);
		if (entityOpt.isPresent()) {
			variant = entityOpt.get();
		} else {
			throw new BrAPIServerDbIdNotFoundException("variant", variantDbId, errorStatus);
		}
		return variant;
	}

	private Variant convertFromEntity(VariantEntity entity) {
		Variant variant = new Variant();
		variant.setAdditionalInfo(entity.getAdditionalInfoMap());
		variant.setAlternateBases(entity.getAlternateBases());
		variant.setCiend(entity.getCiend());
		variant.setCipos(entity.getCipos());
		variant.setCreated(DateUtility.toOffsetDateTime(entity.getCreated()));
		variant.setEnd(entity.getEnd());
		variant.setFiltersApplied(entity.getFiltersApplied());
		variant.setFiltersFailed(entity.getFiltersFailed());
		variant.setFiltersPassed(entity.getFiltersPassed());
		variant.setReferenceBases(entity.getReferenceBases());
		if (entity.getReferenceSet() != null) {
			variant.setReferenceSetDbId(entity.getReferenceSet().getId());
			variant.setReferenceSetName(entity.getReferenceSet().getReferenceSetName());
			variant.setReferenceName(entity.getReferenceSet().getReferenceSetName());
		}
		variant.setStart(entity.getStart());
		variant.setSvlen(entity.getSvlen());
		variant.setUpdated(DateUtility.toOffsetDateTime(entity.getUpdated()));
		variant.setVariantDbId(entity.getId());
		variant.setVariantNames(Arrays.asList(entity.getVariantName()));
		if (entity.getVariantSet() != null) {
			variant.setVariantSetDbId(Arrays.asList(entity.getVariantSet().getId()));
		}
		variant.setVariantType(entity.getVariantType());

		return variant;
	}

	public VariantEntity save(VariantEntity entity) {
		return variantRepository.save(entity);
	}

	public Variant saveVariant(VariantWriteRequest request) throws BrAPIServerException {
		if (request.getVariantName() == null || request.getVariantName().isBlank()) {
			throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "variantName is required");
		}
		if (request.getVariantDbId() != null && variantRepository.findById(request.getVariantDbId()).isPresent()) {
			throw new BrAPIServerException(HttpStatus.CONFLICT, "Variant already exists: " + request.getVariantDbId());
		}
		VariantEntity entity = new VariantEntity();
		if (request.getVariantDbId() != null && !request.getVariantDbId().isBlank()) {
			entity.setId(request.getVariantDbId().trim());
		}
		entity.setCreated(new Date());
		entity.setUpdated(new Date());
		updateEntity(entity, request);
		return convertFromEntity(variantRepository.save(entity));
	}

	public Variant updateVariant(String variantDbId, VariantWriteRequest request) throws BrAPIServerException {
		VariantEntity entity = getVariantEntity(variantDbId, HttpStatus.NOT_FOUND);
		entity.setUpdated(new Date());
		updateEntity(entity, request);
		return convertFromEntity(variantRepository.save(entity));
	}

	public Variant deleteVariant(String variantDbId) throws BrAPIServerException {
		VariantEntity entity = getVariantEntity(variantDbId, HttpStatus.NOT_FOUND);
		assertNoVariantDependencies(variantDbId);
		Variant deleted = convertFromEntity(entity);
		try {
			variantRepository.delete(entity);
			variantRepository.flush();
		} catch (DataIntegrityViolationException e) {
			throw new BrAPIServerException(HttpStatus.CONFLICT, "Variant is in use and cannot be deleted");
		}
		return deleted;
	}

	private void assertNoVariantDependencies(String variantDbId) throws BrAPIServerException {
		Pageable pageReq = PageRequest.of(0, 1);
		SearchQueryBuilder<CallEntity> callQuery = new SearchQueryBuilder<CallEntity>(CallEntity.class)
				.appendSingle(variantDbId, "variant.id");
		if (callRepository.findAllBySearch(callQuery, pageReq).getTotalElements() > 0) {
			throw new BrAPIServerException(HttpStatus.CONFLICT, "Variant is referenced by allele_call records");
		}
		SearchQueryBuilder<MarkerPositionEntity> markerQuery = new SearchQueryBuilder<MarkerPositionEntity>(
				MarkerPositionEntity.class).appendSingle(variantDbId, "variant.id");
		if (markerPositionRepository.findAllBySearch(markerQuery, pageReq).getTotalElements() > 0) {
			throw new BrAPIServerException(HttpStatus.CONFLICT, "Variant is referenced by marker_position records");
		}
	}

	private void updateEntity(VariantEntity entity, VariantWriteRequest request) throws BrAPIServerException {
		entity.setVariantName(request.getVariantName().trim());
		entity.setVariantType(request.getVariantType());
		entity.setStart(request.getStart());
		entity.setEnd(request.getEnd());
		if (request.getStart() != null && request.getEnd() != null && request.getEnd() < request.getStart()) {
			throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "variant end cannot be less than start");
		}
		entity.setReferenceBases(request.getReferenceBases());
		entity.setSvlen(request.getSvlen());
		entity.setFiltersApplied(request.getFiltersApplied());
		entity.setFiltersPassed(request.getFiltersPassed());
		if (request.getAlternateBases() != null) {
			entity.setAlternateBases(request.getAlternateBases());
		}
		if (request.getCiend() != null) {
			entity.setCiend(request.getCiend());
		}
		if (request.getCipos() != null) {
			entity.setCipos(request.getCipos());
		}
		if (request.getFiltersFailed() != null) {
			entity.setFiltersFailed(request.getFiltersFailed());
		}

		VariantSetEntity variantSet = null;
		if (request.getVariantSetDbId() != null && !request.getVariantSetDbId().isBlank()) {
			variantSet = variantSetRepository.findById(request.getVariantSetDbId())
					.orElseThrow(() -> new BrAPIServerDbIdNotFoundException("variantSet", request.getVariantSetDbId(),
							HttpStatus.BAD_REQUEST));
			entity.setVariantSet(variantSet);
		} else if (request.getVariantSetDbId() != null) {
			entity.setVariantSet(null);
		}

		ReferenceSetEntity referenceSet = null;
		if (request.getReferenceSetDbId() != null && !request.getReferenceSetDbId().isBlank()) {
			referenceSet = referenceSetService.getReferenceSetEntity(request.getReferenceSetDbId());
			entity.setReferenceSet(referenceSet);
		} else if (request.getReferenceSetDbId() != null) {
			entity.setReferenceSet(null);
		} else if (variantSet != null && variantSet.getReferenceSet() != null) {
			entity.setReferenceSet(variantSet.getReferenceSet());
		}

		if (variantSet != null && referenceSet != null
				&& variantSet.getReferenceSet() != null
				&& !variantSet.getReferenceSet().getId().equals(referenceSet.getId())) {
			throw new BrAPIServerException(HttpStatus.BAD_REQUEST,
					"referenceSetDbId must match the reference set of the selected variantSet");
		}
	}

}
