package com.oguerreiro.resilient.web.rest;

import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

import com.oguerreiro.resilient.domain.AbstractResilientEntity;
import com.oguerreiro.resilient.domain.DashboardComponent;
import com.oguerreiro.resilient.repository.ResilientJpaRepository;
import com.oguerreiro.resilient.service.AbstractResilientService;
import com.oguerreiro.resilient.service.dto.AbstractResilientDTO;
import com.oguerreiro.resilient.web.rest.errors.BadRequestAlertException;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import tech.jhipster.web.util.HeaderUtil;
import tech.jhipster.web.util.ResponseUtil;

/**
 * Resilient abstract base resource.<br>
 * All REST endpoints should extend this {@link AbstractResilientResource}. Implementation looks like:
 * 
 * <pre>
 * &#64;RestController
 * &#64;RequestMapping("/api/my-sample")
 * public class MySampleResource extends AbstractResource<MySample, MySampleDTO, Long> {
 *   private static final String ENTITY_NAME = "mySample";
 * 
 *   public MySampleResource(MySampleRepository mySampleRepository, MySampleService mySampleService) {
 *     super(MySample.class, mySampleRepository, mySampleService);
 *   }
 * 
 *   &#64;Override
 *   protected String getEntityName() {
 *     return ENTITY_NAME;
 *   }
 * }
 * </pre>
 * 
 * The resource implementation class, will define the base URI mapping ({@code @RequestMapping("/api/my-sample")}).<br>
 * The {@link AbstractResilientResource } provides REST basic operations (CRUD):
 * <h3>REST Base Endpoints</h3>
 * <p>
 * <table border="1">
 * <tr>
 * <th>URI</th>
 * <th>VERB</th>
 * <th>Description</th>
 * </tr>
 * <tr>
 * <td></td>
 * <td>GET</td>
 * <td>List endpoint. See {@link AbstractResilientResource#getAll()}.</td>
 * </tr>
 * <td></td>
 * <td>POST</td>
 * <td>Create endpoint. See {@link AbstractResilientResource#create(AbstractResilientDTO)}.</td>
 * </tr>
 * <tr>
 * <td>/{id}</td>
 * <td>PUT</td>
 * <td>Update endpoint. See {@link AbstractResilientResource#update(Serializable, AbstractResilientDTO)}.</td>
 * </tr>
 * <tr>
 * <td>/{id}</td>
 * <td>PATCH</td>
 * <td>Partial Update endpoint. See
 * {@link AbstractResilientResource#partialUpdate(Serializable, AbstractResilientDTO)}.</td>
 * </tr>
 * <tr>
 * <td>/{id}</td>
 * <td>GET</td>
 * <td>Read endpoint. See {@link AbstractResilientResource#getEntity(Serializable)}.</td>
 * </tr>
 * <tr>
 * <td>/{id}</td>
 * <td>DELETE</td>
 * <td>Delete endpoint. See {@link AbstractResilientResource#delete(Serializable)}.</td>
 * </tr>
 * </table>
 * </p>
 * 
 * @param <T>   the mapped entity domain class type. Example: MySample
 * @param <DTO> the entity domain DTO class, for <T>. Example: MySampleDTO
 * @param <ID>  the entity domain key class type. Example: Long, assuming the id is a Long
 */
public abstract class AbstractResilientResource<T extends AbstractResilientEntity<T, ID>, DTO extends AbstractResilientDTO<ID>, ID extends Serializable> {
  protected final Logger log = LoggerFactory.getLogger(this.getClass());

  // Constants to enforce public knowledge of Abstract endpoint method names. 
  public static final String ENDPOINT_METHOD_NAME_CREATE = "create";
  public static final String ENDPOINT_METHOD_NAME_READ = "getEntity";
  public static final String ENDPOINT_METHOD_NAME_UPDATE = "update";
  public static final String ENDPOINT_METHOD_NAME_PARTIAL = "partialUpdate";
  public static final String ENDPOINT_METHOD_NAME_DELETE = "delete";
  public static final String ENDPOINT_METHOD_NAME_LIST = "getAll";
  public static final String ENDPOINT_METHOD_NAME_CRITERIA_LIST = "getAllByCriteria";

  @Value("${jhipster.clientApp.name}")
  private String applicationName;

