package com.oguerreiro.resilient.service;

import java.time.Instant;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.oguerreiro.resilient.domain.Period;
import com.oguerreiro.resilient.domain.PeriodVersion;
import com.oguerreiro.resilient.domain.enumeration.PeriodStatus;
import com.oguerreiro.resilient.repository.PeriodRepository;
import com.oguerreiro.resilient.security.SecurityUtils;
import com.oguerreiro.resilient.service.dto.PeriodDTO;
import com.oguerreiro.resilient.service.dto.PeriodVersionDTO;
import com.oguerreiro.resilient.service.interceptors.BeforeSaveServiceInterceptor;
import com.oguerreiro.resilient.service.mapper.PeriodMapper;
import com.oguerreiro.resilient.service.mapper.PeriodVersionMapper;

import jakarta.persistence.EntityNotFoundException;

/**
 * Service Implementation for managing
 * {@link com.oguerreiro.resilient.domain.Period}.
 */
@Service
@Transactional
public class PeriodService extends AbstractResilientService<PeriodDTO, Period, Long>
    implements BeforeSaveServiceInterceptor<PeriodDTO, Period, Long> {

  private final PeriodRepository periodRepository;

  private final PeriodMapper periodMapper;
  private final PeriodVersionMapper periodVersionMapper;

  public PeriodService(PeriodRepository periodRepository, PeriodMapper periodMapper,
      PeriodVersionMapper periodVersionMapper) {
    super(Period.class, periodRepository, periodMapper);
    this.periodRepository = periodRepository;
    this.periodMapper = periodMapper;
    this.periodVersionMapper = periodVersionMapper;
  }

  /* Interface BeforeSaveServiceInterceptor */
  @Override
  public Period beforeSave(Period domain) {
    Period period = (Period) domain;

    // Initial setup (defaults)
    period.setCreationDate(Instant.now());
    period.setCreationUsername(
        SecurityUtils.getCurrentUserLogin().orElseThrow(() -> new RuntimeException("Value not found")));
    period.setState(PeriodStatus.CREATED);

    return period;
  }

  /**
   * Update a period.
   *
   * @param  periodDTO the entity to save.
   * @return           the persisted entity.
   */
  @Override
  public PeriodDTO update(PeriodDTO periodDTO) {
    log.debug("Request to update Period : {}", periodDTO);

    Period period = this.updateSafe(periodDTO);
    return periodMapper.toDto(period);
  }

  public Period updateSafe(PeriodDTO periodDTO) {
    log.debug("Request to updateSafe Period : {}", periodDTO);

    Period existingPeriod = periodRepository.findByIdAndVersionWithRelationships(periodDTO.getId(),
        periodDTO.getVersion()).orElseThrow(() -> new EntityNotFoundException("Period not found"));

    // Merge Period updatable properties
    periodMapper.updatePeriodFromDto(periodDTO, existingPeriod);

    // Merge PeriodVersion's collections, updatable properties
    Set<PeriodVersion> updatedPeriodVersions = periodDTO.getPeriodVersions().stream().map(periodVersionDTO -> {
      // New entry? Just add it
      if (periodVersionDTO.getId() == null) {
        PeriodVersion newPeriodVersion = periodMapper.toPeriodVersion(periodVersionDTO);
        newPeriodVersion.setCreationDate(null);
        newPeriodVersion.setCreationUsername(null);
        newPeriodVersion.setState(null);
        existingPeriod.addPeriodVersion(newPeriodVersion);
        return newPeriodVersion;
      }

      // Find the PeriodVersion in the loaded Period.PerdiodVersions
      PeriodVersion periodVersion = findExistingPeriodVersion(existingPeriod.getPeriodVersions(),
          periodVersionDTO.getId());
      if (periodVersion == null) {
        // Not found. Add it... hummm, isn't it strange that it wasn't found???
        throw new RuntimeException(
            "This is totally unexpected. Only new PeriodVersion's (without id) can be added. But in this caso, the PeriodVersion exists, but is not found in the persisted Period !!");
      } else {
        // Update the existing PeriodVersion
        return updatePeriodPeriodVersion(periodVersion, periodVersionDTO);
      }
    }).collect(Collectors.toSet());

    // Must use the same collection instance, or Hibernate will throw an exception
    // because the collection is already in the session.
    existingPeriod.getPeriodVersions().clear();
    existingPeriod.getPeriodVersions().addAll(updatedPeriodVersions);

    return periodRepository.save(existingPeriod);
  }

  /**
   * Get one period by id, with relations.
   *
   * @param  id the id of the entity.
   * @return    the entity.
   */
  @Transactional(readOnly = true)
  public Optional<PeriodDTO> findOneWithToOneRelationships(Long id) {
    log.debug("Request to get Period : {}", id);
    return periodRepository.findOneWithEagerRelationships(id).map(periodMapper::toDto);
  }

  private PeriodVersion findExistingPeriodVersion(List<PeriodVersion> periodVersions, Long id) {
    return periodVersions.stream().filter(item -> item.getId().equals(id)).findFirst().orElse(null);
  }

  private PeriodVersion updatePeriodPeriodVersion(PeriodVersion periodVersion, PeriodVersionDTO perdiotVersionDto) {

    periodVersion.setDescription(perdiotVersionDto.getDescription());
    periodVersion.setName(perdiotVersionDto.getName());
    return periodVersion;
  }

  @Transactional(readOnly = true)
  public List<PeriodDTO> findAllByState(PeriodStatus state) {
    log.debug("REST request to get all findAllByStatus : {}", state);
    //@formatter:off
    return periodRepository.findAllByState(state)
            .stream()
            .map(periodMapper::toDto).collect(Collectors.toCollection(LinkedList::new));
    //@formatter:on
  }

  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void saveWithNewTransaction(Period period) {
    log.debug("REST request to get all saveWithNewTransaction : {}", period);

    this.periodRepository.save(period);
  }
}
