/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2013, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.patching.tool;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.CORE_SERVICE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.FAILED;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.FAILURE_DESCRIPTION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.HOST;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.INPUT_STREAM_INDEX;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP_ADDR;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OUTCOME;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESULT;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUCCESS;

import java.io.File;
import java.io.IOException;
import java.util.List;

import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.PathElement;
import org.jboss.as.controller.client.ModelControllerClient;
import org.jboss.as.controller.client.Operation;
import org.jboss.as.controller.client.OperationBuilder;
import org.jboss.as.controller.descriptions.ModelDescriptionConstants;
import org.jboss.as.patching.Constants;
import org.jboss.as.patching.ContentConflictsException;
import org.jboss.as.patching.PatchInfo;
import org.jboss.as.patching.PatchingException;
import org.jboss.as.patching.VerbosePatchInfo;
import org.jboss.as.patching.installation.PatchableTarget.TargetInfo;
import org.jboss.as.patching.logging.PatchLogger;
import org.jboss.as.patching.metadata.ContentItem;
import org.jboss.as.patching.metadata.ContentType;
import org.jboss.as.patching.metadata.Patch;
import org.jboss.as.patching.metadata.PatchElement;
import org.jboss.as.patching.tool.PatchingHistory.Entry;
import org.jboss.dmr.ModelNode;

/**
 * @author Emanuel Muckenhuber
 */
public abstract class PatchOperationTarget {

    static final PathElement CORE_SERVICES = PathElement.pathElement(CORE_SERVICE, "patching");

    /**
     * Create a local target.
     *
     * @param jbossHome    the jboss home
     * @param moduleRoots  the module roots
     * @param bundlesRoots the bundle roots
     * @return             the local target
     * @throws IOException
     */
    public static final PatchOperationTarget createLocal(final File jbossHome, List<File> moduleRoots, List<File> bundlesRoots) throws IOException {
        final PatchTool tool = PatchTool.Factory.createLocalTool(jbossHome, moduleRoots, bundlesRoots);
        return new LocalPatchOperationTarget(tool);
    }

    /**
     * Create a standalone target.
     *
     * @param controllerClient the connected controller client to a standalone instance.
     * @return the remote target
     */
    public static final PatchOperationTarget createStandalone(final ModelControllerClient controllerClient) {
        final PathAddress address = PathAddress.EMPTY_ADDRESS.append(CORE_SERVICES);
        return new RemotePatchOperationTarget(address, controllerClient);
    }

    /**
     * Create a host target.
     *
     * @param hostName the host name
     * @param client the connected controller client to the master host.
     * @return the remote target
     */
    public static final PatchOperationTarget createHost(final String hostName, final ModelControllerClient client) {
        final PathElement host = PathElement.pathElement(HOST, hostName);
        final PathAddress address = PathAddress.EMPTY_ADDRESS.append(host, CORE_SERVICES);
        return new RemotePatchOperationTarget(address, client);
    }

    //

    protected abstract ModelNode streams() throws PatchingException;

    protected abstract ModelNode info() throws PatchingException;
    protected abstract ModelNode info(String streamName) throws PatchingException;

    protected abstract ModelNode info(String patchId, boolean verbose) throws PatchingException;
    protected abstract ModelNode info(String streamName, String patchId, boolean verbose) throws PatchingException;

    protected ModelNode history() throws PatchingException {
        return history(false);
    }
    protected abstract ModelNode history(boolean excludeAgedOut) throws PatchingException;
    protected ModelNode history(String streamName) throws PatchingException {
        return history(streamName, false);
    }
    protected abstract ModelNode history(String streamName, boolean excludeAgedOut) throws PatchingException;

    protected abstract ModelNode applyPatch(final File file, final ContentPolicyBuilderImpl builder) throws PatchingException;

