/*
 *
 *  *
 *  * Copyright 2020 First People's Cultural Council
 *  *
 *  * Licensed under the Apache License, Version 2.0 (the "License");
 *  * you may not use this file except in compliance with the License.
 *  * You may obtain a copy of the License at
 *  *
 *  *     http://www.apache.org/licenses/LICENSE-2.0
 *  *
 *  * Unless required by applicable law or agreed to in writing, software
 *  * distributed under the License is distributed on an "AS IS" BASIS,
 *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  * See the License for the specific language governing permissions and
 *  * limitations under the License.
 *  * /
 *
 */

package ca.firstvoices.publisher.services;

import static ca.firstvoices.data.lifecycle.Constants.ENABLED_STATE;
import static ca.firstvoices.data.lifecycle.Constants.PUBLISHED_STATE;
import static ca.firstvoices.data.lifecycle.Constants.PUBLISH_TRANSITION;
import static ca.firstvoices.data.lifecycle.Constants.UNPUBLISH_TRANSITION;
import static ca.firstvoices.data.schemas.DialectTypesConstants.FV_ALPHABET;
import static ca.firstvoices.data.schemas.DialectTypesConstants.FV_AUDIO;
import static ca.firstvoices.data.schemas.DialectTypesConstants.FV_BOOK;
import static ca.firstvoices.data.schemas.DialectTypesConstants.FV_BOOK_ENTRY;
import static ca.firstvoices.data.schemas.DialectTypesConstants.FV_CATEGORY;
import static ca.firstvoices.data.schemas.DialectTypesConstants.FV_CHARACTER;
import static ca.firstvoices.data.schemas.DialectTypesConstants.FV_GALLERY;
import static ca.firstvoices.data.schemas.DialectTypesConstants.FV_LABEL;
import static ca.firstvoices.data.schemas.DialectTypesConstants.FV_LINK;
import static ca.firstvoices.data.schemas.DialectTypesConstants.FV_PHRASE;
import static ca.firstvoices.data.schemas.DialectTypesConstants.FV_PICTURE;
import static ca.firstvoices.data.schemas.DialectTypesConstants.FV_PORTAL;
import static ca.firstvoices.data.schemas.DialectTypesConstants.FV_VIDEO;
import static ca.firstvoices.data.schemas.DialectTypesConstants.FV_WORD;
import static ca.firstvoices.data.schemas.DomainTypesConstants.FV_DIALECT;
import static ca.firstvoices.data.schemas.DomainTypesConstants.FV_SHARED_DATA_NAME;

import ca.firstvoices.core.io.services.TransitionChildrenStateService;
import ca.firstvoices.core.io.utils.DialectUtils;
import ca.firstvoices.core.io.utils.PropertyUtils;
import ca.firstvoices.core.io.utils.StateUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.IdRef;
import org.nuxeo.ecm.core.api.PropertyException;
import org.nuxeo.ecm.core.api.model.Property;
import org.nuxeo.ecm.core.lifecycle.LifeCycleService;
import org.nuxeo.runtime.api.Framework;

/**
 * @author loopingz
 */
public class FirstVoicesPublisherServiceImpl implements FirstVoicesPublisherService {

  private static final Log log = LogFactory.getLog(FirstVoicesPublisherServiceImpl.class);

  private static final String MEDIA_ORIGIN_FIELD = "fvmedia:origin";

  private final TransitionChildrenStateService transitionChildrenService = Framework
      .getService(TransitionChildrenStateService.class);

  private CoreSession session;

  //================================================================================
  // TRANSITION METHODS
  // These do not deal with proxies; simply with transitioning state
  //================================================================================

