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

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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.VariantSetWriteRequest;
import org.brapi.test.BrAPITestServer.model.entity.core.StudyEntity;
import org.brapi.test.BrAPITestServer.model.entity.geno.CallEntity;
import org.brapi.test.BrAPITestServer.model.entity.geno.ReferenceSetEntity;
import org.brapi.test.BrAPITestServer.model.entity.geno.CallSetEntity;
import org.brapi.test.BrAPITestServer.model.entity.geno.VariantEntity;
import org.brapi.test.BrAPITestServer.model.entity.geno.VariantSetAnalysisEntity;
import org.brapi.test.BrAPITestServer.model.entity.geno.VariantSetAvailableFormatEntity;
import org.brapi.test.BrAPITestServer.model.entity.geno.VariantSetEntity;
import org.brapi.test.BrAPITestServer.repository.geno.CallSetRepository;
import org.brapi.test.BrAPITestServer.repository.geno.VariantRepository;
import org.brapi.test.BrAPITestServer.repository.geno.VariantSetRepository;
import org.brapi.test.BrAPITestServer.service.core.StudyService;
import org.brapi.test.BrAPITestServer.service.DateUtility;
import org.brapi.test.BrAPITestServer.service.PagingUtility;
import org.brapi.test.BrAPITestServer.service.SearchQueryBuilder;
import org.brapi.test.BrAPITestServer.service.UpdateUtility;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

import io.swagger.model.IndexPagination;
import io.swagger.model.Metadata;
import io.swagger.model.geno.Analysis;
import io.swagger.model.geno.CallSetsSearchRequest;
import io.swagger.model.geno.CallsSearchRequest;
import io.swagger.model.geno.DataTypePrimitives;
import io.swagger.model.geno.VariantSet;
import io.swagger.model.geno.VariantSetAvailableFormats;
import io.swagger.model.geno.VariantSetMetadataFields;
import io.swagger.model.geno.VariantSetsSearchRequest;
import io.swagger.model.geno.VariantsSearchRequest;
import io.swagger.model.geno.VariantSetsExtractRequest;

@Service
public class VariantSetService {
	private static final int EXTRACT_COPY_LIMIT = 25;

	private final VariantSetRepository variantSetRepository;
	private final VariantRepository variantRepository;
	private final CallSetRepository callSetRepository;
	private final VariantService variantService;
	private final CallSetService callSetService;
	private final CallService callService;
	private final ReferenceSetService referenceSetService;
	private final StudyService studyService;

	public VariantSetService(VariantSetRepository variantSetRepository, VariantRepository variantRepository,
			CallSetRepository callSetRepository, VariantService variantService, CallSetService callSetService,
			CallService callService, ReferenceSetService referenceSetService, StudyService studyService) {
		this.variantSetRepository = variantSetRepository;
		this.variantRepository = variantRepository;
		this.callSetRepository = callSetRepository;
		this.variantService = variantService;
		this.callSetService = callSetService;
		this.callService = callService;
		this.referenceSetService = referenceSetService;
		this.studyService = studyService;
	}

	public List<VariantSet> findVariantSets(String variantSetDbId, String variantDbId, String callSetDbId,
			String studyDbId, String studyName, String referenceSetDbId, String commonCropName, String programDbId,
			String externalReferenceId, String externalReferenceSource, Metadata metadata) {
		VariantSetsSearchRequest request = new VariantSetsSearchRequest();
		if (variantSetDbId != null)
			request.addVariantSetDbIdsItem(variantSetDbId);
		if (variantDbId != null)
			request.addVariantDbIdsItem(variantDbId);
		if (callSetDbId != null)
			request.addCallSetDbIdsItem(callSetDbId);
		if (studyDbId != null)
			request.addStudyDbIdsItem(studyDbId);
		if (studyName != null)
			request.addStudyNamesItem(studyName);
		if (referenceSetDbId != null)
			request.addReferenceSetDbIdsItem(referenceSetDbId);
		if (commonCropName != null)
			request.addCommonCropNamesItem(commonCropName);
		if (programDbId != null)
			request.addProgramDbIdsItem(programDbId);

		request.addExternalReferenceItem(externalReferenceId, null, externalReferenceSource);
		return findVariantSets(request, metadata);
	}

