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

import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
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.ReferenceBasesWriteRequest;
import org.brapi.test.BrAPITestServer.model.entity.geno.ReferenceBasesPageEntity;
import org.brapi.test.BrAPITestServer.model.entity.geno.ReferenceEntity;
import org.brapi.test.BrAPITestServer.repository.geno.ReferenceBaseRepository;
import org.brapi.test.BrAPITestServer.service.PagingUtility;
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.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

import io.swagger.model.Metadata;
import io.swagger.model.geno.ReferenceBasesPage;

@Service
public class ReferenceBasesService {
	private static final Pattern BASES_PATTERN = Pattern.compile("^[ACGTNacgtn*.-]*$");

	private final ReferenceBaseRepository referenceBaseRepository;
	private final ReferenceService referenceService;

	public ReferenceBasesService(ReferenceBaseRepository referenceBaseRepository, ReferenceService referenceService) {
		this.referenceBaseRepository = referenceBaseRepository;
		this.referenceService = referenceService;
	}

	public List<ReferenceBasesPage> findReferenceBasesPages(Metadata metadata) {
		Pageable pageReq = PagingUtility.getPageRequest(metadata);
		Page<ReferenceBasesPageEntity> page = referenceBaseRepository.findAll(pageReq);
		List<ReferenceBasesPage> data = page.getContent().stream().map(this::convertFromEntity).collect(Collectors.toList());
		PagingUtility.calculateMetaData(metadata, page);
		return data;
	}

	public ReferenceBasesPage getReferenceBasesPage(String referenceBasesDbId) throws BrAPIServerException {
		return convertFromEntity(getReferenceBasesPageEntity(referenceBasesDbId, HttpStatus.NOT_FOUND));
	}

	public ReferenceBasesPage saveReferenceBasesPage(ReferenceBasesWriteRequest request) throws BrAPIServerException {
		validateRequest(request, true);
		if (request.getReferenceBasesDbId() != null
				&& referenceBaseRepository.findById(request.getReferenceBasesDbId()).isPresent()) {
			throw new BrAPIServerException(HttpStatus.CONFLICT,
					"ReferenceBases already exists: " + request.getReferenceBasesDbId());
		}
		assertUniquePageNumber(null, request.getReferenceDbId(), request.getPageNumber());
		ReferenceBasesPageEntity entity = new ReferenceBasesPageEntity();
		if (request.getReferenceBasesDbId() != null && !request.getReferenceBasesDbId().isBlank()) {
			entity.setId(request.getReferenceBasesDbId().trim());
		}
		updateEntity(entity, request);
		return convertFromEntity(referenceBaseRepository.save(entity));
	}

	public ReferenceBasesPage updateReferenceBasesPage(String referenceBasesDbId, ReferenceBasesWriteRequest request)
			throws BrAPIServerException {
		validateRequest(request, false);
		ReferenceBasesPageEntity entity = getReferenceBasesPageEntity(referenceBasesDbId, HttpStatus.NOT_FOUND);
		assertUniquePageNumber(referenceBasesDbId, request.getReferenceDbId(), request.getPageNumber());
		updateEntity(entity, request);
		return convertFromEntity(referenceBaseRepository.save(entity));
	}

	public ReferenceBasesPage deleteReferenceBasesPage(String referenceBasesDbId) throws BrAPIServerException {
		ReferenceBasesPageEntity entity = getReferenceBasesPageEntity(referenceBasesDbId, HttpStatus.NOT_FOUND);
		ReferenceBasesPage deleted = convertFromEntity(entity);
		try {
			referenceBaseRepository.delete(entity);
			referenceBaseRepository.flush();
		} catch (DataIntegrityViolationException e) {
			throw new BrAPIServerException(HttpStatus.CONFLICT, "ReferenceBases cannot be deleted");
		}
		return deleted;
	}

	private ReferenceBasesPageEntity getReferenceBasesPageEntity(String referenceBasesDbId, HttpStatus errorStatus)
			throws BrAPIServerException {
		Optional<ReferenceBasesPageEntity> entityOpt = referenceBaseRepository.findById(referenceBasesDbId);
		if (entityOpt.isPresent()) {
			return entityOpt.get();
		}
		throw new BrAPIServerDbIdNotFoundException("referenceBases", referenceBasesDbId, errorStatus);
	}

	private void validateRequest(ReferenceBasesWriteRequest request, boolean creating) throws BrAPIServerException {
		if (request.getReferenceDbId() == null || request.getReferenceDbId().isBlank()) {
			throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "referenceDbId is required");
		}
		if (request.getPageNumber() == null) {
			throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "pageNumber is required");
		}
		if (request.getPageNumber() < 0) {
			throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "pageNumber must be non-negative");
		}
		if (creating && (request.getBases() == null || request.getBases().isBlank())) {
			throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "bases is required");
		}
		if (request.getBases() != null) {
			if (request.getBases().length() > 2048) {
				throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "bases cannot exceed 2048 characters");
			}
			if (!BASES_PATTERN.matcher(request.getBases()).matches()) {
				throw new BrAPIServerException(HttpStatus.BAD_REQUEST, "bases contains invalid characters");
			}
		}
	}

	private void assertUniquePageNumber(String currentId, String referenceDbId, Integer pageNumber)
			throws BrAPIServerException {
		Pageable pageReq = PageRequest.of(0, 1000, Sort.by("pageNumber"));
		Page<ReferenceBasesPageEntity> page = referenceBaseRepository.findByReferenceId(referenceDbId, pageReq);
		for (ReferenceBasesPageEntity existing : page.getContent()) {
			if (pageNumber != null && pageNumber.equals(existing.getPageNumber())
					&& (currentId == null || !currentId.equals(existing.getId()))) {
				throw new BrAPIServerException(HttpStatus.CONFLICT,
						"pageNumber already exists for this reference: " + pageNumber);
			}
		}
	}

	private void updateEntity(ReferenceBasesPageEntity entity, ReferenceBasesWriteRequest request)
			throws BrAPIServerException {
		ReferenceEntity reference = referenceService.getReferenceEntity(request.getReferenceDbId());
		entity.setReference(reference);
		entity.setPageNumber(request.getPageNumber());
		if (request.getBases() != null) {
			entity.setBases(request.getBases().trim().toUpperCase());
		}
	}

	private ReferenceBasesPage convertFromEntity(ReferenceBasesPageEntity entity) {
		ReferenceBasesPage page = new ReferenceBasesPage();
		page.setReferenceBasesDbId(entity.getId());
		page.setPageNumber(entity.getPageNumber());
		page.setBases(entity.getBases());
		if (entity.getReference() != null) {
			page.setReferenceDbId(entity.getReference().getId());
			page.setReferenceName(entity.getReference().getReferenceName());
		}
		return page;
	}
}
