/*
 * Copyright 2015 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.
 *
 *      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.kie.remote.services.rest;

import static org.kie.remote.services.rest.async.cmd.DeploymentCmd.DEPLOYMENT_UNIT;
import static org.kie.remote.services.rest.async.cmd.DeploymentCmd.JOB_ID;
import static org.kie.remote.services.rest.async.cmd.DeploymentCmd.JOB_TYPE;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.jbpm.kie.services.impl.KModuleDeploymentService;
import org.jbpm.kie.services.impl.KModuleDeploymentUnit;
import org.jbpm.runtime.manager.impl.deploy.DeploymentDescriptorImpl;
import org.jbpm.services.api.DefinitionService;
import org.jbpm.services.api.DeploymentService;
import org.jbpm.services.api.RuntimeDataService;
import org.jbpm.services.api.model.DeployedUnit;
import org.jbpm.services.api.model.ProcessDefinition;
import org.jbpm.services.cdi.Kjar;
import org.kie.api.executor.CommandContext;
import org.kie.api.executor.ExecutorService;
import org.kie.internal.query.QueryContext;
import org.kie.internal.runtime.conf.DeploymentDescriptor;
import org.kie.internal.runtime.conf.MergeMode;
import org.kie.internal.runtime.conf.RuntimeStrategy;
import org.kie.remote.services.cdi.DeploymentInfoBean;
import org.kie.remote.services.exception.KieRemoteServicesDeploymentException;
import org.kie.remote.services.rest.async.JobResultManager;
import org.kie.remote.services.rest.async.cmd.DeploymentCmd;
import org.kie.remote.services.rest.async.cmd.JobType;
import org.kie.remote.services.rest.exception.KieRemoteRestOperationException;
import org.kie.services.client.serialization.jaxb.impl.deploy.JaxbDeploymentDescriptor;
import org.kie.services.client.serialization.jaxb.impl.deploy.JaxbDeploymentJobResult;
import org.kie.services.client.serialization.jaxb.impl.deploy.JaxbDeploymentUnit;
import org.kie.services.client.serialization.jaxb.impl.deploy.JaxbDeploymentUnit.JaxbDeploymentStatus;
import org.kie.services.client.serialization.jaxb.impl.deploy.JaxbDeploymentUnitList;
import org.kie.services.client.serialization.jaxb.impl.process.JaxbProcessDefinition;
import org.kie.services.client.serialization.jaxb.impl.process.JaxbProcessDefinitionList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class is extended by both the {@link DeploymentResourceImpl} and the
 * {@link DeploymentsResourceImpl}.
 * </p>
 * It contains methods and fields common to both REST resource implementations.
 */
@Singleton
public class DeployResourceBase extends ResourceBase {

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

    /* KIE information and processing */

    @Inject
    @Kjar
    private KModuleDeploymentService deploymentService;

    @Inject
    private DeploymentInfoBean deploymentInfoBean;

    @Inject
    private RuntimeDataService runtimeDataService;

    @Inject
    private DefinitionService bpmn2DataService;

    /* Async */

    @Inject
    private ExecutorService jobExecutor;

    private final AtomicLong jobIdGen = new AtomicLong(0);

    @Inject
    private JobResultManager jobResultMgr;

    // Helper methods ------------------------------------------------------------------------------------------------------------

    /**
     * Convert the {@link KModuleDeploymentUnit} instance from the {@link DeploymentService}
     * to a {@link JaxbDeploymentUnit} usable by the REST service.
     * @param kDepUnit The {@link KModuleDeploymentUnit} instance
     * @return A {@link JaxbDeploymentUnit} instance
     */
    static JaxbDeploymentUnit convertKModuleDepUnitToJaxbDepUnit(KModuleDeploymentUnit kDepUnit ) {
        JaxbDeploymentUnit jDepUnit = new JaxbDeploymentUnit(
                kDepUnit.getGroupId(),
                kDepUnit.getArtifactId(),
                kDepUnit.getVersion(),
                kDepUnit.getKbaseName(),
                kDepUnit.getKsessionName());
        jDepUnit.setStrategy(kDepUnit.getStrategy());
        return jDepUnit;
    }

