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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.lang3.math.NumberUtils;
import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerDbIdNotFoundException;
import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException;
import org.brapi.test.BrAPITestServer.model.dto.geno.CallWriteRequest;
import org.brapi.test.BrAPITestServer.model.entity.geno.CallEntity;
import org.brapi.test.BrAPITestServer.model.entity.geno.CallSetEntity;
import org.brapi.test.BrAPITestServer.model.entity.geno.VariantEntity;
import org.brapi.test.BrAPITestServer.repository.geno.CallRepository;
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.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.Call;
import io.swagger.model.geno.CallGenotypeMetadata;
import io.swagger.model.geno.CallsListResponseResult;
import io.swagger.model.geno.CallsSearchRequest;
import io.swagger.model.geno.DataTypePrimitives;
import io.swagger.model.geno.ListValue;
import jakarta.validation.Valid;

@Service
public class CallService {

	public static final boolean EXPAND_HOMOZYFOTES_DEFAULT = true;
	public static final String SEP_PHASED_DEFAULT = "/";
	public static final String SEP_UNPHASED_DEFAULT = "|";
	public static final String UNKNOWN_STRING_DEFAULT = ".";

	private final CallRepository callRepository;
	private final CallSetService callSetService;
	private final VariantService variantService;

	public CallService(CallRepository callRepository, CallSetService callSetService, VariantService variantService) {
		this.callRepository = callRepository;
		this.callSetService = callSetService;
		this.variantService = variantService;
	}

	public CallsListResponseResult findCalls(String callSetDbId, String variantDbId, String variantSetDbId,
			Boolean expandHomozygotes, String unknownString, String sepPhased, String sepUnphased, Metadata metadata) {
		CallsSearchRequest request = new CallsSearchRequest();
		if (callSetDbId != null)
			request.addCallSetDbIdsItem(callSetDbId);
		if (variantDbId != null)
			request.addVariantDbIdsItem(variantDbId);
		if (variantSetDbId != null)
			request.addVariantSetDbIdsItem(variantSetDbId);
		request.setExpandHomozygotes(expandHomozygotes);
		request.setUnknownString(unknownString);
		request.setSepPhased(sepPhased);
		request.setSepUnphased(sepUnphased);

		return findCalls(request, metadata);
	}

	public CallsListResponseResult findCalls(CallsSearchRequest request, Metadata metadata) {
		if (request == null) {
			request = new CallsSearchRequest();
		}
		final CallsSearchRequest formattingRequest = request;
		List<Call> calls = findCallEntities(request, metadata).stream().map(e -> {
			return convertFromEntityWithFormatting(e, formattingRequest);
		}).collect(Collectors.toList());
		CallsListResponseResult result = buildResult(request, calls);
		return result;
	}

	public List<CallEntity> findCallEntities(CallsSearchRequest request, Metadata metadata) {
		if (request == null) {
			request = new CallsSearchRequest();
		}
		Pageable pageReq = PagingUtility.getPageRequest(metadata);
		boolean callSetFilter = hasValues(request.getCallSetDbIds());
		boolean variantFilter = hasValues(request.getVariantDbIds());
		boolean variantSetFilter = hasValues(request.getVariantSetDbIds());
		Page<CallEntity> page;
		if (!callSetFilter && !variantFilter && !variantSetFilter) {
			page = callRepository.findAllWithReferences(pageReq);
		} else {
			page = callRepository.findByFiltersWithReferences(safeFilterValues(request.getCallSetDbIds()),
					callSetFilter,
					safeFilterValues(request.getVariantDbIds()),
					variantFilter,
					safeFilterValues(request.getVariantSetDbIds()),
					variantSetFilter,
					pageReq);
		}
		PagingUtility.calculateMetaData(metadata, page);
		return page.getContent();
	}

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

	private List<String> safeFilterValues(List<String> values) {
		if (values == null) {
			return List.of("__brapi_no_filter__");
		}
		List<String> filtered = values.stream().filter(value -> value != null && !value.isBlank())
				.collect(Collectors.toList());
		return filtered.isEmpty() ? List.of("__brapi_no_filter__") : filtered;
	}

