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

import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashSet;
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.CallSetWriteRequest;
import org.brapi.test.BrAPITestServer.model.entity.geno.CallSetEntity;
import org.brapi.test.BrAPITestServer.model.entity.geno.SampleEntity;
import org.brapi.test.BrAPITestServer.model.entity.geno.VariantSetEntity;
import org.brapi.test.BrAPITestServer.repository.geno.CallRepository;
import org.brapi.test.BrAPITestServer.repository.geno.CallSetRepository;
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.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

import io.swagger.model.Metadata;
import io.swagger.model.geno.CallSet;
import io.swagger.model.geno.CallSetsSearchRequest;

@Service
public class CallSetService {

	private final CallSetRepository callSetRepository;
	private final CallRepository callRepository;
	private final SampleService sampleService;
	private final VariantSetRepository variantSetRepository;

	public CallSetService(CallSetRepository callSetRepository, CallRepository callRepository,
			SampleService sampleService, VariantSetRepository variantSetRepository) {
		this.callSetRepository = callSetRepository;
		this.callRepository = callRepository;
		this.sampleService = sampleService;
		this.variantSetRepository = variantSetRepository;
	}

	public List<CallSet> findCallSets(String callSetDbId, String callSetName, String variantSetDbId, String sampleDbId,
			String germplasmDbId, String externalReferenceId, String externalReferenceSource, Metadata metadata) {
		CallSetsSearchRequest request = new CallSetsSearchRequest();
		if (callSetDbId != null)
			request.addCallSetDbIdsItem(callSetDbId);
		if (callSetName != null)
			request.addCallSetNamesItem(callSetName);
		if (variantSetDbId != null)
			request.addVariantSetDbIdsItem(variantSetDbId);
		if (sampleDbId != null)
			request.addSampleDbIdsItem(sampleDbId);
		if (germplasmDbId != null)
			request.addGermplasmDbIdsItem(germplasmDbId);

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

	public List<CallSet> findCallSets(CallSetsSearchRequest request, Metadata metadata) {
		return findCallSetEntities(request, metadata).stream().map(this::convertFromEntity).collect(Collectors.toList());
	}

	public List<CallSetEntity> findCallSetEntities(CallSetsSearchRequest request, Metadata metadata) {
		Pageable pageReq = PagingUtility.getPageRequest(metadata);
		SearchQueryBuilder<CallSetEntity> searchQuery = new SearchQueryBuilder<CallSetEntity>(CallSetEntity.class);
		if(request.getVariantSetDbIds() != null) {
			searchQuery = searchQuery.join("variantSets", "variantSet")
					.appendList(request.getVariantSetDbIds(), "*variantSet.id");
		}
		
		searchQuery = searchQuery.appendList(request.getCallSetDbIds(), "id")
				.appendList(request.getCallSetNames(), "callSetName")
				.appendList(request.getGermplasmDbIds(), "sample.germplasm.id")
				.appendList(request.getGermplasmNames(), "sample.germplasm.germplasmName")
				.appendList(request.getSampleDbIds(), "sample.id")
				.appendList(request.getSampleNames(), "sample.sampleName");

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

	public CallSet getCallSet(String callSetDbId) throws BrAPIServerException {
		return convertFromEntity(getCallSetEntity(callSetDbId, HttpStatus.NOT_FOUND));
	}

	public CallSetEntity getCallSetEntity(String callSetDbId) throws BrAPIServerException {
		return getCallSetEntity(callSetDbId, HttpStatus.BAD_REQUEST);
	}

	public CallSetEntity getCallSetEntity(String callSetDbId, HttpStatus errorStatus) throws BrAPIServerException {
		CallSetEntity callSet = null;
		Optional<CallSetEntity> entityOpt = callSetRepository.findById(callSetDbId);
		if (entityOpt.isPresent()) {
			callSet = entityOpt.get();
		} else {
			throw new BrAPIServerDbIdNotFoundException("callSet", callSetDbId, errorStatus);
		}
		return callSet;
	}

	private CallSet convertFromEntity(CallSetEntity entity) {
		CallSet callSet = new CallSet();
		callSet.setAdditionalInfo(entity.getAdditionalInfoMap());
		callSet.setCallSetDbId(entity.getId());
		callSet.setCallSetName(entity.getCallSetName());
		callSet.setCreated(DateUtility.toOffsetDateTime(entity.getCreated()));
		if (entity.getSample() != null) {
			callSet.setSampleDbId(entity.getSample().getId());
			if (entity.getSample().getObservationUnit() != null && entity.getSample().getObservationUnit().getStudy() != null)
				callSet.setStudyDbId(entity.getSample().getObservationUnit().getStudy().getId());
		}
		callSet.setUpdated(DateUtility.toOffsetDateTime(entity.getUpdated()));
		if (entity.getVariantSets() != null)
			callSet.setVariantSetDbIds(entity.getVariantSets().stream().map(e -> e.getId()).collect(Collectors.toList()));

		return callSet;
	}

	public CallSetEntity save(CallSetEntity entity) {
		return callSetRepository.save(entity);
	}

	public void ensureVariantSetBinding(String callSetDbId, String variantSetDbId) throws BrAPIServerException {
		if (callSetDbId == null || callSetDbId.isBlank() || variantSetDbId == null || variantSetDbId.isBlank()) {
			return;
		}
		CallSetEntity callSet = getCallSetEntity(callSetDbId.trim());
		VariantSetEntity variantSet = variantSetRepository.findById(variantSetDbId.trim())
				.orElseThrow(() -> new BrAPIServerDbIdNotFoundException("variantSet", variantSetDbId,
						HttpStatus.BAD_REQUEST));
		if (callSet.getVariantSets() == null) {
			callSet.setVariantSets(new ArrayList<>());
		}
		boolean exists = callSet.getVariantSets().stream().anyMatch(item -> variantSetDbId.trim().equals(item.getId()));
		if (!exists) {
			callSet.getVariantSets().add(variantSet);
			callSet.setUpdated(new Date());
			callSetRepository.save(callSet);
		}
	}

	public CallSet saveCallSet(CallSetWriteRequest request) throws BrAPIServerException {
		if (request.getCallSetName() == null || request.getCallSetName().isBlank()) {
			throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "callSetName is required");
		}
		if (request.getSampleDbId() == null || request.getSampleDbId().isBlank()) {
			throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "sampleDbId is required");
		}
		if (request.getCallSetDbId() != null && callSetRepository.findById(request.getCallSetDbId()).isPresent()) {
			throw new BrAPIServerException(HttpStatus.CONFLICT, "CallSet already exists: " + request.getCallSetDbId());
		}
		CallSetEntity entity = new CallSetEntity();
		if (request.getCallSetDbId() != null && !request.getCallSetDbId().isBlank()) {
			entity.setId(request.getCallSetDbId().trim());
		}
		entity.setCreated(new Date());
		entity.setUpdated(new Date());
		updateEntity(entity, request);
		return convertFromEntity(callSetRepository.save(entity));
	}