    /**
     * Create a {@link KModuleDeploymentUnit} instance using the given information
     * @param deploymentId The deployment id
     * @param descriptor The optional {@link JaxbDeploymentDescriptor} instance with additional information
     * @return The {@link KModuleDeploymentUnit} instance
     */
    // pkg scope for tests
    static KModuleDeploymentUnit createDeploymentUnit(String deploymentId, JaxbDeploymentDescriptor descriptor) {
        String [] gavKK = deploymentId.split(":");
        KModuleDeploymentUnit deployUnit = new KModuleDeploymentUnit(gavKK[0], gavKK[1], gavKK[2]);
        if( gavKK.length > 3 ) {
            deployUnit.setKbaseName(gavKK[3]);
        }
        if( gavKK.length > 4 ) {
            deployUnit.setKsessionName(gavKK[4]);
        }
        if (descriptor != null) {
            DeploymentDescriptor realDepDesc = convertToDeploymentDescriptor(descriptor);
            deployUnit.setDeploymentDescriptor(realDepDesc);
        }
        return deployUnit;
    }

    /**
     * Convert the received {@link JaxbDeploymentDescriptor} instance to a {@link DeploymentDescriptor} instance
     * that the {@link DeploymentService} can process.
     * @param jaxbDepDesc The received {@link JaxbDeploymentDescriptor} instance
     * @return A {@link DeploymentDescriptor} instance
     */
    private static DeploymentDescriptor convertToDeploymentDescriptor( JaxbDeploymentDescriptor jaxbDepDesc ) {
        DeploymentDescriptorImpl depDescImpl = new DeploymentDescriptorImpl(jaxbDepDesc.getPersistenceUnit());

        depDescImpl.setAuditPersistenceUnit(jaxbDepDesc.getAuditPersistenceUnit());
        depDescImpl.setAuditMode(jaxbDepDesc.getAuditMode());
        depDescImpl.setPersistenceMode(jaxbDepDesc.getPersistenceMode());
        depDescImpl.setRuntimeStrategy(jaxbDepDesc.getRuntimeStrategy());
        depDescImpl.setMarshallingStrategies(jaxbDepDesc.getMarshallingStrategies());
        depDescImpl.setEventListeners(jaxbDepDesc.getEventListeners());
        depDescImpl.setTaskEventListeners(jaxbDepDesc.getTaskEventListeners());
        depDescImpl.setGlobals(jaxbDepDesc.getGlobals());
        depDescImpl.setWorkItemHandlers(jaxbDepDesc.getWorkItemHandlers());
        depDescImpl.setEnvironmentEntries(jaxbDepDesc.getEnvironmentEntries());
        depDescImpl.setConfiguration(jaxbDepDesc.getConfiguration());
        depDescImpl.setRequiredRoles(jaxbDepDesc.getRequiredRoles());
        depDescImpl.setClasses(jaxbDepDesc.getRemoteableClasses());
        depDescImpl.setLimitSerializationClasses(jaxbDepDesc.getLimitSerializationClasses());

        return depDescImpl;
    }

    // Deployment methods ---------------------------------------------------------------------------------------------------------