	private CallsListResponseResult buildResult(CallsSearchRequest request, List<Call> calls) {
		if (request == null) {
			request = new CallsSearchRequest();
		}
		CallsListResponseResult result = new CallsListResponseResult();
		result.setData(calls);
		result.setExpandHomozygotes(EXPAND_HOMOZYFOTES_DEFAULT);
		if (request.isExpandHomozygotes() != null)
			result.setExpandHomozygotes(request.isExpandHomozygotes());
		result.setSepPhased(SEP_PHASED_DEFAULT);
		if (request.getSepPhased() != null)
			result.setSepPhased(request.getSepPhased());
		result.setSepUnphased(SEP_UNPHASED_DEFAULT);
		if (request.getSepUnphased() != null)
			result.setSepUnphased(request.getSepUnphased());
		result.setUnknownString(UNKNOWN_STRING_DEFAULT);
		if (request.getUnknownString() != null)
			result.setUnknownString(request.getUnknownString());

		return result;
	}

	public Call convertFromEntity(CallEntity entity) {
		return convertFromEntityWithFormatting(entity, new CallsSearchRequest());
	}

	private Call convertFromEntityWithFormatting(CallEntity entity, CallsSearchRequest request) {
		Call call = new Call();
		Map<String, Object> additionalInfo = entity.getAdditionalInfoMap();
		if (additionalInfo == null) {
			additionalInfo = new HashMap<>();
		} else {
			additionalInfo = new HashMap<>(additionalInfo);
		}
		additionalInfo.put("callDbId", entity.getId());
		call.setAdditionalInfo(additionalInfo);
		if (entity.getCallSet() != null) {
			call.setCallSetDbId(entity.getCallSet().getId());
			call.setCallSetName(entity.getCallSet().getCallSetName());
		}
		if (entity.getGenotype() != null) {
			String genotypeString = entity.getGenotype();
			if(request.getSepPhased() != null)
				genotypeString = genotypeString.replace(SEP_PHASED_DEFAULT, request.getSepPhased());
			if(request.getSepUnphased() != null)
				genotypeString = genotypeString.replace(SEP_UNPHASED_DEFAULT, request.getSepUnphased());

			ListValue listValues = new ListValue();
			listValues.setValues(Arrays.asList(genotypeString));
			call.setGenotype(listValues);
			call.setGenotypeValue(genotypeString);
		}
		if (entity.getReadDepth() != null) {
			CallGenotypeMetadata metadata = new CallGenotypeMetadata();
			metadata.setDataType(DataTypePrimitives.INTEGER);
			metadata.setFieldAbbreviation("RD");
			metadata.setFieldName("Read Depth");
			metadata.setFieldValue(entity.getReadDepth().toString());
			call.addGenotypeMetadataItem(metadata);
		}
		if (entity.getGenotypeLikelihood() != null) {
			call.setGenotypeLikelihood(Arrays.asList(entity.getGenotypeLikelihood()));

			CallGenotypeMetadata metadata = new CallGenotypeMetadata();
			metadata.setDataType(DataTypePrimitives.INTEGER);
			metadata.setFieldAbbreviation("GL");
			metadata.setFieldName("Genotype Likelihood");
			metadata.setFieldValue(entity.getGenotypeLikelihood().toString());

			call.addGenotypeMetadataItem(metadata);
		}
		call.setPhaseSet(entity.getPhaseSet());
		if (entity.getVariant() != null) {
			call.setVariantDbId(entity.getVariant().getId());
			call.setVariantName(entity.getVariant().getVariantName());
			if (entity.getVariant().getVariantSet() != null) {
				call.setVariantSetDbId(entity.getVariant().getVariantSet().getId());
				call.setVariantSetName(entity.getVariant().getVariantSet().getVariantSetName());
			}
		}
		return call;
	}

	public void save(List<CallEntity> entities) {
		callRepository.saveAll(entities);
	}

	public CallsListResponseResult updateCalls(List<Call> body) throws BrAPIServerException {
		if (body == null || body.isEmpty()) {
			return findFirstExistingCallResult();
		}
		CallsSearchRequest searchReq = new CallsSearchRequest();
		List<CallEntity> savedEntities = new ArrayList<>();
		List<Call> responseCalls = new ArrayList<>();
		Set<String> processedBindings = new HashSet<>();
		for (Call call : body) {
			if (call == null) {
				continue;
			}
			fillVariantSetDbId(call);
			if (call.getCallSetDbId() == null || call.getVariantDbId() == null || call.getVariantSetDbId() == null) {
				responseCalls.add(call);
				continue;
			}
			searchReq.addCallSetDbIdsItem(call.getCallSetDbId());
			searchReq.addVariantDbIdsItem(call.getVariantDbId());
			searchReq.addVariantSetDbIdsItem(call.getVariantSetDbId());
			Optional<CallEntity> entityOpt = callRepository.findByCompositeKey(call.getCallSetDbId(),
					call.getVariantDbId(), call.getVariantSetDbId());
			if (entityOpt.isPresent()) {
				CallEntity entity = entityOpt.get();
				updateEntity(entity, call);
				savedEntities.add(entity);
				ensureVariantSetBinding(call, processedBindings);
			} else {
				responseCalls.add(call);
			}
		}
		savedEntities = callRepository.saveAll(savedEntities);

		responseCalls.addAll(savedEntities.stream().map(e -> {
			return convertFromEntityWithFormatting(e, searchReq);
		}).collect(Collectors.toList()));
		CallsListResponseResult result = buildResult(searchReq, responseCalls);
		return result;
	}