  private String domainSimpleName;
  private String requestMappingDefaultURI;

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

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

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

  protected final AbstractResilientService<DTO, T, ID> getService() {
    return this.domainService;
  }

  /* *** Abstract methods *** */
  /* ********************************************************************************* */
  /**
   * Provide a functional name for the entity. Usually the domain class name, cammel cased, starting with lowercase.
   * Example, for domain the {@link DashboardComponent} should return 'dashboardComponent'. This is a suggestion only.
   * 
   * @return a String with the functional entity name
   */
  protected abstract String getEntityName();

  /* *** Public methods (server side) *** */
  /* ********************************************************************************* */
  /**
   * The domain which this Resource provides endpoints for. DON'T be confused with getClass() that will return the Class
   * of {@code this} instance.
   * 
   * @return
   */
  public Class<T> getDomainClass() {
    return this.domainClass;
  }

  /* *** STANDARD REST endpoints *** */
  /* ********************************************************************************* */
  /**
   * {@code POST  /<entity path>} : Create a new entity.
   *
   * @param  domainDTO          the domainDTO to create.
   * @return                    the {@link ResponseEntity} with status {@code 201 (Created)} and with body the new
   *                            domainDTO, or with status {@code 400 (Bad Request)} if the domain has already an ID.
   * @throws URISyntaxException if the Location URI syntax is incorrect.
   */
  @PreAuthorize("@resilientSecurity.canAccess()")
  @PostMapping("")
  public ResponseEntity<DTO> create(@Valid @RequestBody DTO domainDTO) throws URISyntaxException {
    log.debug("REST request to save " + this.domainSimpleName + " : {}", domainDTO);
    if (domainDTO.getId() != null) {
      throw new BadRequestAlertException("A new " + this.domainSimpleName + " cannot already have an ID",
          this.getEntityName(), "idexists");
    }
    domainDTO = domainService.save(domainDTO);
    return ResponseEntity.created(new URI(getRequestMappingDefaultURI() + domainDTO.getId())).headers(
        HeaderUtil.createEntityCreationAlert(applicationName, true, this.getEntityName(),
            domainDTO.getId().toString())).body(domainDTO);
  }

  /**
   * {@code PUT  /<entity path>/:id} : Updates an existing entity.
   *
   * @param  id                 the id of the domainDTO to save.
   * @param  domainDTO          the domainDTO to update.
   * @return                    the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated
   *                            domainDTO,
   *                            or with status {@code 400 (Bad Request)} if the domainDTO is not valid,
   *                            or with status {@code 500 (Internal Server Error)} if the domainDTO couldn't be
   *                            updated.
   * @throws URISyntaxException if the Location URI syntax is incorrect.
   */
  @PreAuthorize("@resilientSecurity.canAccess()")
  @PutMapping("/{id}")
  public ResponseEntity<DTO> update(@PathVariable(value = "id", required = false) final ID id,
      @Valid @RequestBody DTO domainDTO) throws URISyntaxException {
    log.debug("REST request to update " + this.domainSimpleName + " : {}, {}", id, domainDTO);
    if (domainDTO.getId() == null) {
      throw new BadRequestAlertException("Invalid id", getEntityName(), "idnull");
    }
    if (!Objects.equals(id, domainDTO.getId())) {
      throw new BadRequestAlertException("Invalid ID", getEntityName(), "idinvalid");
    }

    if (!domainRepository.existsById(id)) {
      throw new BadRequestAlertException("Entity not found", getEntityName(), "idnotfound");
    }

    domainDTO = domainService.update(domainDTO);
    return ResponseEntity.ok().headers(HeaderUtil.createEntityUpdateAlert(applicationName, true, this.getEntityName(),
        domainDTO.getId().toString())).body(domainDTO);
  }

