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

import java.util.List;
import java.util.Optional;

import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerDbIdNotFoundException;
import org.brapi.test.BrAPITestServer.exceptions.BrAPIServerException;
import org.brapi.test.BrAPITestServer.model.dto.geno.GenomeMapWriteRequest;
import org.brapi.test.BrAPITestServer.model.dto.geno.LinkageGroupWriteRequest;
import org.brapi.test.BrAPITestServer.model.entity.geno.GenomeMapEntity;
import org.brapi.test.BrAPITestServer.model.entity.geno.LinkageGroupEntity;
import org.brapi.test.BrAPITestServer.model.entity.geno.MarkerPositionEntity;
import org.brapi.test.BrAPITestServer.repository.geno.GenomeMapRepository;
import org.brapi.test.BrAPITestServer.repository.geno.LinkageGroupRepository;
import org.brapi.test.BrAPITestServer.repository.geno.MarkerPositionRepository;
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.core.CropService;
import org.springframework.beans.factory.annotation.Autowired;
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.GenomeMap;
import io.swagger.model.geno.LinkageGroup;

@Service
public class GenomeMapService {

	private final GenomeMapRepository genomeMapRepository;
	private final LinkageGroupRepository linkageGroupRepository;
	private final MarkerPositionRepository markerPositionRepository;
	private final CropService cropService;

	@Autowired
	public GenomeMapService(GenomeMapRepository genomeMapRepository, LinkageGroupRepository linkageGroupRepository,
			MarkerPositionRepository markerPositionRepository, CropService cropService) {
		this.genomeMapRepository = genomeMapRepository;
		this.linkageGroupRepository = linkageGroupRepository;
		this.markerPositionRepository = markerPositionRepository;
		this.cropService = cropService;
	}

	public List<GenomeMap> findMaps(String commonCropName, String mapPUI, String scientificName, String type,
			String programDbId, String trialDbId, String studyDbId, Metadata metadata) {
		SearchQueryBuilder<GenomeMapEntity> searchQuery = new SearchQueryBuilder<GenomeMapEntity>(GenomeMapEntity.class);

		if (programDbId != null || trialDbId != null || studyDbId != null) {
			searchQuery = searchQuery.join("studies", "study");
		}
		
		searchQuery.appendSingle(commonCropName, "crop.cropName")
				.appendSingle(mapPUI, "mapPUI")
				.appendSingle(scientificName, "scientificName")
				.appendSingle(type, "type")
				.appendSingle(programDbId, "*study.trial.program.id")
				.appendSingle(trialDbId, "*study.trial.id")
				.appendSingle(studyDbId, "*study.id");

		Pageable pageReq = PagingUtility.getPageRequest(metadata);
		Page<GenomeMapEntity> page = genomeMapRepository.findAllBySearch(searchQuery, pageReq);
		List<GenomeMap> maps = page.map(this::convertFromEntity).getContent();
		PagingUtility.calculateMetaData(metadata, page);
		return maps;
	}

	public GenomeMap getMap(String mapDbId) throws BrAPIServerException {
		GenomeMap map = null;
		Optional<GenomeMapEntity> entityOpt = genomeMapRepository.findById(mapDbId);
		if (entityOpt.isPresent()) {
			map = convertFromEntity(entityOpt.get());
		} else {
			throw new BrAPIServerDbIdNotFoundException("map", mapDbId, HttpStatus.NOT_FOUND);
		}
		return map;
	}

	public List<LinkageGroup> findLinkageGroups(String mapDbId, Metadata metadata) {
		SearchQueryBuilder<LinkageGroupEntity> searchQuery = new SearchQueryBuilder<LinkageGroupEntity>(
				LinkageGroupEntity.class).appendSingle(mapDbId, "genomeMap.id");

		Pageable pageReq = PagingUtility.getPageRequest(metadata);
		Page<LinkageGroupEntity> page = linkageGroupRepository.findAllBySearch(searchQuery, pageReq);
		List<LinkageGroup> linkageGroups = page.map(this::convertLinkageGroupFromEntity).getContent();
		PagingUtility.calculateMetaData(metadata, page);
		return linkageGroups;
	}