	private CallsListResponseResult findFirstExistingCallResult() {
		List<Call> calls = findCallEntities(new CallsSearchRequest(), new Metadata()).stream()
				.limit(1)
				.map(this::convertFromEntity)
				.collect(Collectors.toList());
		return buildResult(new CallsSearchRequest(), calls);
	}

	private void fillVariantSetDbId(Call call) throws BrAPIServerException {
		if (call.getVariantSetDbId() != null || call.getVariantDbId() == null || call.getVariantDbId().isBlank()) {
			return;
		}
		VariantEntity variant = variantService.getVariantEntity(call.getVariantDbId());
		if (variant.getVariantSet() != null) {
			call.setVariantSetDbId(variant.getVariantSet().getId());
		}
	}

	private void updateEntity(CallEntity entity, Call call) {
		if (call.getAdditionalInfo() != null) {
			entity.setAdditionalInfo(call.getAdditionalInfo());
		}
		if (call.getPhaseSet() != null) {
			entity.setPhaseSet(call.getPhaseSet());
		}
		if (call.getGenotypeLikelihood() != null && !call.getGenotypeLikelihood().isEmpty()) {
			entity.setGenotypeLikelihood(call.getGenotypeLikelihood().get(0));
		}
		if (call.getGenotypeValue() != null) {
			entity.setGenotype(call.getGenotypeValue());
		} else if (call.getGenotype() != null && call.getGenotype().getValues() != null
				&& !call.getGenotype().getValues().isEmpty()) {
			entity.setGenotype(call.getGenotype().getValues().get(0));
		}
		if (call.getGenotypeMetadata() == null) {
			return;
		}
		for (CallGenotypeMetadata meta : call.getGenotypeMetadata()) {
			if (meta == null || meta.getFieldAbbreviation() == null) {
				continue;
			}
			if (meta.getFieldAbbreviation().equalsIgnoreCase("RD")) {
				entity.setReadDepth(NumberUtils.toInt(meta.getFieldValue()));
			}
			if (meta.getFieldAbbreviation().equalsIgnoreCase("GL")) {
				entity.setGenotypeLikelihood(NumberUtils.toDouble(meta.getFieldValue()));
			}
		}

	}

	public Call getCall(String callDbId) throws BrAPIServerException {
		return convertFromEntity(getCallEntity(callDbId, HttpStatus.NOT_FOUND));
	}

	public CallEntity getCallEntity(String callDbId) throws BrAPIServerException {
		return getCallEntity(callDbId, HttpStatus.BAD_REQUEST);
	}

	public CallEntity getCallEntity(String callDbId, HttpStatus errorStatus) throws BrAPIServerException {
		Optional<CallEntity> entityOpt = callRepository.findById(callDbId);
		if (entityOpt.isPresent()) {
			return entityOpt.get();
		}
		throw new BrAPIServerDbIdNotFoundException("call", callDbId, errorStatus);
	}

