/*
 * Copyright 2018 Red Hat, Inc. and/or its affiliates.
 *
 * 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 org.jbpm.process.workitem.jpa;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.persistence.Query;

import org.jbpm.process.workitem.core.AbstractLogOrThrowWorkItemHandler;
import org.jbpm.process.workitem.core.util.RequiredParameterValidator;
import org.jbpm.process.workitem.core.util.Wid;
import org.jbpm.process.workitem.core.util.WidMavenDepends;
import org.jbpm.process.workitem.core.util.WidParameter;
import org.jbpm.process.workitem.core.util.WidResult;
import org.jbpm.process.workitem.core.util.service.WidAction;
import org.jbpm.process.workitem.core.util.service.WidService;
import org.kie.api.runtime.process.WorkItem;
import org.kie.api.runtime.process.WorkItemManager;
import org.kie.internal.runtime.Cacheable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A WorkItemHandler to perform JPA operations. <br />
 * An <b>Action</b> must be provided as an input parameter. The supported value
 * for the Action parameters:
 * <ul>
 * <li>Create: Persist the object and return the attached entity. TO persist an
 * object you must provide the object using the WIH parameter <b>Entity</b></li>
 * <li>Update: Update an attached entity and returns the updated entity. You
 * must provide the attached entity using the <b>Entity</b> WIH parameter.</li>
 * <li>Delete: Delete an attached entity. You must provide the attached entity
 * using the <b>Entity</b> WIH parameter.</li>
 * <li>Get: Get an entity by ID. You must provide the return type using the
 * <b>Type</b> parameter with the FQN of the target class and the ID using the
 * <b>Id</b> parameter.</li>
 * <li>Query: Executes a named query and return the list of results (if any).
 * The query must be provided using the <b>Query</b> input parameter and you may
 * provide query parameters in the form of a Map with key String and value
 * Object using the <b>QueryParameters</b> WIH input parameter. The result of
 * the query is put on the output parameter <b>QueryResults</b>.</li>
 * </ul>
 * When registering the WIH in the deployment descriptor, you must provide the
 * classloader where your mapped entities are and the name of the persistence
 * unit you configured in <em>persistence.xml</em>.
 */

@Wid(widfile = "JPADefinitions.wid", name = "JPA",
        displayName = "JPA",
        defaultHandler = "mvel: new org.jbpm.process.workitem.jpa.JPAWorkItemHandler()",
        documentation = "${artifactId}/index.html",
        parameters = {
                @WidParameter(name = "Type"),
                @WidParameter(name = "Id"),
                @WidParameter(name = "Entity"),
                @WidParameter(name = "Action", required = true),
                @WidParameter(name = "Query"),
                @WidParameter(name = "QueryParameters"),
                @WidParameter(name = "QueryResults")
        },
        results = {
                @WidResult(name = "Result")
        },
        mavenDepends = {
                @WidMavenDepends(group = "${groupId}", artifact = "${artifactId}", version = "${version}")
        },
        serviceInfo = @WidService(category = "${name}", description = "${description}",
                keywords = "jpa,query,named,entity",
                action = @WidAction(title = "Perform a JPA operation")
        ))