  /**
   * {@code PATCH  /<entity path>/:id} : Partial updates given fields of an existing entity, field will ignore if it is
   * null
   *
   * @param  id                 the id of the domainDTO to save.
   * @param  domainDTO          the domainDTO to update.
   * @return                    the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated
   *                            domainDTO,
   *                            or with status {@code 400 (Bad Request)} if the domainDTO is not valid,
   *                            or with status {@code 404 (Not Found)} if the domainDTO is not found,
   *                            or with status {@code 500 (Internal Server Error)} if the domainDTO couldn't be
   *                            updated.
   * @throws URISyntaxException if the Location URI syntax is incorrect.
   */
  @PreAuthorize("@resilientSecurity.canAccess()")
  @PatchMapping(value = "/{id}", consumes = { "application/json", "application/merge-patch+json" })
  public ResponseEntity<DTO> partialUpdate(@PathVariable(value = "id", required = false) final ID id,
      @NotNull @RequestBody DTO domainDTO) throws URISyntaxException {
    log.debug("REST request to partial update " + this.domainSimpleName + " partially : {}, {}", id, domainDTO);
    if (domainDTO.getId() == null) {
      throw new BadRequestAlertException("Invalid id", getEntityName(), "idnull");
    }
    if (!Objects.equals(id, domainDTO.getId())) {
      throw new BadRequestAlertException("Invalid ID", getEntityName(), "idinvalid");
    }

    if (!domainRepository.existsById(id)) {
      throw new BadRequestAlertException("Entity not found", getEntityName(), "idnotfound");
    }

    Optional<DTO> result = domainService.partialUpdate(domainDTO);

    return ResponseUtil.wrapOrNotFound(result,
        HeaderUtil.createEntityUpdateAlert(applicationName, true, getEntityName(), domainDTO.getId().toString()));
  }

  /**
   * {@code GET  /<entity path>} : get all the entity.
   *
   * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of entities in body.
   */
  @PreAuthorize("@resilientSecurity.canAccess()")
  @GetMapping("")
  public List<DTO> getAll() {
    log.debug("REST request to get all " + this.domainSimpleName + "'s.");
    return domainService.findAll(this.getAllSort());
  }

  /**
   * Override this to provide a ORDER BY for the getAll endpoint.
   * Example: return Sort.by(Sort.Order.asc("name"), Sort.Order.desc("id"));
   * 
   * @return
   */
  protected Sort getAllSort() {
    return Sort.unsorted();
  }

  /**
   * {@code GET  /<entity path>/:id} : get entity by the "id".
   *
   * @param  id the id of the domainDTO to retrieve.
   * @return    the {@link ResponseEntity} with status {@code 200 (OK)} and with body the domainDTO, or with status
   *            {@code 404 (Not Found)}.
   */
  @PreAuthorize("@resilientSecurity.canAccess()")
  @GetMapping("/{id}")
  public ResponseEntity<DTO> getEntity(@PathVariable("id") ID id) {
    log.debug("REST request to get " + this.domainSimpleName + " : {}", id);
    Optional<DTO> domainDTO = domainService.findOne(id);
    return ResponseUtil.wrapOrNotFound(domainDTO);
  }

  /**
   * {@code DELETE  /<entity path>/:id} : delete entity by the "id".
   *
   * @param  id the id of the domainDTO to delete.
   * @return    the {@link ResponseEntity} with status {@code 204 (NO_CONTENT)}.
   */
  @PreAuthorize("@resilientSecurity.canAccess()")
  @DeleteMapping("/{id}")
  public ResponseEntity<Void> delete(@PathVariable("id") ID id) {
    log.debug("REST request to delete " + this.domainSimpleName + " : {}", id);
    domainService.delete(id);
    return ResponseEntity.noContent().headers(
        HeaderUtil.createEntityDeletionAlert(applicationName, true, getEntityName(), id.toString())).build();
  }

  /**
   * Dynamically extracts the endpoint URI from the @RequestMapping, needed for responses,
   * <p>
   * For the next example, it will return : "/api/dashboard-components".
   * 
   * <pre>
   * <code>
   * &#64;RestController
   * &#64;RequestMapping("/api/dashboard-components")
   * public class DashboardComponentResource extends AbstractResource<DashboardComponent,
   *                 DashboardComponentDTO, Long> {
   * </code>
   * </pre>
   * </p>
   */
  private String getRequestMappingDefaultURI() {
    if (this.requestMappingDefaultURI == null) {
      RequestMapping requestMapping = this.getClass().getAnnotation(RequestMapping.class);
      if (requestMapping == null || requestMapping.path() == null || requestMapping.path().length == 0) {
        this.requestMappingDefaultURI = "";
      } else {
        this.requestMappingDefaultURI = requestMapping.path()[0];
      }
    }

    return this.requestMappingDefaultURI;
  }
}
