/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2022, 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.wildfly.extension.microprofile.health;

import static org.jboss.as.controller.OperationContext.Stage.MODEL;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.EXTENSION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.FAILURE_DESCRIPTION;
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.RESULT;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUBSYSTEM;
import static org.wildfly.extension.microprofile.health.MicroProfileHealthExtension.EXTENSION_NAME;
import static org.wildfly.extension.microprofile.health.MicroProfileHealthSubsystemDefinition.SECURITY_ENABLED;

import java.util.LinkedHashMap;
import java.util.Map;

import org.jboss.as.controller.OperationContext;
import org.jboss.as.controller.OperationFailedException;
import org.jboss.as.controller.OperationStepHandler;
import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.PathElement;
import org.jboss.as.controller.ProcessType;
import org.jboss.as.controller.RunningMode;
import org.jboss.as.controller.SimpleMapAttributeDefinition;
import org.jboss.as.controller.SimpleOperationDefinitionBuilder;
import org.jboss.as.controller.StringListAttributeDefinition;
import org.jboss.as.controller.access.management.SensitiveTargetAccessConstraintDefinition;
import org.jboss.as.controller.descriptions.ResourceDescriptionResolver;
import org.jboss.as.controller.operations.MultistepUtil;
import org.jboss.as.controller.operations.common.Util;
import org.jboss.as.controller.registry.ManagementResourceRegistration;
import org.jboss.as.controller.registry.OperationEntry;
import org.jboss.as.controller.registry.Resource;
import org.jboss.dmr.ModelNode;
import org.jboss.dmr.ModelType;
import org.wildfly.extension.microprofile.health._private.MicroProfileHealthLogger;

/**
 * @author <a href="mailto:kabir.khan@jboss.com">Kabir Khan</a>
 */
public class MicroProfileHealthMigrateOperation implements OperationStepHandler {
    public static final String MIGRATE = "migrate";
    public static final String MIGRATION_WARNINGS = "migration-warnings";
    public static final String MIGRATION_ERROR = "migration-error";
    public static final String MIGRATION_OPERATIONS = "migration-operations";
    public static final String DESCRIBE_MIGRATION = "describe-migration";

    private static final PathAddress EXTENSION_ADDR = PathAddress.pathAddress(EXTENSION, EXTENSION_NAME);
    static final String HEALTH_SUBSYSTEM_NAME = "health";
    static final String HEALTH_EXTENSION_NAME = "org.wildfly.extension.health";
    private static final PathElement HEALTH_SUBSYSTEM_ELEMENT = PathElement.pathElement(SUBSYSTEM, HEALTH_SUBSYSTEM_NAME);
    private static final PathElement HEALTH_EXTENSION_ELEMENT = PathElement.pathElement(EXTENSION, HEALTH_EXTENSION_NAME);

    // We're not transforming resources here so there will be no warnings but it is good to keep our ops consistent with other migration ops
    public static final StringListAttributeDefinition MIGRATION_WARNINGS_ATTR = new StringListAttributeDefinition.Builder(MIGRATION_WARNINGS)
            .setRequired(false)
            .build();

    public static final SimpleMapAttributeDefinition MIGRATION_ERROR_ATTR = new SimpleMapAttributeDefinition.Builder(MIGRATION_ERROR, ModelType.OBJECT, true)
            .setValueType(ModelType.OBJECT)
            .setRequired(false)
            .build();

    private boolean describe;

    static void registerOperations(ManagementResourceRegistration registry, ResourceDescriptionResolver resourceDescriptionResolver) {
        registry.registerOperationHandler(new SimpleOperationDefinitionBuilder(MIGRATE, resourceDescriptionResolver)
                        .setAccessConstraints(SensitiveTargetAccessConstraintDefinition.READ_WHOLE_CONFIG)
                        .setReplyParameters(MIGRATION_WARNINGS_ATTR, MIGRATION_ERROR_ATTR)
                        .build(),
                new MicroProfileHealthMigrateOperation(false));
        registry.registerOperationHandler(new SimpleOperationDefinitionBuilder(DESCRIBE_MIGRATION, resourceDescriptionResolver)
                        .setAccessConstraints(SensitiveTargetAccessConstraintDefinition.READ_WHOLE_CONFIG)
                        .setReplyParameters(MIGRATION_WARNINGS_ATTR)
                        .setReadOnly()
                        .build(),
                new MicroProfileHealthMigrateOperation(true));
    }

    private MicroProfileHealthMigrateOperation(boolean describe) {
        this.describe = describe;
    }

