package com.oguerreiro.resilient.service;

import java.io.Serializable;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.oguerreiro.resilient.domain.AbstractResilientEntity;
import com.oguerreiro.resilient.repository.ResilientJpaRepository;
import com.oguerreiro.resilient.service.dto.AbstractResilientDTO;
import com.oguerreiro.resilient.service.interceptors.AfterSaveServiceInterceptor;
import com.oguerreiro.resilient.service.interceptors.AfterUpdateServiceInterceptor;
import com.oguerreiro.resilient.service.interceptors.BeforeDeleteServiceInterceptor;
import com.oguerreiro.resilient.service.interceptors.BeforeSaveServiceInterceptor;
import com.oguerreiro.resilient.service.interceptors.BeforeUpdateServiceInterceptor;
import com.oguerreiro.resilient.service.mapper.ResilientEntityMapper;

import jakarta.persistence.EntityManager;

/**
 * 
 * @param <DTO>, the domain entity DTO class, eg. DomainDTO
 * @param <T>,   the domain entity class type, eg. Domain
 * @param <ID>,  the domain entity key type, eg. Long
 */
@Service
@Transactional
public abstract class AbstractResilientService<DTO extends AbstractResilientDTO<ID>, T extends AbstractResilientEntity<T, ID>, ID extends Serializable> {
  protected final Logger log = LoggerFactory.getLogger(this.getClass());

  private String domainSimpleName;

  private final ResilientJpaRepository<T, ID> domainRepository;
  private final ResilientEntityMapper<DTO, T> domainMapper;
  private final Class<T> domainClass;  // This is a best practice to get the domain class. To infer from the generic (DOMAIN) is VERY hard. 

  @Autowired
  private EntityManager entityManager;

  protected AbstractResilientService(Class<T> domainClass, ResilientJpaRepository<T, ID> domainRepository,
      ResilientEntityMapper<DTO, T> domainMapper) {
    this.domainRepository = domainRepository;
    this.domainMapper = domainMapper;
    this.domainClass = domainClass;

    // Set some sugar values
    this.domainSimpleName = this.domainClass.getSimpleName(); //Used in logging
  }

  protected ResilientJpaRepository<T, ID> getRepository() {
    return this.domainRepository;
  }

  protected EntityManager getEntityManager() {
    return this.entityManager;
  }

  /**
   * Save a Entity Domain.
   *
   * @param  domainDTO the entity to save.
   * @return           the persisted entity.
   */
  public DTO save(DTO domainDTO) {
    log.debug("Request to save " + this.domainSimpleName + " : {}", domainDTO);
    T domain = domainMapper.toEntity(domainDTO);
    domain = this.callBeforeSaveInterceptor(domain);
    domain = domainRepository.save(domain);
    domain = this.callAfterSaveInterceptor(domain);
    return domainMapper.toDto(domain);
  }

  private T callBeforeSaveInterceptor(T domain) {
    if (this instanceof BeforeSaveServiceInterceptor) {
      return ((BeforeSaveServiceInterceptor<DTO, T, ID>) this).beforeSave(domain);
    }
    return domain;
  }

  private T callAfterSaveInterceptor(T domain) {
    if (this instanceof AfterSaveServiceInterceptor) {
      return ((AfterSaveServiceInterceptor<DTO, T, ID>) this).afterSave(domain);
    }
    return domain;
  }

  /**
   * Update a Entity Domain.
   *
   * @param  domainDTO the entity to save.
   * @return           the persisted entity.
   */
  public DTO update(DTO domainDTO) {
    log.debug("Request to update " + this.domainSimpleName + " : {}", domainDTO);
    T domain = domainMapper.toEntity(domainDTO);
    domain = this.callBeforeUpdateInterceptor(domain);
    domain = domainRepository.save(domain);
    domain = this.callAfterUpdateInterceptor(domain);
    return domainMapper.toDto(domain);
  }

  private T callBeforeUpdateInterceptor(T domain) {
    if (this instanceof BeforeUpdateServiceInterceptor) {
      return ((BeforeUpdateServiceInterceptor<DTO, T, ID>) this).beforeUpdate(domain);
    }
    return domain;
  }

  private T callAfterUpdateInterceptor(T domain) {
    if (this instanceof AfterUpdateServiceInterceptor) {
      return ((AfterUpdateServiceInterceptor<DTO, T, ID>) this).afterUpdate(domain);
    }
    return domain;
  }

  /**
   * Partially update a entity of type <T>.
   *
   * @param  domainDTO the entity to update partially.
   * @return           the persisted entity.
   */
  public Optional<DTO> partialUpdate(DTO domainDTO) {
    log.debug("Request to partially update " + this.domainSimpleName + " : {}", domainDTO);

    return domainRepository.findById(domainDTO.getId()).map(existingDomain -> {
      domainMapper.partialUpdate(existingDomain, domainDTO);

      return existingDomain;
    }).map(domainRepository::save).map(domainMapper::toDto);
  }

  /**
   * Get all the entities of type <T> wrapped in a DTO of type <DTO>.
   *
   * @return the list of entities.
   */
  @Transactional(readOnly = true)
  public List<DTO> findAll() {
    log.debug("Request to get all " + this.domainSimpleName + "'s.");
    return this.findAll(Sort.unsorted());
  }

  /**
   * Get all the entities of type <T> wrapped in a DTO of type <DTO>. Sorted by the sort parameter.
   *
   * @return the list of entities.
   */
  @Transactional(readOnly = true)
  public List<DTO> findAll(Sort sort) {
    log.debug("Request to get all " + this.domainSimpleName + "'s. Sorted by:", sort);
    return domainRepository.findAll(sort).stream().map(domainMapper::toDto).collect(
        Collectors.toCollection(LinkedList::new));
  }

  /**
   * Get one entity by id, of type <T> wrapped in a DTO of type <DTO>.
   *
   * @param  id the id of the entity.
   * @return    the entity.
   */
  @Transactional(readOnly = true)
  public Optional<DTO> findOne(ID id) {
    log.debug("Request to get " + this.domainSimpleName + " : {}", id);
    Optional<T> entity = domainRepository.findById(id);
    Optional<DTO> dto = entity.map(domainMapper::toDto);
    return dto;
  }

  /**
   * Delete the Entity Domain by id.
   *
   * @param id the id of the entity.
   */
  public void delete(ID id) {
    log.debug("Request to delete " + this.domainSimpleName + " : {}", id);
    this.callBeforeDeleteInterceptor(id);
    domainRepository.deleteById(id);
  }

  private void callBeforeDeleteInterceptor(ID id) {
    if (this instanceof BeforeDeleteServiceInterceptor) {
      ((BeforeDeleteServiceInterceptor<DTO, T, ID>) this).beforeDelete(id);
    }
  }

  /* Eager relationships */
  /**
   * Get all the emissionFactors with eager load of many-to-many relationships.
   *
   * @return the list of entities.
   */
  public Page<DTO> findAllWithEagerRelationships(Pageable pageable) {
    log.debug("Request to findAllWithEagerRelationships : {}", pageable);
    return domainRepository.findAllWithEagerRelationships(pageable).map(domainMapper::toDto);
  }

  /**
   * Get one emissionFactor by id.
   *
   * @param  id the id of the entity.
   * @return    the entity.
   */
  @Transactional(readOnly = true)
  public Optional<DTO> findOneWithEagerRelationships(ID id) {
    log.debug("Request to findOneWithEagerRelationships : {}", id);
    return domainRepository.findOneWithEagerRelationships(id).map(domainMapper::toDto);
  }
}