	public CallSet updateCallSet(String callSetDbId, CallSetWriteRequest request) throws BrAPIServerException {
		CallSetEntity entity = getCallSetEntity(callSetDbId, HttpStatus.NOT_FOUND);
		entity.setUpdated(new Date());
		updateEntity(entity, request);
		return convertFromEntity(callSetRepository.save(entity));
	}

	public CallSet deleteCallSet(String callSetDbId) throws BrAPIServerException {
		CallSetEntity entity = getCallSetEntity(callSetDbId, HttpStatus.NOT_FOUND);
		assertNoCallSetDependencies(callSetDbId);
		CallSet deleted = convertFromEntity(entity);
		try {
			callSetRepository.delete(entity);
			callSetRepository.flush();
		} catch (DataIntegrityViolationException e) {
			throw new BrAPIServerException(HttpStatus.CONFLICT, "CallSet is in use and cannot be deleted");
		}
		return deleted;
	}

	private void assertNoCallSetDependencies(String callSetDbId) throws BrAPIServerException {
		if (callRepository.countByCallSetDbId(callSetDbId) > 0) {
			throw new BrAPIServerException(HttpStatus.CONFLICT, "CallSet is referenced by allele_call records");
		}
	}

	private void updateEntity(CallSetEntity entity, CallSetWriteRequest request) throws BrAPIServerException {
		if (request.getCallSetName() != null && !request.getCallSetName().isBlank()) {
			entity.setCallSetName(request.getCallSetName().trim());
		} else if (entity.getCallSetName() == null || entity.getCallSetName().isBlank()) {
			throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "callSetName is required");
		}
		if (request.getSampleDbId() != null && !request.getSampleDbId().isBlank()) {
			SampleEntity sample = sampleService.getSampleEntity(request.getSampleDbId());
			entity.setSample(sample);
		} else if (entity.getSample() == null) {
			throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "sampleDbId is required");
		}
		if (request.getVariantSetDbIds() != null) {
			List<VariantSetEntity> variantSets = new ArrayList<>();
			LinkedHashSet<String> seen = new LinkedHashSet<>();
			for (String variantSetDbId : request.getVariantSetDbIds()) {
				if (variantSetDbId == null || variantSetDbId.isBlank()) {
					continue;
				}
				String normalizedId = variantSetDbId.trim();
				if (!seen.add(normalizedId)) {
					continue;
				}
				VariantSetEntity variantSet = variantSetRepository.findById(normalizedId)
						.orElseThrow(() -> new BrAPIServerDbIdNotFoundException("variantSet", variantSetDbId,
								HttpStatus.BAD_REQUEST));
				variantSets.add(variantSet);
			}
			entity.setVariantSets(variantSets);
		}
	}
}