    @Override
    public void execute(OperationContext context, ModelNode modelNode) throws OperationFailedException {
        if (!describe && context.getRunningMode() != RunningMode.ADMIN_ONLY) {
            throw MicroProfileHealthLogger.LOGGER.migrateOperationAllowedOnlyInAdminOnly();
        }

        if (context.readResourceFromRoot(context.getCurrentAddress().getParent(), false).hasChild(HEALTH_SUBSYSTEM_ELEMENT)) {
            throw MicroProfileHealthLogger.LOGGER.cannotMigrateHealthAlreadyDefined();
        }

        LinkedHashMap<PathAddress, ModelNode> operations = new LinkedHashMap<>();

        if (!context.readResourceFromRoot(PathAddress.EMPTY_ADDRESS, false).hasChild(HEALTH_EXTENSION_ELEMENT)) {
            addHealthExtension(context, operations);
        }

        ModelNode healthSubsystemAdd = createSubsystemAddFromLegacyModel(
                context.getCurrentAddress().getParent(),
                context.readResource(PathAddress.EMPTY_ADDRESS));

        context.addStep(new OperationStepHandler() {
            @Override
            public void execute(OperationContext operationContext, ModelNode modelNode) throws OperationFailedException {
                addOpsToRemoveSubsystemAndExtension(operations, context);
                addHealthSubsystemAddOp(operations, healthSubsystemAdd);

                if (describe) {
                    ModelNode result = new ModelNode();
                    result.get(MIGRATION_OPERATIONS).set(operations.values());
                    context.getResult().set(result);
                } else {
                    final Map<PathAddress, ModelNode> migrateOpResponses = new LinkedHashMap<>();
                    MultistepUtil.recordOperationSteps(context, operations, migrateOpResponses);

                    context.completeStep(new OperationContext.ResultHandler() {
                        @Override
                        public void handleResult(OperationContext.ResultAction resultAction, OperationContext context, ModelNode modelNode) {
                            final ModelNode result = new ModelNode();
                            ModelNode rw = new ModelNode().setEmptyList();
                            result.get(MIGRATION_WARNINGS).set(rw);
                            if (resultAction == OperationContext.ResultAction.ROLLBACK) {
                                for (Map.Entry<PathAddress, ModelNode> entry : migrateOpResponses.entrySet()) {
                                    if (entry.getValue().hasDefined(FAILURE_DESCRIPTION)) {
                                        //we check for failure description, as every node has 'failed', but one
                                        //the real error has a failure description
                                        //we break when we find the first one, as there will only ever be one failure
                                        //as the op stops after the first failure
                                        ModelNode desc = new ModelNode();
                                        desc.get(OP).set(operations.get(entry.getKey()));
                                        desc.get(RESULT).set(entry.getValue());
                                        result.get(MIGRATION_ERROR).set(desc);
                                        break;
                                    }
                                }
                                context.getFailureDescription().set(new ModelNode(MicroProfileHealthLogger.LOGGER.migrationFailed()));
                            }
                            context.getResult().set(result);
                        }
                    });
                }
            }
        }, MODEL);

    }

    private void addHealthSubsystemAddOp(LinkedHashMap<PathAddress, ModelNode> operations, ModelNode metricsSubsystemAdd) {
        PathAddress addr = PathAddress.pathAddress(metricsSubsystemAdd.get(OP_ADDR));
        operations.put(addr, metricsSubsystemAdd);
    }

    private void addOpsToRemoveSubsystemAndExtension(LinkedHashMap<PathAddress, ModelNode> operations, OperationContext context) {
        ProcessType processType = context.getCallEnvironment().getProcessType();
        boolean domainMode = processType != ProcessType.STANDALONE_SERVER && processType != ProcessType.SELF_CONTAINED;

        // Remove the subsystem
        operations.put(context.getCurrentAddress(), Util.createRemoveOperation(context.getCurrentAddress()));
        if (!domainMode) {
            operations.put(EXTENSION_ADDR, Util.createRemoveOperation(EXTENSION_ADDR));
        }
    }


    private void addHealthExtension(final OperationContext context, final Map<PathAddress, ModelNode> operations) {
        final PathAddress extensionAddress = PathAddress.EMPTY_ADDRESS.append(HEALTH_EXTENSION_ELEMENT);
        OperationEntry addEntry = context.getRootResourceRegistration().getOperationEntry(extensionAddress, ADD);
        final ModelNode addOperation = Util.createAddOperation(extensionAddress);
        if (describe) {
            operations.put(extensionAddress, addOperation);
        } else {
            context.addStep(context.getResult().get(extensionAddress.toString()), addOperation, addEntry.getOperationHandler(), MODEL);
        }
    }

    private ModelNode createSubsystemAddFromLegacyModel(PathAddress parentAddress, Resource legacyResource) {
        ModelNode addOp = Util.createAddOperation(parentAddress.append(SUBSYSTEM, HEALTH_SUBSYSTEM_NAME));
        ModelNode legacyModel = legacyResource.getModel();
        if (legacyModel.hasDefined(SECURITY_ENABLED.getName())) {
            addOp.get(SECURITY_ENABLED.getName()).set(legacyModel.get(SECURITY_ENABLED.getName()));
        }
        return addOp;
    }
}
