package com.oguerreiro.resilient.repository;

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

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.NoRepositoryBean;

import com.oguerreiro.resilient.error.ResilientJpaMethodNotImplementedRuntimeException;

/**
 * Resilient base JPA Repository.<br>
 * All repositories (interfaces) should extend this {@link ResilientJpaRepository}. Simply extend like this:
 * 
 * <pre>
 * &#64;Repository
 * public interface MySampleRepository extends ResilientJpaRepository<MySample, Long> {
 * }
 * </pre>
 * <p>
 * This interface, defines the common functionality of the Resilient framework JpaRepositories. Extending the Jpa
 * interfaces:
 * <ul>
 * <li>JpaRepository - Common database methods. See {@link ResilientJpaRepository}</li>
 * <li>JpaSpecificationExecutor - Provides methods for advanced querying, using {@link Specification}. See
 * {@link JpaSpecificationExecutor}</li>
 * </ul>
 * </p>
 * 
 * <p>
 * Other repository method's:
 * <ul>
 * <li>{@link #findByIdAndVersion(Object, Integer)} - Searches using {@code ID} and {@code Version}. See
 * {@link #findByIdAndVersion(Object, Integer)}</li>
 * </ul>
 * </p>
 * 
 * @param <T>  the mapped entity domain class. Example: MySample
 * @param <ID> the entity domain key class type. Example: Long, assuming the id is a Long
 */
@NoRepositoryBean
public interface ResilientJpaRepository<T, ID> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T> {

  /**
   * Searches a record by {@code id} and {@code version}. Intended to get read a record from database with a given
   * version. This securely guarantees that the intended record is searched, OR, a concurrency colligion has happen.
   * 
   * @param  <S>     the return type of the repository. Example: MySample [ extends AbstractEntity<MySample, Long> ]
   * @param  id      the key of the domain
   * @param  version the record version, to guarantee that the version wasn't changed
   * @return
   */
  <S extends T> S findByIdAndVersion(ID id, Integer version);

  /* Eager loading */
  /**
   * Method used to fetch one domain by its key, with eager relations fetching (OneToOne relations only).<br>
   * Because JPA is mainly interface methods, this acts like an {@code abstract} method. Subclasses, must redeclare this
   * method providing the correct {@code @Query("")} for eager fetching.<br>
   * <p>
   * Example:
   * 
   * <pre>
   * <code>
   * &#64;Override
   * &#64;Query("select domain from DomainEntity domain left join fetch domain.oneToOneProperty where domain.id =:id")
   * Optional&lt;T&gt; findOneWithToOneRelationships(@Param("id") Long id);
   * </code>
   * </pre>
   * 
   * In this example, the fetched domain will have the property {@code oneToOneProperty} eagerly initialized.
   * </p>
   * 
   * <p>
   * IF the Domain entity have BAG relationships, the override is done to the default it self:
   * 
   * <pre>
   * <code>
   * &#64;Override
   * default Optional&lt;T&gt; findOneWithToOneRelationships(@Param("id") Long id) {
   *   return this.fetchBagRelationships(this.findById(id));
   * } 
   * </code>
   * </pre>
   * 
   * The {@code fetchBagRelationships} method its a JpaMethod defined in the DomainEntityRepository impl interface.
   * </p>
   * 
   * @param  id     the domain id to load
   * @return        its expected to return a entity domain instance (<T>) with all OneToOne relations eagerly
   *                initialized
   * @throws {@link ResilientJpaMethodNotImplementedRuntimeException} if the specific JPA interface for the
   *                <T>
   *                doesn't redefine this method.
   */
  default Optional<T> findOneWithEagerRelationships(ID id) {
    throw new ResilientJpaMethodNotImplementedRuntimeException(this.getClass().getSimpleName(),
        "findOneWithEagerRelationships");
  }

  /**
   * Method used to fetch all domain's, with eager relations fetching (OneToOne relations only).<br>
   * Because JPA is mainly interface methods, this acts like an {@code abstract} method. Subclasses, must redeclare this
   * method providing the correct {@code @Query("")} for eager fetching.<br>
   * <p>
   * Example:
   * 
   * <pre>
   * <code>
   * &#64;Override
   * &#64;Query("select domain from DomainEntity domain left join fetch domain.oneToOneProperty")
   * List&lt;T&gt; findAllWithEagerRelationships();
   * </code>
   * </pre>
   * 
   * <p>
   * IF the Entity Domain have BAG relationships, the override is done to the default it self:
   * 
   * <pre>
   * <code>
   * &#64;Override
   * default List&lt;T&gt; findAllWithEagerRelationships() {
   *   return this.fetchBagRelationships(this.findAll());
   * }
   * </code>
   * </pre>
   * 
   * The {@code fetchBagRelationships} method its a JpaMethod defined in the DomainEntityRepository impl interface.
   * </p>
   * 
   * In this example, the fetched domain collection, will have the property {@code oneToOneProperty} eagerly initialized
   * on each domain instance.
   * </p>
   * 
   * @return        its expected to return a list of <T> instance's with all OneToOne relations eagerly initialized
   * @throws {@link ResilientJpaMethodNotImplementedRuntimeException} if the specific JPA interface for the
   *                <T>
   *                doesn't redefine this method.
   */
  default List<T> findAllWithEagerRelationships() {
    throw new ResilientJpaMethodNotImplementedRuntimeException(this.getClass().getSimpleName(),
        "findAllWithEagerRelationships");
  }

  /**
   * Method used to fetch all domain's, with eager relations fetching (OneToOne relations only), with paging.<br>
   * Because JPA is mainly interface methods, this acts like an {@code abstract} method. Subclasses, must redeclare this
   * method providing the correct {@code @Query("")} for eager fetching.<br>
   * <p>
   * Example:
   * 
   * <pre>
   * <code>
   * &#64;Override
   * &#64;Query(value = "select domain from DomaimnEntity domain left join fetch domain.oneToOneProperty", countQuery = "select count(domain) from DomainEntity domain")
   * Page&lt;T&gt; findAllWithEagerRelationships(Pageable pageable);
   * </code>
   * </pre>
   * 
   * In this example, the fetched {@link Page}, will have it's <T> collection with the property
   * {@code oneToOneProperty} eagerly initialized.
   * on each domain instance.
   * </p>
   * <p>
   * IF the Entity Domain have BAG relationships, the override is done to the default it self:
   * 
   * <pre>
   * <code>
   * &#64;Override
   * default Page&lt;T&gt; findAllWithEagerRelationships(Pageable pageable) {
   *   return this.fetchBagRelationships(this.findAll(pageable));
   * }
   * </code>
   * </pre>
   * 
   * The {@code fetchBagRelationships} method its a JpaMethod defined in the DomainEntityRepository impl interface.
   * </p>
   * 
   * @return        its expected to return a {@link Page} with it's <T> collection instance's with all OneToOne
   *                relations eagerly initialized
   * @throws {@link ResilientJpaMethodNotImplementedRuntimeException} if the specific JPA interface for the
   *                <T>
   *                doesn't redefine this method.
   */
  default Page<T> findAllWithEagerRelationships(Pageable pageable) {
    throw new ResilientJpaMethodNotImplementedRuntimeException(this.getClass().getSimpleName(),
        "findAllWithEagerRelationships");
  }
}