	public List<VariantSet> findVariantSets(VariantSetsSearchRequest request, Metadata metadata) {
		return convertFromEntities(findVariantSetEntities(request, metadata));
	}

	private List<VariantSetEntity> findVariantSetEntities(VariantSetsSearchRequest request, Metadata metadata) {
		Pageable pageReq = PagingUtility.getPageRequest(metadata);
		SearchQueryBuilder<VariantSetEntity> searchQuery = new SearchQueryBuilder<VariantSetEntity>(
				VariantSetEntity.class);
		if (request.getCallSetDbIds() != null) {
			searchQuery.join("callSets", "callSet").appendList(request.getCallSetDbIds(), "*callSet.id");
		}
		if (request.getVariantDbIds() != null) {
			searchQuery.join("variants", "variant").appendList(request.getVariantDbIds(), "*variant.id");
		}
		searchQuery.appendList(request.getStudyDbIds(), "study.id")
				.appendList(request.getStudyNames(), "study.studyName")
				.appendList(request.getReferenceSetDbIds(), "referenceSet.id")
				.appendList(request.getVariantSetDbIds(), "id");

		Page<VariantSetEntity> page;
		if (isUnfiltered(request)) {
			page = variantSetRepository.findAllPrioritizePopulated(pageReq);
		} else {
			page = variantSetRepository.findAllBySearch(searchQuery, pageReq);
		}
		PagingUtility.calculateMetaData(metadata, page);
		return page.getContent();
	}

	private boolean isUnfiltered(VariantSetsSearchRequest request) {
		return request.getCallSetDbIds() == null && request.getVariantDbIds() == null
				&& request.getStudyDbIds() == null && request.getStudyNames() == null
				&& request.getReferenceSetDbIds() == null && request.getVariantSetDbIds() == null
				&& request.getCommonCropNames() == null && request.getProgramDbIds() == null
				&& request.getExternalReferenceIDs() == null && request.getExternalReferenceSources() == null;
	}

	public VariantSet getVariantSet(String variantSetDbId) throws BrAPIServerException {
		return convertFromEntity(getVariantSetEntity(variantSetDbId, HttpStatus.NOT_FOUND));
	}