    /**
     * Determines the status of a deployment
     * @param checkDeploymentService Whether or not to use the {@link DeploymentService} when checking the status
     * @return A {@link JaxbDeploymentUnit} representing the status
     */
    public JaxbDeploymentUnit determineStatus(String deploymentId, boolean checkDeploymentService) {

        JaxbDeploymentUnit jaxbDepUnit;
        if( checkDeploymentService ) {
            DeployedUnit deployedUnit = deploymentService.getDeployedUnit(deploymentId);

            // Deployed
            if( deployedUnit != null ) {
                KModuleDeploymentUnit depUnit = (KModuleDeploymentUnit) deployedUnit.getDeploymentUnit();
                jaxbDepUnit = convertKModuleDepUnitToJaxbDepUnit(depUnit);
                jaxbDepUnit.setStatus(JaxbDeploymentStatus.DEPLOYED);
                return jaxbDepUnit;
            }
        }

        String [] gavKK = deploymentId.split(":");
        switch( gavKK.length ) {
        case 3:
            jaxbDepUnit = new JaxbDeploymentUnit(gavKK[0], gavKK[1], gavKK[2]);
            break;
        case 4:
            jaxbDepUnit = new JaxbDeploymentUnit(gavKK[0], gavKK[1], gavKK[2], gavKK[3], null);
            break;
        case 5:
            jaxbDepUnit = new JaxbDeploymentUnit(gavKK[0], gavKK[1], gavKK[2], gavKK[3], gavKK[4]);
            break;
        default:
            throw KieRemoteRestOperationException.notFound("Invalid deployment id: " + deploymentId);
        }
        jaxbDepUnit.setStatus(JaxbDeploymentStatus.UNDEPLOYED);
        return jaxbDepUnit;
    }

    public JaxbDeploymentJobResult submitDeployJob(String deploymentId, String strategy, String mergeMode, JaxbDeploymentDescriptor deployDescriptor ) {
        JaxbDeploymentJobResult jobResult;
        DeployedUnit deployedUnit = deploymentService.getDeployedUnit(deploymentId);
        if( deployedUnit != null ) {
            // If the deployment unit already exists, request can not be completed..
            KModuleDeploymentUnit kDepUnit = (KModuleDeploymentUnit) deployedUnit.getDeploymentUnit();
            JaxbDeploymentUnit jaxbDepUnit = convertKModuleDepUnitToJaxbDepUnit(kDepUnit);
            jobResult = new JaxbDeploymentJobResult(
                    null,
                    "The deployment already exists and must be first undeployed!",
                   jaxbDepUnit,
                   JobType.DEPLOY.toString());
            jobResult.setSuccess(false);
        } else {

            KModuleDeploymentUnit deploymentUnit = createDeploymentUnit(deploymentId, deployDescriptor);

            if( strategy != null ) {
                strategy = strategy.toUpperCase();
                RuntimeStrategy runtimeStrategy;
                try {
                    runtimeStrategy = RuntimeStrategy.valueOf(strategy);
                } catch( IllegalArgumentException iae ) {
                    throw KieRemoteRestOperationException.badRequest("Runtime strategy '" + strategy + "' does not exist.");
                }
                deploymentUnit.setStrategy(runtimeStrategy);
            }
            if (mergeMode != null) {
                mergeMode = mergeMode.toUpperCase();
                MergeMode mode;
                try {
                    mode = MergeMode.valueOf(mergeMode);
                }  catch( IllegalArgumentException iae ) {
                    throw KieRemoteRestOperationException.badRequest("Merge mode '" + mergeMode + "' does not exist.");
                }
                deploymentUnit.setMergeMode(mode);
            }

            jobResult = scheduleDeploymentJobRequest(deploymentId, JobType.DEPLOY, deploymentUnit);
        }
        return jobResult;
    }