    protected abstract ModelNode rollback(final String patchId, final ContentPolicyBuilderImpl builder, boolean rollbackTo, final boolean restoreConfiguration) throws PatchingException;
    protected abstract ModelNode rollback(final String streamName, final String patchId,
            final ContentPolicyBuilderImpl builder, boolean rollbackTo, final boolean restoreConfiguration) throws PatchingException;

    protected abstract ModelNode rollbackLast(final ContentPolicyBuilderImpl builder, final boolean restoreConfiguration) throws PatchingException;
    protected abstract ModelNode rollbackLast(final String streamName, final ContentPolicyBuilderImpl builder, final boolean restoreConfiguration) throws PatchingException;

    protected static class LocalPatchOperationTarget extends PatchOperationTarget {

        private final PatchTool tool;
        public LocalPatchOperationTarget(PatchTool tool) {
            this.tool = tool;
        }

        @Override
        protected ModelNode streams() throws PatchingException {
            final List<String> streams = tool.getPatchStreams();
            final ModelNode result = new ModelNode();
            result.get(OUTCOME).set(SUCCESS);
            final ModelNode list = result.get(RESULT).setEmptyList();
            for(final String stream : streams) {
                list.add(stream);
            }
            return result;
        }

        @Override
        protected ModelNode info() throws PatchingException {
            return info(null);
        }

        @Override
        protected ModelNode info(String streamName) throws PatchingException {
            final PatchInfo info = tool.getPatchInfo(streamName);
            final ModelNode response = new ModelNode();
            response.get(OUTCOME).set(SUCCESS);
            final ModelNode result = response.get(RESULT);
            result.get(Constants.VERSION).set(info.getVersion());
            result.get(Constants.CUMULATIVE).set(info.getCumulativePatchID());
            result.get(Constants.PATCHES).setEmptyList();
            for(final String patch : info.getPatchIDs()) {
                result.get(Constants.PATCHES).add(patch);
            }
            if(info instanceof VerbosePatchInfo) {
                final VerbosePatchInfo vInfo = (VerbosePatchInfo) info;
                if(vInfo.hasLayers()) {
                    final ModelNode layersNode = result.get(Constants.LAYER);
                    for(String name : vInfo.getLayerNames()) {
                        addLayerInfo(layersNode.get(name), vInfo.getLayerInfo(name));
                    }
                }
                if(vInfo.hasAddOns()) {
                    final ModelNode layersNode = result.get(Constants.ADD_ON);
                    for(String name : vInfo.getAddOnNames()) {
                        addLayerInfo(layersNode.get(name), vInfo.getAddOnInfo(name));
                    }
                }
            }
            return response;
        }

        private void addLayerInfo(final ModelNode layerNode, final TargetInfo layerInfo) {
            layerNode.get(Constants.CUMULATIVE).set(layerInfo.getCumulativePatchID());
            final ModelNode patchesNode = layerNode.get(Constants.PATCHES).setEmptyList();
            if(!layerInfo.getPatchIDs().isEmpty()) {
                for(String patchId : layerInfo.getPatchIDs()) {
                    patchesNode.add(patchId);
                }
            }
        }

        @Override
        protected ModelNode history(boolean excludeAgedOut) {
            return history(null, excludeAgedOut);
        }

        @Override
        protected ModelNode history(String streamName, boolean excludeAgedOut) {
            final ModelNode result = new ModelNode();
            result.get(OUTCOME).set(SUCCESS);
            try {
                result.get(RESULT).set(tool.getPatchingHistory(streamName).getHistory(excludeAgedOut));
            } catch (PatchingException e) {
                return formatFailedResponse(e);
            }
            return result;
        }

        @Override
        protected ModelNode applyPatch(final File file, final ContentPolicyBuilderImpl builder) {
            final ContentVerificationPolicy policy = builder.createPolicy();
            ModelNode result = new ModelNode();
            try {
                PatchingResult apply = tool.applyPatch(file, policy);
                apply.commit();
                result.get(OUTCOME).set(SUCCESS);
                result.get(RESULT).setEmptyObject();
            } catch (PatchingException e) {
                return formatFailedResponse(e);
            }
            return result;
        }

