/*
 * Copyright (C) 2000 - 2024 Silverpeas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * As a special exception to the terms and conditions of version 3.0 of
 * the GPL, you may redistribute this Program in connection with Free/Libre
 * Open Source Software ("FLOSS") applications as described in Silverpeas's
 * FLOSS exception. You should have received a copy of the text describing
 * the FLOSS exception, and it is also available here:
 * "https://www.silverpeas.org/legal/floss_exception.html"
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
package org.silverpeas.components.resourcesmanager.service;

import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.silverpeas.components.resourcesmanager.model.*;
import org.silverpeas.core.ResourceReference;
import org.silverpeas.core.annotation.Service;
import org.silverpeas.core.contribution.attachment.AttachmentServiceProvider;
import org.silverpeas.core.contribution.attachment.model.SimpleDocument;
import org.silverpeas.core.contribution.content.form.RecordSet;
import org.silverpeas.core.contribution.template.publication.PublicationTemplate;
import org.silverpeas.core.contribution.template.publication.PublicationTemplateManager;
import org.silverpeas.core.date.period.Period;
import org.silverpeas.core.index.indexing.model.FullIndexEntry;
import org.silverpeas.core.index.indexing.model.IndexEngineProxy;
import org.silverpeas.core.index.indexing.model.IndexEntryKey;
import org.silverpeas.kernel.util.StringUtil;

import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class SimpleResourcesManager implements ResourcesManager, Serializable {

  private static final long serialVersionUID = -8053955818376554252L;
  private static final String TYPE = "Resource";
  @Inject
  private CategoryService categoryService;
  @Inject
  private ResourceService resourceService;
  @Inject
  private ReservationService reservationService;
  @Inject
  private ReservedResourceService reservedResourceService;

  /**
   * Creating a new resource category.
   * @param category the category to create
   */
  @Override
  @Transactional
  public void createCategory(Category category) {
    categoryService.createCategory(category);
    createCategoryIndex(category);
  }

  @Override
  public List<Category> getCategories(String instanceId) {
    return categoryService.getCategories(instanceId);
  }

  @Override
  public Category getCategory(Long id) {
    return categoryService.getCategory(id);
  }

  @Override
  @Transactional
  public void updateCategory(Category category) {
    categoryService.updateCategory(category);
  }

  @Override
  @Transactional
  public void deleteCategory(Long id, String componentId) {
    // First delete all resources of category
    List<Resource> resources = getResourcesByCategory(id);
    for (Resource resource : resources) {
      resourceService.deleteResource(resource.getIdAsLong());
      deleteIndex(resource.getIdAsLong(), TYPE, componentId);
    }
    // Then delete category itself
    categoryService.deleteCategory(id);
    deleteIndex(id, "Category", componentId);
  }

  @Override
  @Transactional
  public void createResource(Resource resource) {
    resourceService.createResource(resource);
    createResourceIndex(resource);
  }

  @Override
  public Resource getResource(Long id) {
    return resourceService.getResource(id);
  }

  @Override
  public List<Resource> getResourcesByCategory(Long categoryId) {
    return resourceService.getResourcesByCategory(categoryId);
  }

  @Override
  public List<Resource> getBookableResources(String componentId) {
    return resourceService.getBookableResources(componentId);
  }

  @Override
  @Transactional
  public void deleteResource(Long id, String componentId) {
    resourceService.deleteResource(id);
    deleteIndex(id, TYPE, componentId);
  }

  @Override
  public List<Resource> getResourcesReservable(String instanceId, Date startDate, Date endDate) {
    return resourceService.listAvailableResources(instanceId, String.valueOf(startDate.getTime()),
        String.valueOf(endDate.getTime()));
  }

  @Override
  public List<Resource> getResourcesOfReservation(String instanceId, Long reservationId) {
    return resourceService.listResourcesOfReservation(reservationId);
  }

  @Override
  @Transactional
  public void updateReservation(Reservation reservation, List<Long> resourceIds,
      boolean updateDate) {
    List<ReservedResource> reservedResources = reservedResourceService.
        findAllReservedResourcesOfReservation(reservation.getIdAsLong());
    Map<Long, ReservedResource> oldReservedResources = new HashMap<>(reservedResources.size());
    for (ReservedResource reservedResource : reservedResources) {
      oldReservedResources.put(reservedResource.getResourceId(), reservedResource);
    }
    String reservationStatus = processReservationModification(reservation, resourceIds,
        updateDate, oldReservedResources);
    for (ReservedResource oldReservedResource : oldReservedResources.values()) {
      reservedResourceService.delete(oldReservedResource);
    }
    reservation.setStatus(reservationStatus);
    reservationService.updateReservation(reservation);
    createReservationIndex(reservation);
  }

  private String processReservationModification(Reservation reservation, List<Long> resourceIds,
      boolean updateDate, Map<Long, ReservedResource> oldReservedResources) {
    boolean refused = false;
    boolean forValidation = false;
    String reservationStatus = ResourceStatus.STATUS_VALIDATE;
    for (Long resourceId : resourceIds) {
      ReservedResource reservedResource = oldReservedResources.remove(resourceId);
      boolean isCreation = (reservedResource == null);
      reservedResource = applyModification(reservation, updateDate, resourceId, isCreation,
          reservedResource);
      if (reservedResource.isValidationRequired()) {
        forValidation = true;
      }
      if (reservedResource.isRefused()) {
        refused = true;
      }
    }
    if (forValidation) {
      reservationStatus = ResourceStatus.STATUS_FOR_VALIDATION;
    }
    if (refused) {
      reservationStatus = ResourceStatus.STATUS_REFUSED;
    }
    return reservationStatus;
  }

  private ReservedResource applyModification(Reservation reservation, boolean updateDate,
      Long resourceId, boolean isCreation, ReservedResource reservedResource) {
    if (isCreation || updateDate) {
      if (reservedResource == null) {
        reservedResource = new ReservedResource();
        reservedResource.setReservedResourceId(Long.toString(resourceId), reservation.getId());
      }
      if (resourceService.isManager(Long.parseLong(reservation.getUserId()), resourceId)) {
        reservedResource.setStatus(ResourceStatus.STATUS_VALIDATE);
      } else if (resourceService.getManagers(resourceId).isEmpty()) {
        reservedResource.setStatus(ResourceStatus.STATUS_VALIDATE);
      } else {
        reservedResource.setStatus(ResourceStatus.STATUS_FOR_VALIDATION);
      }
      if (isCreation) {
        reservedResourceService.create(reservedResource);
      } else {
        reservedResource.setResource(null);
        reservedResource.setReservation(null);
        reservedResourceService.update(reservedResource);
      }
    }
    return reservedResource;
  }

  /**
   * Get from the given aimed resources those that are unavailable on the given period.
   * @param instanceId the current component instance identifier
   * @param resources the list of resource identifiers
   * @param startDate the start date
   * @param endDate the end date
   * @return a list of reserved resources
   */
  @Override
  public List<Resource> getReservedResources(String instanceId, List<Long> resources,
      Date startDate, Date endDate) {
    return getReservedResources(instanceId, resources, startDate, endDate, null);
  }

  /**
   * Get from the given aimed resources those that are unavailable on the given period. Resources
   * attached to reservationIdToSkip are excluded (but can still be returned if they are attached
   * to another reservation on the given period).
   * @param instanceId the current component instance identifier
   * @param aimedResourceIds the identifiers of the resources to get.
   * @param startDate the start date
   * @param endDate the end date
   * @param reservationIdToSkip the unique identifier of the reservation to skip.
   * @return a list of reserved resources.
   */
  @Override
  public List<Resource> getReservedResources(String instanceId, List<Long> aimedResourceIds,
      Date startDate, Date endDate, Long reservationIdToSkip) {
    String startPeriod = String.valueOf(startDate.getTime());
    String endPeriod = String.valueOf(endDate.getTime());
    return resourceService
        .findAllReservedResources((reservationIdToSkip != null ? reservationIdToSkip : -1),
            aimedResourceIds, startPeriod, endPeriod);
  }

  @Override
  public List<Reservation> getReservations(String instanceId) {
    return reservationService.findAllReservations(instanceId);
  }

  @Override
  public List<Reservation> getUserReservations(String instanceId, String userId) {
    return reservationService.findAllReservations(instanceId);
  }

  @Override
  public Reservation getReservation(String instanceId, Long reservationId) {
    return reservationService.getReservation(reservationId);
  }

  @Override
  @Transactional
  public void deleteReservation(Long id, String componentId) {
    deleteIndex(id, "Reservation", componentId);
    List<SimpleDocument> documents = AttachmentServiceProvider.getAttachmentService()
        .listDocumentsByForeignKey(new ResourceReference(id.toString(), componentId), null);
    for (SimpleDocument document : documents) {
      AttachmentServiceProvider.getAttachmentService().deleteAttachment(document);
    }
    reservationService.deleteReservation(id);
  }

  @Override
  public List<Reservation> getReservationForValidation(String instanceId, String userId,
      final Period period) {
    String[] searchPeriod = buildSearchPeriod(period);
    return reservationService
        .findAllReservationsForValidation(instanceId, Long.parseLong(userId), searchPeriod[0],
            searchPeriod[1]);
  }

  @Override
  public List<Reservation> getReservationOfUser(String instanceId, Integer userId,
      final Period period) {
    String[] searchPeriod = buildSearchPeriod(period);
    return reservationService
        .findAllReservationsInRange(instanceId, userId, searchPeriod[0], searchPeriod[1]);
  }

  @Override
  public List<Reservation> getReservationWithResourcesOfCategory(final String instanceId,
      Integer userId, final Period period, Long categoryId) {
    String[] searchPeriod = buildSearchPeriod(period);
    return reservationService
        .findAllReservationsForCategoryInRange(instanceId, userId, categoryId, searchPeriod[0],
            searchPeriod[1]);
  }

  @Override
  public List<Reservation> getReservationWithResource(final String instanceId, Integer userId,
      final Period period, final Long resourceId) {
    String[] searchPeriod = buildSearchPeriod(period);
    return reservationService
        .findAllReservationsForResourceInRange(instanceId, userId, resourceId, searchPeriod[0],
            searchPeriod[1]);
  }

  /**
   * Construct the period of search.
   * @param period the searched period
   */
  private String[] buildSearchPeriod(Period period) {
    return new String[]{String.valueOf(period.getBeginDate().getTime()),
        String.valueOf(period.getEndDate().getTime())};
  }

  @Override
  public String getResourceOfReservationStatus(Long resourceId, Long reservationId) {
    ReservedResource reserved =
        reservedResourceService.getReservedResource(resourceId, reservationId);
    return reserved.getStatus();
  }

  private void createCategoryIndex(Category category) {

    if (category != null) {
      FullIndexEntry indexEntry = new FullIndexEntry(
          new IndexEntryKey(category.getInstanceId(), "Category", category.getIdAsString()));
      indexEntry.setTitle(category.getName());
      indexEntry.setPreview(category.getDescription());
      if (category.getUpdateDate() != null) {
        indexEntry.setCreationDate(category.getUpdateDate());
      } else {
        indexEntry.setCreationDate(category.getCreationDate());
      }
      indexEntry.setCreationUser(category.getCreaterId());
      IndexEngineProxy.addIndexEntry(indexEntry);
    }
  }

  private void createResourceIndex(Resource resource) {
    if (resource != null) {
      // Index the Reservation
      FullIndexEntry indexEntry =
          new FullIndexEntry(new IndexEntryKey(resource.getInstanceId(), TYPE,
              resource.getIdAsString()));
      indexEntry.setTitle(resource.getName());
      indexEntry.setPreview(resource.getDescription());
      if (resource.getUpdateDate() != null) {
        indexEntry.setCreationDate(resource.getUpdateDate());
      } else {
        indexEntry.setCreationDate(resource.getCreationDate());
      }
      indexEntry.setCreationUser(resource.getCreaterId());
      Long categoryId = resource.getCategoryId();
      if (categoryId != null) {
        Category category = getCategory(categoryId);
        if (category != null) {
          String xmlFormName = category.getForm();
          if (StringUtil.isDefined(xmlFormName)) {
            indexResourceForm(resource, indexEntry, xmlFormName);
          }
        }
      }

      IndexEngineProxy.addIndexEntry(indexEntry);
    }
  }

  private void indexResourceForm(Resource resource, FullIndexEntry indexEntry, String xmlFormName) {
    String xmlFormShortName =
        xmlFormName.substring(xmlFormName.indexOf('/') + 1, xmlFormName.indexOf('.'));
    PublicationTemplate pubTemplate;
    try {
      pubTemplate = PublicationTemplateManager.getInstance().getPublicationTemplate(resource.
          getInstanceId() + ":" + xmlFormShortName);
      RecordSet set = pubTemplate.getRecordSet();
      set.indexRecord(String.valueOf(resource.getIdAsLong()), xmlFormName, indexEntry);
    } catch (Exception e) {
      throw new ResourcesManagerRuntimeException(e);
    }
  }

  private void createReservationIndex(Reservation reservation) {
    if (reservation != null) {
      FullIndexEntry indexEntry = new FullIndexEntry(new IndexEntryKey(reservation.getInstanceId(),
          "Reservation", reservation.getIdAsString()));
      indexEntry.setTitle(reservation.getEvent());
      indexEntry.setPreview(reservation.getReason());
      indexEntry.setCreationDate(reservation.getCreationDate());
      indexEntry.setCreationUser(reservation.getUserId());
      indexEntry.setKeywords(reservation.getPlace());
      IndexEngineProxy.addIndexEntry(indexEntry);
    }
  }

  private void deleteIndex(Long objectId, String objectType, String componentId) {
    if (objectId != null) {
      IndexEntryKey indexEntry = new IndexEntryKey(componentId, objectType,
          String.valueOf(objectId));
      IndexEngineProxy.removeIndexEntry(indexEntry);
    }
  }

  @Override
  public void indexResourceManager(String instanceId) {
    List<Reservation> listOfReservation = getReservations(instanceId);
    if (listOfReservation != null) {
      for (Reservation reservation : listOfReservation) {
        try {
          createReservationIndex(reservation);
        } catch (Exception e) {
          throw new ResourcesManagerRuntimeException(e);
        }
      }
    }
  }

  @Override
  public List<ResourceValidator> getManagers(long resourceId) {
    return resourceService.getManagers(resourceId);
  }

  @Override
  @Transactional
  public void saveReservation(Reservation reservation, List<Long> resourceIds) {
    reservationService.createReservation(reservation, resourceIds);
    createReservationIndex(reservation);
  }

  @Override
  @Transactional
  public void updateReservedResourceStatus(long reservationId, long resourceId, String status) {
    ReservedResource reservedResource =
        reservedResourceService.getReservedResource(resourceId, reservationId);
    if (reservedResource != null) {
      reservedResource.setStatus(status);
      reservedResourceService.update(reservedResource);
      Reservation reservation = reservedResource.getReservation();
      reservation.setStatus(reservationService.computeReservationStatus(reservation));
      reservationService.updateReservation(reservation);
    }
  }

  @Override
  public boolean isManager(long userId, long resourceId) {
    return resourceService.isManager(userId, resourceId);
  }

  @Override
  @Transactional
  public void updateResource(Resource updatedResource, List<Long> managerIds) {
    Resource resource = getResource(updatedResource.getIdAsLong());
    resource.merge(updatedResource);
    resource.getManagers().clear();
    for (Long managerId : managerIds) {
      ResourceValidator validator = new ResourceValidator(resource.getIdAsLong(), managerId);
      if (!resource.getManagers().contains(validator)) {
        resource.getManagers().add(validator);
      }
    }
    resourceService.updateResource(resource);
    createResourceIndex(resource);
  }
}