	public GenomeMap saveMap(GenomeMapWriteRequest request) throws BrAPIServerException {
		if (request.getMapName() == null || request.getMapName().isBlank()) {
			throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "mapName is required");
		}
		if (request.getMapDbId() != null && genomeMapRepository.findById(request.getMapDbId()).isPresent()) {
			throw new BrAPIServerException(HttpStatus.CONFLICT, "GenomeMap already exists: " + request.getMapDbId());
		}
		GenomeMapEntity entity = new GenomeMapEntity();
		if (request.getMapDbId() != null && !request.getMapDbId().isBlank()) {
			entity.setId(request.getMapDbId().trim());
		}
		updateMapEntity(entity, request);
		entity = genomeMapRepository.save(entity);
		ensureMapPUI(entity);
		return convertFromEntity(genomeMapRepository.save(entity));
	}

	public GenomeMap updateMap(String mapDbId, GenomeMapWriteRequest request) throws BrAPIServerException {
		GenomeMapEntity entity = getMapEntity(mapDbId);
		updateMapEntity(entity, request);
		return convertFromEntity(genomeMapRepository.save(entity));
	}

	public GenomeMap deleteMap(String mapDbId) throws BrAPIServerException {
		GenomeMapEntity entity = getMapEntity(mapDbId);
		assertNoMapDependencies(mapDbId);
		GenomeMap deleted = convertFromEntity(entity);
		try {
			genomeMapRepository.delete(entity);
			genomeMapRepository.flush();
		} catch (DataIntegrityViolationException e) {
			throw new BrAPIServerException(HttpStatus.CONFLICT, "GenomeMap is in use and cannot be deleted");
		}
		return deleted;
	}

	public LinkageGroup saveLinkageGroup(String mapDbId, LinkageGroupWriteRequest request) throws BrAPIServerException {
		if (request.getLinkageGroupName() == null || request.getLinkageGroupName().isBlank()) {
			throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "linkageGroupName is required");
		}
		if (request.getLinkageGroupDbId() != null
				&& linkageGroupRepository.findById(request.getLinkageGroupDbId()).isPresent()) {
			throw new BrAPIServerException(HttpStatus.CONFLICT,
					"LinkageGroup already exists: " + request.getLinkageGroupDbId());
		}
		GenomeMapEntity map = getMapEntity(mapDbId);
		assertLinkageGroupNameUnique(map.getId(), request.getLinkageGroupName().trim(), null);
		LinkageGroupEntity entity = new LinkageGroupEntity();
		if (request.getLinkageGroupDbId() != null && !request.getLinkageGroupDbId().isBlank()) {
			entity.setId(request.getLinkageGroupDbId().trim());
		}
		entity.setGenomeMap(map);
		updateLinkageGroupEntity(entity, request);
		return convertLinkageGroupFromEntity(linkageGroupRepository.save(entity));
	}

	public LinkageGroup updateLinkageGroup(String mapDbId, String linkageGroupDbId, LinkageGroupWriteRequest request)
			throws BrAPIServerException {
		LinkageGroupEntity entity = getLinkageGroupEntity(linkageGroupDbId);
		if (entity.getGenomeMap() == null || !mapDbId.equals(entity.getGenomeMap().getId())) {
			throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "LinkageGroup does not belong to map: " + mapDbId);
		}
		if (request.getLinkageGroupName() != null && !request.getLinkageGroupName().isBlank()) {
			assertLinkageGroupNameUnique(mapDbId, request.getLinkageGroupName().trim(), linkageGroupDbId);
		}
		updateLinkageGroupEntity(entity, request);
		return convertLinkageGroupFromEntity(linkageGroupRepository.save(entity));
	}

	public LinkageGroup deleteLinkageGroup(String mapDbId, String linkageGroupDbId) throws BrAPIServerException {
		LinkageGroupEntity entity = getLinkageGroupEntity(linkageGroupDbId);
		if (entity.getGenomeMap() == null || !mapDbId.equals(entity.getGenomeMap().getId())) {
			throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "LinkageGroup does not belong to map: " + mapDbId);
		}
		assertNoLinkageGroupDependencies(linkageGroupDbId);
		LinkageGroup deleted = convertLinkageGroupFromEntity(entity);
		try {
			linkageGroupRepository.delete(entity);
			linkageGroupRepository.flush();
		} catch (DataIntegrityViolationException e) {
			throw new BrAPIServerException(HttpStatus.CONFLICT, "LinkageGroup is in use and cannot be deleted");
		}
		return deleted;
	}

	public GenomeMapEntity getMapEntity(String mapDbId) throws BrAPIServerException {
		return genomeMapRepository.findById(mapDbId)
				.orElseThrow(() -> new BrAPIServerDbIdNotFoundException("map", mapDbId, HttpStatus.NOT_FOUND));
	}

	public LinkageGroupEntity getLinkageGroupEntity(String linkageGroupDbId) throws BrAPIServerException {
		return linkageGroupRepository.findById(linkageGroupDbId).orElseThrow(
				() -> new BrAPIServerDbIdNotFoundException("linkageGroup", linkageGroupDbId, HttpStatus.NOT_FOUND));
	}

	private void assertNoMapDependencies(String mapDbId) throws BrAPIServerException {
		Pageable pageReq = PageRequest.of(0, 1);
		SearchQueryBuilder<LinkageGroupEntity> query = new SearchQueryBuilder<LinkageGroupEntity>(LinkageGroupEntity.class)
				.appendSingle(mapDbId, "genomeMap.id");
		if (linkageGroupRepository.findAllBySearch(query, pageReq).getTotalElements() > 0) {
			throw new BrAPIServerException(HttpStatus.CONFLICT, "GenomeMap is referenced by linkage group records");
		}
	}

	private void assertNoLinkageGroupDependencies(String linkageGroupDbId) throws BrAPIServerException {
		Pageable pageReq = PageRequest.of(0, 1);
		SearchQueryBuilder<MarkerPositionEntity> query = new SearchQueryBuilder<MarkerPositionEntity>(
				MarkerPositionEntity.class).appendSingle(linkageGroupDbId, "linkageGroup.id");
		if (markerPositionRepository.findAllBySearch(query, pageReq).getTotalElements() > 0) {
			throw new BrAPIServerException(HttpStatus.CONFLICT,
					"LinkageGroup is referenced by marker_position records");
		}
	}

	private void assertLinkageGroupNameUnique(String mapDbId, String linkageGroupName, String excludeId)
			throws BrAPIServerException {
		SearchQueryBuilder<LinkageGroupEntity> query = new SearchQueryBuilder<LinkageGroupEntity>(LinkageGroupEntity.class)
				.appendSingle(mapDbId, "genomeMap.id")
				.appendSingle(linkageGroupName, "linkageGroupName");
		Page<LinkageGroupEntity> page = linkageGroupRepository.findAllBySearch(query, PageRequest.of(0, 2));
		for (LinkageGroupEntity existing : page.getContent()) {
			if (excludeId == null || !excludeId.equals(existing.getId())) {
				throw new BrAPIServerException(HttpStatus.CONFLICT,
						"linkageGroupName already exists on this map: " + linkageGroupName);
			}
		}
	}

	private void ensureMapPUI(GenomeMapEntity entity) {
		if (entity.getMapPUI() == null || entity.getMapPUI().isBlank()) {
			entity.setMapPUI("urn:uuid:" + entity.getId());
		}
	}

	private void updateMapEntity(GenomeMapEntity entity, GenomeMapWriteRequest request) throws BrAPIServerException {
		entity.setMapName(request.getMapName().trim());
		entity.setScientificName(trimOrNull(request.getScientificName()));
		entity.setType(trimOrNull(request.getType()));
		entity.setUnit(trimOrNull(request.getUnit()));
		entity.setComments(trimOrNull(request.getComments()));
		entity.setDocumentationURL(trimOrNull(request.getDocumentationURL()));
		if (request.getPublishedDate() != null) {
			entity.setPublishedDate(DateUtility.toDate(request.getPublishedDate()));
		}
		if (request.getCommonCropName() != null && !request.getCommonCropName().isBlank()) {
			entity.setCrop(cropService.getCropEntity(request.getCommonCropName().trim()));
		} else if (request.getCommonCropName() != null) {
			entity.setCrop(null);
		}
	}

	private void updateLinkageGroupEntity(LinkageGroupEntity entity, LinkageGroupWriteRequest request)
			throws BrAPIServerException {
		if (request.getLinkageGroupName() != null && !request.getLinkageGroupName().isBlank()) {
			entity.setLinkageGroupName(request.getLinkageGroupName().trim());
		}
		if (request.getMaxPosition() != null) {
			if (request.getMaxPosition() < 0) {
				throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "maxPosition cannot be negative");
			}
			entity.setMaxMarkerPosition(request.getMaxPosition());
		}
	}

	private String trimOrNull(String value) {
		if (value == null) {
			return null;
		}
		String trimmed = value.trim();
		return trimmed.isEmpty() ? null : trimmed;
	}

	private GenomeMap convertFromEntity(GenomeMapEntity entity) {
		GenomeMap map = new GenomeMap();
		map.setAdditionalInfo(entity.getAdditionalInfoMap());
		map.setComments(entity.getComments());
		if (entity.getCrop() != null)
			map.setCommonCropName(entity.getCrop().getCropName());
		map.setDocumentationURL(entity.getDocumentationURL());
		if (entity.getLinkageGroups() != null) {
			map.setLinkageGroupCount(entity.getLinkageGroups().size());
			map.setMarkerCount((int) entity.getLinkageGroups().stream().filter(lg -> lg.getMarkers() != null)
					.flatMap(lg -> lg.getMarkers().stream()).count());
		}
		map.setMapDbId(entity.getId());
		map.setMapName(entity.getMapName());
		map.setMapPUI(entity.getMapPUI());
		map.setPublishedDate(DateUtility.toOffsetDateTime(entity.getPublishedDate()));
		map.setScientificName(entity.getScientificName());
		map.setType(entity.getType());
		map.setUnit(entity.getUnit());

		return map;
	}

	private LinkageGroup convertLinkageGroupFromEntity(LinkageGroupEntity entity) {
		LinkageGroup group = new LinkageGroup();
		group.setAdditionalInfo(entity.getAdditionalInfoMap());
		group.setLinkageGroupName(entity.getLinkageGroupName());
		int markerCount = entity.getMarkers() != null ? entity.getMarkers().size() : 0;
		group.setMarkerCount(markerCount);
		group.setMaxPosition(entity.getMaxMarkerPosition());
		group.putAdditionalInfoItem("linkageGroupDbId", entity.getId());
		if (entity.getGenomeMap() != null) {
			group.putAdditionalInfoItem("mapDbId", entity.getGenomeMap().getId());
			group.putAdditionalInfoItem("mapName", entity.getGenomeMap().getMapName());
		}
		return group;
	}

}