        @Override
        protected ModelNode rollback(final String patchId, final ContentPolicyBuilderImpl builder, boolean rollbackTo, boolean resetConfiguration) {
            return rollback(null, patchId, builder, rollbackTo, resetConfiguration);
        }

        @Override
        protected ModelNode rollback(final String streamName, final String patchId, final ContentPolicyBuilderImpl builder, boolean rollbackTo, boolean resetConfiguration) {
            final ContentVerificationPolicy policy = builder.createPolicy();
            ModelNode result = new ModelNode();
            try {
                PatchingResult rollback = tool.rollback(streamName, patchId, policy, rollbackTo, resetConfiguration);
                rollback.commit();
                result.get(OUTCOME).set(SUCCESS);
                result.get(RESULT).setEmptyObject();
            } catch (PatchingException e) {
                return formatFailedResponse(e);
            }
            return result;
        }

        @Override
        protected ModelNode rollbackLast(final ContentPolicyBuilderImpl builder, boolean restoreConfiguration) {
            return rollbackLast(null, builder, restoreConfiguration);
        }

        @Override
        protected ModelNode rollbackLast(final String streamName, final ContentPolicyBuilderImpl builder, boolean restoreConfiguration) {
            final ContentVerificationPolicy policy = builder.createPolicy();
            ModelNode result = new ModelNode();
            try {
                PatchingResult rollback = tool.rollbackLast(streamName, policy, restoreConfiguration);
                rollback.commit();
                result.get(OUTCOME).set(SUCCESS);
                result.get(RESULT).setEmptyObject();
            } catch (PatchingException e) {
                return formatFailedResponse(e);
            }
            return result;
        }

        @Override
        protected ModelNode info(String patchId, boolean verbose) throws PatchingException {
            return info(null, patchId, verbose);
        }

        @Override
        protected ModelNode info(String streamName, String patchId, boolean verbose) throws PatchingException {
            if(patchId == null) {
                throw new IllegalArgumentException("patchId is null");
            }
            final PatchingHistory history = tool.getPatchingHistory(streamName);
            try {
                final PatchingHistory.Iterator iterator = history.iterator();
                while(iterator.hasNext()) {
                    final Entry next = iterator.next();
                    if(patchId.equals(next.getPatchId())) {
                        final ModelNode response = new ModelNode();
                        response.get(OUTCOME).set(SUCCESS);
                        final ModelNode result = response.get(RESULT);
                        result.get(Constants.PATCH_ID).set(next.getPatchId());
                        result.get(Constants.TYPE).set(next.getType().getName());
                        final Patch metadata = next.getMetadata();
                        result.get(Constants.IDENTITY_NAME).set(metadata.getIdentity().getName());
                        result.get(Constants.IDENTITY_VERSION).set(metadata.getIdentity().getVersion());
                        result.get(Constants.DESCRIPTION).set(next.getMetadata().getDescription());
                        if (next.getMetadata().getLink() != null) {
                            result.get(Constants.LINK).set(next.getMetadata().getLink());
                        }

                        if (verbose) {
                            final ModelNode elements = result.get(Constants.ELEMENTS).setEmptyList();
                            for(PatchElement e : metadata.getElements()) {
                                final ModelNode element = new ModelNode();
                                element.get(Constants.PATCH_ID).set(e.getId());
                                element.get(Constants.NAME).set(e.getProvider().getName());
                                element.get(Constants.TYPE).set(e.getProvider().isAddOn() ? Constants.ADD_ON : Constants.LAYER);
                                element.get(Constants.DESCRIPTION).set(e.getDescription());
                                elements.add(element);
                            }
                        }
                        return response;
                    }
                }
            } catch (PatchingException e) {
                return formatFailedResponse(e);
            }
            return formatFailedResponse(PatchLogger.ROOT_LOGGER.patchNotFoundInHistory(patchId).getLocalizedMessage());
        }
    }