public class JPAWorkItemHandler extends AbstractLogOrThrowWorkItemHandler
        implements Cacheable {

    private static final Logger logger = LoggerFactory
            .getLogger(JPAWorkItemHandler.class);

    public static final String P_RESULT = "Result";
    public static final String P_TYPE = "Type";
    public static final String P_ID = "Id";
    public static final String P_ENTITY = "Entity";
    public static final String P_ACTION = "Action";
    public static final String P_QUERY = "Query";
    public static final String P_QUERY_PARAMS = "QueryParameters";
    public static final String P_QUERY_RESULTS = "QueryResults";

    public static final String CREATE_ACTION = "CREATE";
    public static final String UPDATE_ACTION = "UPDATE";
    public static final String GET_ACTION = "GET";
    public static final String DELETE_ACTION = "DELETE";
    public static final String QUERY_ACTION = "QUERY";

    private EntityManagerFactory emf;

    private ClassLoader classloader;

    public JPAWorkItemHandler(String persistenceUnit,
                              ClassLoader cl) {
        ClassLoader tccl = Thread.currentThread().getContextClassLoader();
        try {
            Thread.currentThread().setContextClassLoader(cl);
            this.emf = Persistence.createEntityManagerFactory(persistenceUnit);
        } finally {
            Thread.currentThread().setContextClassLoader(tccl);
        }
        this.classloader = cl;
    }

    public void executeWorkItem(WorkItem wi,
                                WorkItemManager wim) {
        Object actionParam = wi.getParameter(P_ACTION);
        Object entity = wi.getParameter(P_ENTITY);
        Object id = wi.getParameter(P_ID);
        Object type = wi.getParameter(P_TYPE);
        Object queryName = wi.getParameter(P_QUERY);
        Object queryParams = wi.getParameter(P_QUERY_PARAMS);
        Map<String, Object> params = new HashMap<String, Object>();
        List<Object> queryResults = Collections.emptyList();
        String action;

        // Only QUERY does no require an entity parameter
        if (entity == null && P_ACTION.equals(QUERY_ACTION)) {
            throw new IllegalArgumentException(
                    "An entity is required. Use the 'entity' parameter");
        }
        action = String.valueOf(actionParam).trim().toUpperCase();
        logger.debug("Action {} on {}",
                     action,
                     entity);
        EntityManager em = emf.createEntityManager();
        try {

            RequiredParameterValidator.validate(this.getClass(),
                                                wi);

            // join the process transaction
            em.joinTransaction();
            switch (action) {
                case DELETE_ACTION:
                    doDelete(em,
                             entity,
                             id);
                    break;
                case GET_ACTION:
                    if (id == null || type == null) {
                        throw new IllegalArgumentException(
                                "Id or type can't be null when getting an entity");
                    }
                    // only works with long for now
                    entity = doGet(em,
                                   type.toString(),
                                   Long.parseLong(id.toString()));
                    break;
                case UPDATE_ACTION:
                    doUpdate(em,
                             entity);
                    break;
                case CREATE_ACTION:
                    em.persist(entity);
                    break;
                case QUERY_ACTION:
                    if (queryName == null) {
                        throw new IllegalArgumentException("You must provide a '"
                                                                   + P_QUERY + "' parameter to run named queries.");
                    }
                    queryResults = doQuery(em,
                                           String.valueOf(queryName),
                                           queryParams);
                    break;
                default:
                    throw new IllegalArgumentException(
                            "Action " + action + " not recognized. Use 'delete', 'create', 'update', query, or 'get'");
            }
            params.put(P_RESULT,
                       entity);
            params.put(P_QUERY_RESULTS,
                       queryResults);
            wim.completeWorkItem(wi.getId(),
                                 params);
        } catch (Exception e) {
            logger.debug("Error performing JPA action ",
                         e);
            handleException(e);
        } finally {
            em.close();
        }
    }

    @SuppressWarnings("unchecked")
    private List<Object> doQuery(EntityManager em,
                                 String queryName,
                                 Object queryParams) {
        logger.debug("About to run query {}",
                     queryName);
        Map<String, Object> params;
        Query namedQuery = em.createQuery(queryName);
        if (queryParams == null) {
            logger.debug("No parameters were provided");
        } else {
            params = ((Map<String, Object>) queryParams);
            logger.debug("Parameters {}",
                         params);
            params.forEach(namedQuery::setParameter);
        }
        return namedQuery.getResultList();
    }

    private Object doUpdate(EntityManager em,
                            Object entity) {
        return em.merge(entity);
    }

    private Object doGet(EntityManager em,
                         String clazz,
                         Object id) {
        Class<?> type = loadClass(clazz);
        return em.find(type,
                       id);
    }

    private void doDelete(EntityManager em,
                          Object entity,
                          Object id) {
        // detached entity
        if (!em.contains(entity)) {
            entity = doGet(em,
                           entity.getClass().getName(),
                           id);
        }
        if (entity == null) {
            throw new IllegalArgumentException("Can't load the entity to remove. Provide an attached entity or the id to load it.");
        }
        em.remove(entity);
    }

    public void close() {
        emf.close();
    }

    public void abortWorkItem(WorkItem wi,
                              WorkItemManager wim) {
        wim.abortWorkItem(wi.getId());
    }

    private Class<?> loadClass(String clazz) {
        try {
            return Class.forName(clazz,
                                 false,
                                 classloader);
        } catch (ClassNotFoundException e) {
            throw new IllegalArgumentException("Can't load type " + clazz);
        }
    }
}