    /**
     * Schedules a deploy or undeploy job with the jbpm-executor for execution.
     * @param jobType The type of job: deploy or undeploy
     * @param deploymentUnit The deployment unit that should be acted upon
     * @return The initial status of the job in a {@link JaxbDeploymentJobResult} instance
     */
    private JaxbDeploymentJobResult scheduleDeploymentJobRequest(String deploymentId, JobType jobType, KModuleDeploymentUnit deploymentUnit) {
        CommandContext ctx = new CommandContext();
        ctx.setData(DEPLOYMENT_UNIT,  deploymentUnit);
        ctx.setData(JOB_TYPE, jobType);
        ctx.setData("retries", 0);
	    ctx.setData("owner", ExecutorService.EXECUTOR_ID);

        String jobTypeLower = jobType.toString().toLowerCase();

        String jobId = "" + System.currentTimeMillis() + "-" + jobIdGen.incrementAndGet();
        ctx.setData(JOB_ID, jobId);
        JaxbDeploymentJobResult jobResult = new JaxbDeploymentJobResult(
                jobId,
                jobTypeLower + " job accepted.",
                convertKModuleDepUnitToJaxbDepUnit(deploymentUnit), jobType.toString());
        jobResult.getDeploymentUnit().setStatus(JaxbDeploymentStatus.ACCEPTED);

        logger.debug( "{} job [{}] for deployment '{}' created.", jobType.toString(), jobId, deploymentUnit.getIdentifier());
        jobResultMgr.putJob(jobResult.getJobId(), jobResult, jobType);
        Long executorJobId;
        try {
            ctx.setData("businessKey", jobId);
            ctx.setData("jobResult", jobResult);

            executorJobId = jobExecutor.scheduleRequest(DeploymentCmd.class.getName(), ctx);
            jobResult.setIdentifier(executorJobId);
            jobResult.setSuccess(true);
        } catch( Exception e ) {
            String msg = "Unable to " + jobType.toString().toLowerCase()
                    + " deployment '" + deploymentId + "': "
                    + e.getClass().getSimpleName() + " thrown [" + e.getMessage() + "]";
            logger.error( msg, e );
            jobResult.setExplanation(msg);
            jobResult.setSuccess(false);
        }

        return jobResult;
    }

    public JaxbDeploymentJobResult submitUndeployJob(String deploymentId) {
        DeployedUnit deployedUnit = deploymentService.getDeployedUnit(deploymentId);
        JaxbDeploymentJobResult jobResult;
        if( deployedUnit != null ) {
            KModuleDeploymentUnit deploymentUnit = (KModuleDeploymentUnit) deployedUnit.getDeploymentUnit();
            jobResult = scheduleDeploymentJobRequest(deploymentId, JobType.UNDEPLOY, deploymentUnit);
        } else {
            JaxbDeploymentUnit depUnit = determineStatus(deploymentId, false);

            String explanation;
            switch( depUnit.getStatus()) {
            case ACCEPTED: // deployment service (above) has not found it, so it must be still deploying
            case DEPLOYED: // minor race condition between the deployment service and this code
            case DEPLOYING: // obvious..
                explanation = "The deployment can not be undeployed because the initial deployment has not yet fully completed.";
                break;
            case DEPLOY_FAILED:
                explanation = "The deployment can not be undeployed because the initial deployment failed.";
                break;
            case NONEXISTENT:
            case UNDEPLOYED:
            case UNDEPLOYING:
                explanation = "The deployment can not be undeployed because it has already been undeployed (or is currently being undeployed)";
                break;
            case UNDEPLOY_FAILED: // from the last request
                explanation = "The last undeployment failed, but the deployment unit is no longer present (and can not be undeployed, thus). "
                        + "There is probably a very high load on this server. Turning on debugging may provide insight.";
                       logger.debug("Stack trace:", new Throwable());
                break;
            default:
                throw new KieRemoteServicesDeploymentException("Unknown deployment unit status: " + depUnit.getStatus());
            }
            jobResult = new JaxbDeploymentJobResult(null, explanation, depUnit, JobType.UNDEPLOY.toString() );
            jobResult.setSuccess(false);
        }
        return jobResult;
    }

    public JaxbDeploymentUnitList getDeploymentList(int [] pageInfo, int maxNumResults) {
        List<String> deploymentIds = new ArrayList<String>(deploymentInfoBean.getDeploymentIds());
        Collections.sort(deploymentIds);

        JaxbDeploymentUnitList jaxbDepUnitList = new JaxbDeploymentUnitList();
        List<JaxbDeploymentUnit> depUnitList = jaxbDepUnitList.getDeploymentUnitList();
        for( String deploymentId : deploymentIds ) {
            DeployedUnit deployedUnit = deploymentService.getDeployedUnit(deploymentId);
            if( deployedUnit != null ) {
                JaxbDeploymentUnit jaxbDepUnit = convertKModuleDepUnitToJaxbDepUnit((KModuleDeploymentUnit) deployedUnit.getDeploymentUnit());
                jaxbDepUnit.setStatus(JaxbDeploymentStatus.DEPLOYED);
                depUnitList.add(jaxbDepUnit);
                if( depUnitList.size() >= maxNumResults) {
                    // pagination parameters indicate that no more than current list is needed
                    break;
                }
            }
        }

        JaxbDeploymentUnitList resultList = paginateAndCreateResult(pageInfo, depUnitList, new JaxbDeploymentUnitList());
        return resultList;
    }