    protected static class RemotePatchOperationTarget extends PatchOperationTarget {

        private final PathAddress address;
        private final ModelControllerClient client;

        public RemotePatchOperationTarget(PathAddress address, ModelControllerClient client) {
            this.address = address;
            this.client = client;
        }

        @Override
        protected ModelNode streams() throws PatchingException {
            final ModelNode operation = new ModelNode();
            operation.get(ModelDescriptionConstants.OP_ADDR).set(address.toModelNode());
            operation.get(ModelDescriptionConstants.OP).set(ModelDescriptionConstants.READ_CHILDREN_NAMES_OPERATION);
            operation.get(ModelDescriptionConstants.CHILD_TYPE).set(Constants.PATCH_STREAM);
            return executeOp(operation);
        }

        @Override
        protected ModelNode info() throws PatchingException {
            return info(null);
        }

        @Override
        protected ModelNode info(String streamName) throws PatchingException {
            final ModelNode operation = new ModelNode();
            operation.get(ModelDescriptionConstants.OP).set(Constants.PATCH_INFO);
            operation.get(ModelDescriptionConstants.OP_ADDR).set(address.toModelNode());
            if(streamName != null) {
                operation.get(ModelDescriptionConstants.OP_ADDR).add(Constants.PATCH_STREAM, streamName);
            }
            operation.get(Constants.VERBOSE).set(true);
            return executeOp(operation);
        }

        @Override
        protected ModelNode history(boolean excludeAgedOut) throws PatchingException {
            return history(null, excludeAgedOut);
        }

        @Override
        protected ModelNode history(String streamName, boolean excludeAgedOut) throws PatchingException {
            final ModelNode operation = new ModelNode();
            operation.get(OP).set(Constants.SHOW_HISTORY);
            operation.get(OP_ADDR).set(address.toModelNode());
            if(streamName != null) {
                operation.get(ModelDescriptionConstants.OP_ADDR).add(Constants.PATCH_STREAM, streamName);
            }
            if(excludeAgedOut) {
                operation.get(Constants.EXCLUDE_AGED_OUT).set(true);
            }
            return executeOp(operation);
        }

        @Override
        protected ModelNode applyPatch(final File file, final ContentPolicyBuilderImpl policyBuilder) throws PatchingException {
            final ModelNode operation = createOperation(Constants.PATCH, address.toModelNode(), policyBuilder);
            operation.get(INPUT_STREAM_INDEX).set(0);
            final OperationBuilder operationBuilder = OperationBuilder.create(operation);
            operationBuilder.addFileAsAttachment(file);
            return executeOp(operationBuilder.build());
        }

        @Override
        protected ModelNode rollback(String patchId, ContentPolicyBuilderImpl builder, boolean rollbackTo, boolean resetConfiguration) throws PatchingException {
            return rollback(null, patchId, builder, rollbackTo, resetConfiguration);
        }

        @Override
        protected ModelNode rollback(String streamName, String patchId, ContentPolicyBuilderImpl builder, boolean rollbackTo, boolean resetConfiguration) throws PatchingException {
            final ModelNode operation = createOperation(Constants.ROLLBACK, address.toModelNode(), builder);
            operation.get(Constants.PATCH_ID).set(patchId);
            operation.get(Constants.RESET_CONFIGURATION).set(resetConfiguration);
            operation.get(Constants.ROLLBACK_TO).set(rollbackTo);
            if(streamName != null) {
                operation.get(ModelDescriptionConstants.OP_ADDR).add(Constants.PATCH_STREAM, streamName);
            }
            return executeOp(operation);
        }

        @Override
        protected ModelNode rollbackLast(ContentPolicyBuilderImpl builder, boolean restoreConfiguration) throws PatchingException {
            return rollbackLast(null, builder, restoreConfiguration);
        }