	public Call saveCall(CallWriteRequest request) throws BrAPIServerException {
		if (request.getCallSetDbId() == null || request.getCallSetDbId().isBlank()) {
			throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "callSetDbId is required");
		}
		if (request.getVariantDbId() == null || request.getVariantDbId().isBlank()) {
			throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "variantDbId is required");
		}
		if (request.getGenotype() == null || request.getGenotype().isBlank()) {
			throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "genotype is required");
		}
		if (request.getCallDbId() != null && callRepository.findById(request.getCallDbId()).isPresent()) {
			throw new BrAPIServerException(HttpStatus.CONFLICT, "Call already exists: " + request.getCallDbId());
		}
		CallSetEntity callSet = callSetService.getCallSetEntity(request.getCallSetDbId());
		VariantEntity variant = variantService.getVariantEntity(request.getVariantDbId());
		assertNoDuplicateCall(callSet.getId(), variant.getId(), null);
		if (request.getReadDepth() != null && request.getReadDepth() < 0) {
			throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "readDepth cannot be negative");
		}

		CallEntity entity = new CallEntity();
		if (request.getCallDbId() != null && !request.getCallDbId().isBlank()) {
			entity.setId(request.getCallDbId().trim());
		}
		updateEntity(entity, request, callSet, variant);
		Call saved = convertFromEntity(callRepository.save(entity));
		if (variant.getVariantSet() != null) {
			callSetService.ensureVariantSetBinding(callSet.getId(), variant.getVariantSet().getId());
		}
		return saved;
	}

	public List<Call> importCalls(List<CallWriteRequest> requests) throws BrAPIServerException {
		List<Call> saved = new ArrayList<>();
		for (CallWriteRequest request : requests) {
			saved.add(saveCall(request));
		}
		return saved;
	}

	public Call updateCall(String callDbId, CallWriteRequest request) throws BrAPIServerException {
		CallEntity entity = getCallEntity(callDbId, HttpStatus.NOT_FOUND);
		CallSetEntity callSet = callSetService.getCallSetEntity(request.getCallSetDbId());
		VariantEntity variant = variantService.getVariantEntity(request.getVariantDbId());
		assertNoDuplicateCall(callSet.getId(), variant.getId(), callDbId);
		if (request.getReadDepth() != null && request.getReadDepth() < 0) {
			throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "readDepth cannot be negative");
		}
		updateEntity(entity, request, callSet, variant);
		Call saved = convertFromEntity(callRepository.save(entity));
		if (variant.getVariantSet() != null) {
			callSetService.ensureVariantSetBinding(callSet.getId(), variant.getVariantSet().getId());
		}
		return saved;
	}

	public Call deleteCall(String callDbId) throws BrAPIServerException {
		CallEntity entity = getCallEntity(callDbId, HttpStatus.NOT_FOUND);
		Call deleted = convertFromEntity(entity);
		try {
			callRepository.delete(entity);
			callRepository.flush();
		} catch (DataIntegrityViolationException e) {
			throw new BrAPIServerException(HttpStatus.CONFLICT, "Call cannot be deleted");
		}
		return deleted;
	}

	private void assertNoDuplicateCall(String callSetDbId, String variantDbId, String excludeCallDbId)
			throws BrAPIServerException {
		Pageable pageReq = PageRequest.of(0, 1);
		SearchQueryBuilder<CallEntity> searchQuery = new SearchQueryBuilder<CallEntity>(CallEntity.class)
				.appendSingle(callSetDbId, "callSet.id")
				.appendSingle(variantDbId, "variant.id");
		Page<CallEntity> page = callRepository.findAllBySearch(searchQuery, pageReq);
		if (page.getTotalElements() > 0) {
			CallEntity existing = page.getContent().get(0);
			if (excludeCallDbId == null || !excludeCallDbId.equals(existing.getId())) {
				throw new BrAPIServerException(HttpStatus.CONFLICT,
						"Call already exists for this callSet and variant");
			}
		}
	}

	private void updateEntity(CallEntity entity, CallWriteRequest request, CallSetEntity callSet,
			VariantEntity variant) {
		entity.setCallSet(callSet);
		entity.setVariant(variant);
		entity.setGenotype(request.getGenotype().trim());
		entity.setReadDepth(request.getReadDepth());
		entity.setGenotypeLikelihood(request.getGenotypeLikelihood());
		entity.setPhaseSet(request.getPhaseSet());
	}

	private void ensureVariantSetBindings(List<Call> calls) throws BrAPIServerException {
		Set<String> processed = new HashSet<>();
		for (Call call : calls) {
			ensureVariantSetBinding(call, processed);
		}
	}

	private void ensureVariantSetBinding(Call call, Set<String> processed) throws BrAPIServerException {
		if (call == null || call.getCallSetDbId() == null || call.getVariantSetDbId() == null) {
			return;
		}
		String key = call.getCallSetDbId() + "|" + call.getVariantSetDbId();
		if (processed.add(key)) {
			callSetService.ensureVariantSetBinding(call.getCallSetDbId(), call.getVariantSetDbId());
		}
	}

}