    public JaxbProcessDefinitionList getProcessDefinitionList(int [] pageInfo, int maxNumResults) {
        List<String> deploymentIds = new ArrayList<String>(deploymentInfoBean.getDeploymentIds());
        Collections.sort(deploymentIds);

        JaxbProcessDefinitionList jaxbProcDefList = new JaxbProcessDefinitionList();
        List<JaxbProcessDefinition> procDefList = jaxbProcDefList.getProcessDefinitionList();
        for( String deploymentId : deploymentIds ) {
            fillProcessDefinitionList(deploymentId, pageInfo, maxNumResults, procDefList);

            if( procDefList.size() == maxNumResults) {
                // pagination parameters indicate that no more than current list is needed
                break;
            }
        }

        JaxbProcessDefinitionList resultList = paginateAndCreateResult(pageInfo, procDefList, new JaxbProcessDefinitionList());
        return resultList;
    }

    public void fillProcessDefinitionList(String deploymentId, int [] pageInfo, int maxNumResults, List<JaxbProcessDefinition> procDefList) {
        List<String> processIdList = Collections.EMPTY_LIST;
        try {
            int offset = ( pageInfo[PAGE_NUM] > 0 ? pageInfo[PAGE_NUM]-1 : 0 ) * pageInfo[PAGE_SIZE];
            int count = ( pageInfo[PAGE_SIZE] > 0 ? pageInfo[PAGE_SIZE] : 10 );
            QueryContext queryContext = new QueryContext(offset, count);
            processIdList = new ArrayList<String>(runtimeDataService.getProcessIds(deploymentId, queryContext));
            Collections.sort(processIdList);
        } catch( Exception e) {
            // possibly because the deployment is being modified and not fully un/deployed.. (un/deploy*ing*)
            logger.debug( "Unable to retrieve process ids for deployment '{}': {}", deploymentId, e.getMessage(), e);
        }
        for( String processId : processIdList ) {
            ProcessDefinition processAssetDesc;
            try {
                processAssetDesc = runtimeDataService.getProcessesByDeploymentIdProcessId(deploymentId, processId);
                if( processAssetDesc == null ) {
                    logger.error( "No process definition information available for process definition '{}' in deployment '{}'!",
                            processId, deploymentId);
                    continue;
                }
            } catch( Exception e ) {
                // possibly because the deployment is being modified and not fully un/deployed.. (un/deploy*ing*)
                logger.debug( "Unable to retrieve process definition for process '{}' in deployment '{}': {}",
                        processId, deploymentId, e.getMessage(), e);
                continue;
            }
            JaxbProcessDefinition jaxbProcDef = convertProcAssetDescToJaxbProcDef(processAssetDesc);
            Map<String, String> variables;
            try {
                variables = bpmn2DataService.getProcessVariables(deploymentId, processId);
            } catch( Exception e) {
                // possibly because the deployment is being modified and not fully un/deployed.. (un/deploy*ing*)
                logger.debug( "Unable to retrieve process definition data for process '{}' in deployment '{}': {}",
                        processId, deploymentId, e.getMessage(), e);
                continue;
            }
            jaxbProcDef.setVariables(variables);
            procDefList.add(jaxbProcDef);

            if( procDefList.size() == maxNumResults) {
                // pagination parameters indicate that no more than current list is needed
                break;
            }
        }
    }

    public void activate(String deploymentId) {
        deploymentService.activate(deploymentId);
    }

    public void deactivate(String deploymentId) {
        deploymentService.deactivate(deploymentId);
    }
}