  @Override
  public void transitionDialectToPublished(CoreSession session, DocumentModel dialect) {
    this.session = session;

    // Transition the dialect to the PUBLISHED state
    // This event will not be handled by publisher listener if this was executed via
    // the PublishDialect endpoint. Otherwise, consider that it will both TRANSITION and
    // create proxies
    if (dialect.getAllowedStateTransitions().contains(PUBLISH_TRANSITION)) {
      dialect.followTransition(PUBLISH_TRANSITION);
    } else {
      log.warn(
          String.format("Tried to publish document that is in an unpublishable state (%s)",
              dialect.getCurrentLifeCycleState()));
      return;
    }

    for (DocumentModel child : session.getChildren(dialect.getRef())) {
      if (StateUtils.followTransitionIfAllowed(child, PUBLISH_TRANSITION)) {
        List<String> nonRecursiveTransitions = Framework
            .getService(LifeCycleService.class)
            .getNonRecursiveTransitionForDocType(child.getType());

        if (nonRecursiveTransitions.contains(PUBLISH_TRANSITION)) {
          // Handle publishing children if type is not configured to do that automatically
          // as defined by `noRecursionForTransitions` on the type.
          if (FV_ALPHABET.equals(child.getType())) {
            // For alphabet we want to transition all characters to publish
            transitionChildrenService.transitionChildren(PUBLISH_TRANSITION, null, child);
          } else {
            // For all other types, we only want to transition enabled
            transitionChildrenService.transitionChildren(PUBLISH_TRANSITION, ENABLED_STATE, child);
          }
        }
      }
    }
  }

  //================================================================================
  // PUBLISH METHODS
  // These will handle creating proxies.
  // Generally, triggered by a lifecycle state change
  //================================================================================

  @Override
  public DocumentModel publish(CoreSession session, DocumentModel doc) {
    this.session = session;

    boolean isDialect = DialectUtils.isDialect(doc);
    DocumentModel dialect = DialectUtils.getDialect(session, doc);

    if (!isDialect && !StateUtils.isPublished(dialect)) {
      log.error(String
          .format("Tried to publish a `%s` type in a dialect that is not published, doc id: %s",
              doc.getType(),
              doc.getId()));
      return null;
    }

    if (isDialect) {
      publishDialectParents(doc);
      publishDependencies(doc, null);
    } else if (FV_PORTAL.equals(doc.getType())) {
      publishDependencies(doc, null);
    } else if (isPublishableAsset(doc.getType())) {
      publishDependencies(doc, Collections.singletonList(MEDIA_ORIGIN_FIELD));

      if (FV_BOOK.equals(doc.getType())) {
        // For books, transition all direct children to publish
        // These events will be picked up by the listener
        // Better to do this here, then rely on BulkLifeCycleChangeListener
        // that may introduce the bug FW-1967
        transitionChildrenService.transitionChildren(PUBLISH_TRANSITION, null, doc);
      }
    }

    return doc;
  }

  private void publishDialectParents(DocumentModel dialect) {

    DocumentModel language = session.getParentDocument(dialect.getRef());
    DocumentModel languageFamily = session.getParentDocument(language.getRef());

    StateUtils.followTransitionIfAllowed(language, PUBLISH_TRANSITION);
    StateUtils.followTransitionIfAllowed(languageFamily, PUBLISH_TRANSITION);
  }