        @Override
        protected ModelNode rollbackLast(String streamName, ContentPolicyBuilderImpl builder, boolean restoreConfiguration) throws PatchingException {
            final ModelNode operation = createOperation(Constants.ROLLBACK_LAST, address.toModelNode(), builder);
            operation.get(Constants.RESET_CONFIGURATION).set(restoreConfiguration);
            if(streamName != null) {
                operation.get(ModelDescriptionConstants.OP_ADDR).add(Constants.PATCH_STREAM, streamName);
            }
            return executeOp(operation);
        }

        @Override
        protected ModelNode info(String patchId, boolean verbose) throws PatchingException {
            return info(null, patchId, verbose);
        }

        @Override
        protected ModelNode info(String streamName, String patchId, boolean verbose) throws PatchingException {
            final ModelNode operation = new ModelNode();
            operation.get(ModelDescriptionConstants.OP).set(Constants.PATCH_INFO);
            operation.get(ModelDescriptionConstants.OP_ADDR).set(address.toModelNode());
            operation.get(Constants.PATCH_ID).set(patchId);
            if(streamName != null) {
                operation.get(ModelDescriptionConstants.OP_ADDR).add(Constants.PATCH_STREAM, streamName);
            }
            if(verbose) {
                operation.get(Constants.VERBOSE).set(true);
            }
            return executeOp(operation);
        }

        protected ModelNode executeOp(ModelNode operation) throws PatchingException {
            try {
                return client.execute(operation);
            } catch (IOException e) {
                throw new PatchingException("Failed to execute operation " + operation, e);
            }
        }

        protected ModelNode executeOp(Operation operation) throws PatchingException {
            try {
                return client.execute(operation);
            } catch (IOException e) {
                throw new PatchingException("Failed to execute operation " + operation.getOperation(), e);
            }
        }
    }

    static ModelNode formatFailedResponse(final String msg) {
        final ModelNode result = new ModelNode();
        result.get(OUTCOME).set(FAILED);
        result.get(FAILURE_DESCRIPTION, Constants.MESSAGE).set(msg);
        return result;
    }

    static ModelNode formatFailedResponse(final PatchingException e) {
        final ModelNode result = new ModelNode();
        result.get(OUTCOME).set(FAILED);
        formatFailedResponse(e, result.get(FAILURE_DESCRIPTION));
        return result;
    }

    public static void formatFailedResponse(final PatchingException e, final ModelNode failureDescription) {
        if(e instanceof ContentConflictsException) {
            failureDescription.get(Constants.MESSAGE).set(PatchLogger.ROOT_LOGGER.detectedConflicts());
            final ModelNode conflicts = failureDescription.get(Constants.CONFLICTS);
            for(final ContentItem item : ((ContentConflictsException)e).getConflicts()) {
                final ContentType type = item.getContentType();
                switch (type) {
                    case BUNDLE:
                        conflicts.get(Constants.BUNDLES).add(item.getRelativePath());
                        break;
                    case MODULE:
                        conflicts.get(Constants.MODULES).add(item.getRelativePath());
                        break;
                    case MISC:
                        conflicts.get(Constants.MISC).add(item.getRelativePath());
                        break;
                }
            }
        } else {
            failureDescription.set(e.getLocalizedMessage());
        }
    }

    static ModelNode createOperation(final String operationName, final ModelNode addr, final ContentPolicyBuilderImpl builder) {
        final ModelNode operation = new ModelNode();
        operation.get(ModelDescriptionConstants.OP).set(operationName);
        operation.get(ModelDescriptionConstants.OP_ADDR).set(addr);

        // Process the policy
        operation.get(Constants.OVERRIDE_MODULES).set(builder.ignoreModulesChanges);
        operation.get(Constants.OVERRIDE_ALL).set(builder.overrideAll);
        if(! builder.override.isEmpty()) {
            for(final String o : builder.override) {
                operation.get(Constants.OVERRIDE).add(o);
            }
        }
        if(! builder.preserve.isEmpty()) {
            for(final String p : builder.preserve) {
                operation.get(Constants.PRESERVE).add(p);
            }
        }
        return operation;
    }
}