	public VariantSet saveVariantSet(VariantSetWriteRequest request) throws BrAPIServerException {
		if (request.getVariantSetName() == null || request.getVariantSetName().isBlank()) {
			throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "variantSetName is required");
		}
		if (request.getVariantSetDbId() != null
				&& variantSetRepository.findById(request.getVariantSetDbId()).isPresent()) {
			throw new BrAPIServerException(HttpStatus.CONFLICT,
					"VariantSet already exists: " + request.getVariantSetDbId());
		}
		VariantSetEntity entity = new VariantSetEntity();
		if (request.getVariantSetDbId() != null && !request.getVariantSetDbId().isBlank()) {
			entity.setId(request.getVariantSetDbId().trim());
		}
		updateEntity(entity, request);
		return convertFromEntity(variantSetRepository.save(entity));
	}

	public VariantSet updateVariantSet(String variantSetDbId, VariantSetWriteRequest request)
			throws BrAPIServerException {
		VariantSetEntity entity = getVariantSetEntity(variantSetDbId, HttpStatus.NOT_FOUND);
		updateEntity(entity, request);
		return convertFromEntity(variantSetRepository.save(entity));
	}

	public VariantSet deleteVariantSet(String variantSetDbId) throws BrAPIServerException {
		VariantSetEntity entity = getVariantSetEntity(variantSetDbId, HttpStatus.NOT_FOUND);
		assertNoVariantSetDependencies(entity);
		VariantSet deleted = convertFromEntity(entity);
		try {
			variantSetRepository.delete(entity);
			variantSetRepository.flush();
		} catch (DataIntegrityViolationException e) {
			throw new BrAPIServerException(HttpStatus.CONFLICT, "VariantSet is in use and cannot be deleted");
		}
		return deleted;
	}

	private void assertNoVariantSetDependencies(VariantSetEntity entity) throws BrAPIServerException {
		if (entity.getVariants() != null && !entity.getVariants().isEmpty()) {
			throw new BrAPIServerException(HttpStatus.CONFLICT, "VariantSet is referenced by Variant records");
		}
		if (entity.getCallSets() != null && !entity.getCallSets().isEmpty()) {
			throw new BrAPIServerException(HttpStatus.CONFLICT, "VariantSet is referenced by CallSet records");
		}
		if (entity.getAnalysis() != null && !entity.getAnalysis().isEmpty()) {
			throw new BrAPIServerException(HttpStatus.CONFLICT, "VariantSet is referenced by analysis records");
		}
		if (entity.getAvailableFormats() != null && !entity.getAvailableFormats().isEmpty()) {
			throw new BrAPIServerException(HttpStatus.CONFLICT, "VariantSet is referenced by available format records");
		}
	}

	private void updateEntity(VariantSetEntity entity, VariantSetWriteRequest request) throws BrAPIServerException {
		entity.setVariantSetName(request.getVariantSetName().trim());
		if (request.getReferenceSetDbId() != null && !request.getReferenceSetDbId().isBlank()) {
			ReferenceSetEntity referenceSet = referenceSetService.getReferenceSetEntity(request.getReferenceSetDbId());
			entity.setReferenceSet(referenceSet);
		} else if (request.getReferenceSetDbId() != null) {
			entity.setReferenceSet(null);
		}
		if (request.getStudyDbId() != null && !request.getStudyDbId().isBlank()) {
			StudyEntity study = studyService.getStudyEntity(request.getStudyDbId());
			entity.setStudy(study);
		} else if (request.getStudyDbId() != null) {
			entity.setStudy(null);
		}
	}

	public VariantSetEntity getVariantSetEntity(String variantSetDbId) throws BrAPIServerException {
		return getVariantSetEntity(variantSetDbId, HttpStatus.BAD_REQUEST);
	}

	public VariantSetEntity getVariantSetEntity(String variantSetDbId, HttpStatus errorStatus)
			throws BrAPIServerException {
		VariantSetEntity variantSet = null;
		Optional<VariantSetEntity> entityOpt = variantSetRepository.findById(variantSetDbId);
		if (entityOpt.isPresent()) {
			variantSet = entityOpt.get();
		} else {
			throw new BrAPIServerDbIdNotFoundException("variantSet", variantSetDbId, errorStatus);
		}
		return variantSet;
	}

	private List<VariantSet> convertFromEntities(List<VariantSetEntity> entities) {
		if (entities.isEmpty()) {
			return List.of();
		}
		List<String> variantSetDbIds = entities.stream().map(VariantSetEntity::getId).collect(Collectors.toList());
		Map<String, Long> variantCounts = toCountMap(variantRepository.countVariantsGroupedByVariantSetId(variantSetDbIds));
		Map<String, Long> callSetCounts = toCountMap(callSetRepository.countCallSetsGroupedByVariantSetId(variantSetDbIds));
		return entities.stream()
				.map(entity -> convertFromEntity(entity,
						variantCounts.getOrDefault(entity.getId(), 0L),
						callSetCounts.getOrDefault(entity.getId(), 0L)))
				.collect(Collectors.toList());
	}

	private Map<String, Long> toCountMap(List<Object[]> rows) {
		Map<String, Long> counts = new HashMap<>();
		for (Object[] row : rows) {
			counts.put((String) row[0], ((Number) row[1]).longValue());
		}
		return counts;
	}

	private VariantSet convertFromEntity(VariantSetEntity entity) {
		return convertFromEntity(entity, variantRepository.countByVariantSet_Id(entity.getId()),
				callSetRepository.countByVariantSetDbId(entity.getId()));
	}

	private VariantSet convertFromEntity(VariantSetEntity entity, long variantCount, long callSetCount) {
		VariantSet variantSet = new VariantSet();
		UpdateUtility.convertFromEntity(entity, variantSet);
		if (entity.getAnalysis() != null)
			variantSet.setAnalysis(
					entity.getAnalysis().stream().map(this::convertFromEntity).collect(Collectors.toList()));
		if (entity.getAvailableFormats() != null)
			variantSet.setAvailableFormats(
					entity.getAvailableFormats().stream().map(this::convertFromEntity).collect(Collectors.toList()));
		variantSet.setCallSetCount(toCountInteger(callSetCount));
		if (entity.getReferenceSet() != null)
			variantSet.setReferenceSetDbId(entity.getReferenceSet().getId());
		if (entity.getStudy() != null)
			variantSet.setStudyDbId(entity.getStudy().getId());
		variantSet.setVariantCount(toCountInteger(variantCount));
		variantSet.setVariantSetDbId(entity.getId());
		variantSet.setVariantSetName(entity.getVariantSetName());

		VariantSetMetadataFields metaDataFieldGT = new VariantSetMetadataFields();
		metaDataFieldGT.setDataType(DataTypePrimitives.STRING);
		metaDataFieldGT.setFieldAbbreviation("GT");
		metaDataFieldGT.setFieldName("Genotype");
		variantSet.addMetadataFieldsItem(metaDataFieldGT);

		VariantSetMetadataFields metaDataFieldRD = new VariantSetMetadataFields();
		metaDataFieldRD.setDataType(DataTypePrimitives.INTEGER);
		metaDataFieldRD.setFieldAbbreviation("RD");
		metaDataFieldRD.setFieldName("Read Depth");
		variantSet.addMetadataFieldsItem(metaDataFieldRD);

		VariantSetMetadataFields metaDataFieldGL = new VariantSetMetadataFields();
		metaDataFieldGL.setDataType(DataTypePrimitives.FLOAT);
		metaDataFieldGL.setFieldAbbreviation("GL");
		metaDataFieldGL.setFieldName("Genotype Likelihood");
		variantSet.addMetadataFieldsItem(metaDataFieldGL);

		return variantSet;
	}

	private Integer toCountInteger(long count) {
		return count > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) count;
	}

	private Analysis convertFromEntity(VariantSetAnalysisEntity entity) {
		Analysis analysis = new Analysis();
		analysis.setAnalysisDbId(entity.getId());
		analysis.setAnalysisName(entity.getAnalysisName());
		analysis.setCreated(DateUtility.toOffsetDateTime(entity.getCreated()));
		analysis.setDescription(entity.getDescription());
		analysis.setSoftware(entity.getSoftware());
		analysis.setType(entity.getType());
		analysis.setUpdated(DateUtility.toOffsetDateTime(entity.getUpdated()));

		return analysis;
	}

	private VariantSetAvailableFormats convertFromEntity(VariantSetAvailableFormatEntity entity) {
		VariantSetAvailableFormats format = new VariantSetAvailableFormats();
		format.setDataFormat(entity.getDataFormat());
		format.setFileFormat(entity.getFileFormat());
		format.setFileURL(entity.getFileURL());
		format.setExpandHomozygotes(entity.getExpandHomozygotes());
		format.setSepPhased(entity.getSepPhased());
		format.setSepUnphased(entity.getSepUnphased());
		format.setUnknownString(entity.getUnknownString());

		return format;
	}

	public VariantSet extractVariantSet(VariantSetsExtractRequest request) throws BrAPIServerException {
		if (request == null) {
			request = new VariantSetsExtractRequest();
		}
		if (!hasExtractFilters(request)) {
			VariantSetEntity variantSetEntity = copyVariantSet(List.of());
			variantSetEntity = variantSetRepository.save(variantSetEntity);
			return convertFromEntity(variantSetEntity);
		}
		CallsSearchRequest callsRequest = new CallsSearchRequest();
		CallSetsSearchRequest callSetRequest = new CallSetsSearchRequest();
		VariantsSearchRequest variantRequest = new VariantsSearchRequest();
		VariantSetsSearchRequest variantSetsRequest = new VariantSetsSearchRequest();
		if (request.getCallSetDbIds() != null) {
			callsRequest.setCallSetDbIds(request.getCallSetDbIds());
			callSetRequest.setCallSetDbIds(request.getCallSetDbIds());
			variantSetsRequest.setCallSetDbIds(request.getCallSetDbIds());
		}
		if (request.getVariantDbIds() != null) {
			callsRequest.setVariantDbIds(request.getVariantDbIds());
			variantRequest.setVariantDbIds(request.getVariantDbIds());
			variantSetsRequest.setVariantDbIds(request.getVariantDbIds());
		}
		if (request.getVariantSetDbIds() != null) {
			callsRequest.setVariantSetDbIds(request.getVariantSetDbIds());
			callSetRequest.setVariantSetDbIds(request.getVariantSetDbIds());
			variantRequest.setVariantSetDbIds(request.getVariantSetDbIds());
			variantSetsRequest.setVariantSetDbIds(request.getVariantSetDbIds());
		}

		Metadata extractionMetadata = extractionMetadata();
		List<VariantSetEntity> variantSets = findVariantSetEntities(variantSetsRequest, extractionMetadata);
		List<VariantEntity> variants = variantService.findVariantEntities(variantRequest, extractionMetadata());
		List<CallSetEntity> callSets = callSetService.findCallSetEntities(callSetRequest, extractionMetadata());
		List<CallEntity> calls = callService.findCallEntities(callsRequest, extractionMetadata());

		VariantSetEntity variantSetEntity = copyVariantSet(variantSets);
		Map<String, VariantEntity> newVariantMap = copyVariants(variantSetEntity, variants);
		Map<String, CallSetEntity> newCallSetMap = copyCallSets(variantSetEntity, callSets);
		copyCalls(newVariantMap, newCallSetMap, calls);

		variantSetEntity = variantSetRepository.save(variantSetEntity);
		return convertFromEntity(variantSetEntity);
	}

	private boolean hasExtractFilters(VariantSetsExtractRequest request) {
		return hasValues(request.getCallSetDbIds()) || hasValues(request.getVariantDbIds())
				|| hasValues(request.getVariantSetDbIds());
	}

	private boolean hasValues(List<String> values) {
		return values != null && values.stream().anyMatch(value -> value != null && !value.isBlank());
	}

	private Metadata extractionMetadata() {
		Metadata metadata = new Metadata();
		IndexPagination pagination = new IndexPagination();
		pagination.setCurrentPage(0);
		pagination.setPageSize(EXTRACT_COPY_LIMIT);
		metadata.setPagination(pagination);
		return metadata;
	}

	private VariantSetEntity copyVariantSet(List<VariantSetEntity> variantSets) throws BrAPIServerException {
		VariantSetEntity entity = new VariantSetEntity();
		;
		if (variantSets.size() == 1) {
			entity.setVariantSetName(variantSets.get(0).getVariantSetName() + "-Copy");
		} else if (variantSets.size() > 1) {
			entity.setVariantSetName(variantSets.get(0).getVariantSetName() + "-AndOthers");
		} else {
			entity.setVariantSetName("Extracted VariantSet");
		}
		entity.setId(null);
		entity.setCallSets(new ArrayList<>());
		entity.setVariants(new ArrayList<>());

		entity = variantSetRepository.save(entity);
		return entity;
	}

	private Map<String, VariantEntity> copyVariants(VariantSetEntity variantSetEntity, List<VariantEntity> variants) {
		Map<String, VariantEntity> newVariantMap = new HashMap<>();
		for (VariantEntity variant : variants) {
			VariantEntity entity = new VariantEntity(variant);
			String oldId = entity.getId();
			entity.setId(null);
			entity.setVariantSet(variantSetEntity);
			VariantEntity saved = variantService.save(entity);
			newVariantMap.put(oldId, saved);
			variantSetEntity.getVariants().add(saved);
		}
		return newVariantMap;
	}

	private Map<String, CallSetEntity> copyCallSets(VariantSetEntity variantSetEntity, List<CallSetEntity> callSets) {
		Map<String, CallSetEntity> newCallSetMap = new HashMap<>();
		for (CallSetEntity callSet : callSets) {
			CallSetEntity entity = new CallSetEntity(callSet);
			String oldId = entity.getId();
			entity.setId(null);
			entity.setVariantSets(new ArrayList<>());
			entity.getVariantSets().add(variantSetEntity);
			CallSetEntity saved = callSetService.save(entity);
			newCallSetMap.put(oldId, saved);
			variantSetEntity.getCallSets().add(saved);
		}
		return newCallSetMap;
	}

	private void copyCalls(Map<String, VariantEntity> newVariantMap, Map<String, CallSetEntity> newCallSetMap,
			List<CallEntity> calls) {
		List<CallEntity> newCalls = new ArrayList<>();
		for (CallEntity call : calls) {
			CallEntity entity = new CallEntity(call);
			entity.setId(null);
			if (entity.getCallSet() == null || entity.getVariant() == null) {
				continue;
			}
			entity.setCallSet(newCallSetMap.get(entity.getCallSet().getId()));
			entity.setVariant(newVariantMap.get(entity.getVariant().getId()));
			if (entity.getCallSet() == null || entity.getVariant() == null) {
				continue;
			}
			newCalls.add(entity);
		}
		callService.save(newCalls);
	}
}