  /**
   * Iterates over dependencies (IDs referenced in fields such as `related_audio`) and publishes
   * those dependencies. The resulting ID of the proxy is assigned to the correct proxy field based
   * on the dependencies map. Can handle both single references and arrays.
   *
   * @param doc                        the proxy to create values on (a proxy is a copy of a
   *                                     Workspace document in a sections)
   * @param dependenciesToSkipPublishing optionally dependencies that should not be published
   */
  private void publishDependencies(DocumentModel doc, List<String> dependenciesToSkipPublishing) {

    ArrayList<String> schemasToEvaluate = new ArrayList<>();
    schemasToEvaluate.add("fvdialect");

    if (dependenciesToSkipPublishing == null) {
      dependenciesToSkipPublishing = new ArrayList<>();
    }

    // Check differences in schemas (using conversion to JSON)
    for (String schema : doc.getSchemas()) {
      if (!schemasToEvaluate.contains(schema)) {
        continue;
      }

      Map<String, Object> properties = doc.getProperties(schema);

      for (Entry<String, Object> entry : properties.entrySet()) {
        String dependencyEntry = entry.getKey();

        if (dependenciesToSkipPublishing.contains(entry.getKey())) {
          continue;
        }

        try {

          Property workspaceProperty = doc.getProperty(entry.getKey());

          if (!PropertyUtils.isEmpty(doc, dependencyEntry)) {
            // If workspace field has a value, handle list/non list accordingly
            Set<String> workspacePropertyValues;

            if (workspaceProperty.isList() && !workspaceProperty.isContainer()) {
              workspacePropertyValues =
                  new LinkedHashSet<>(PropertyUtils.getValuesAsList(doc, dependencyEntry));
            } else if (workspaceProperty.isContainer()) {
              // Skip complex lists (containers)
              continue;
            } else {
              workspacePropertyValues = new LinkedHashSet<>(
                  Collections.singleton(String.valueOf(workspaceProperty.getValueForWrite())));
            }

            for (String workspaceDependencyId : workspacePropertyValues) {
              IdRef dependencyRef = new IdRef(workspaceDependencyId);

              if (!session.exists(dependencyRef) || session.isTrashed(dependencyRef)) {
                // Skip if workspace dependency document is trashed or non-existent
                // This will also skip dependencies that are not GUIDs (i.e. represent linked docs)
                continue;
              }

              DocumentModel dependencyDoc = session.getDocument(dependencyRef);

              if (!dependenciesToSkipPublishing.contains(dependencyEntry) && DialectUtils.isDialectGrandChild(dependencyDoc)
                  && !dependencyDoc.getPathAsString().contains(FV_SHARED_DATA_NAME)) {
                // Publish dependency
                StateUtils.followTransitionIfAllowed(dependencyDoc, PUBLISH_TRANSITION);
              }
            }
          }
        } catch (PropertyException e) {
          log.warn(
              String.format("Could not publish dependency property %s while publishing parent %s of type %s",
                  dependencyEntry, doc.getId(), doc.getType()));
        }
      }
    }
  }

  //================================================================================
  // UNPUBLISH METHODS
  // These will handle a mixture of transitions and removing proxies
  //================================================================================

  @Override
  public void unpublish(DocumentModel doc) {
    if (doc == null) {
      return;
    }

    if (doc.getCoreSession() != null) {
      this.session = doc.getCoreSession();
    }

    if (FV_DIALECT.equals(doc.getType())) {
      // Transition the state of the children
      transitionChildrenService.transitionChildren(UNPUBLISH_TRANSITION, PUBLISHED_STATE, doc);
    } else if (isPublishableAsset(doc.getType())) {
      unpublishAsset(doc);
    } else if (DialectUtils.isDialectChild(doc)) {
      // Transition all children that are in the published state
      transitionChildrenService.transitionChildren(UNPUBLISH_TRANSITION, PUBLISHED_STATE, doc);
    }
  }

  /**
   * Remove an asset proxy; won't clear proxies for related assets since they may be used by other
   * assets.
   * Potentially remove dependencies ????
   */
  public void unpublishAsset(DocumentModel asset) {
    // potentially unpublish depdendencies that are only linked to this asset??
  }

  //================================================================================
  // GENERAL HELPER METHODS
  //================================================================================

  /**
   * @return true if the asset should be handled by the asset publishing mechanism
   */
  private boolean isPublishableAsset(String type) {
    return FV_BOOK_ENTRY.equals(type) || FV_BOOK.equals(type) || FV_PHRASE.equals(type)
        || FV_WORD.equals(type) || FV_LABEL.equals(type) || FV_PICTURE.equals(type) || FV_VIDEO
        .equals(type) || FV_AUDIO.equals(type) || FV_CATEGORY.equals(type) || FV_CHARACTER
        .equals(type) || FV_GALLERY.equals(type) || FV_LINK.equals(type);
  }
}