/*
 * Copyright The WildFly Authors
 * SPDX-License-Identifier: Apache-2.0
 */

package org.jboss.as.host.controller.ignored;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.CLONE;
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.PROFILE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.REMOVE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.TO_PROFILE;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.PathElement;
import org.jboss.as.controller.SimpleResourceDefinition;
import org.jboss.as.controller.descriptions.ModelDescriptionConstants;
import org.jboss.as.controller.registry.ManagementResourceRegistration;
import org.jboss.as.controller.registry.Resource;
import org.jboss.as.domain.controller.LocalHostControllerInfo;
import org.jboss.as.host.controller.descriptions.HostResolver;
import org.jboss.dmr.ModelNode;

/**
 * Registry for excluded domain-level resources. To be used by slave Host Controllers to ignore requests
 * for particular resources that the host cannot understand. This is a mechanism to allow hosts running earlier
 * AS releases to function as slaves in domains whose master is in a later release.
 *
 *
 * @author Brian Stansberry (c) 2011 Red Hat Inc.
 */
public class IgnoredDomainResourceRegistry {

    private final LocalHostControllerInfo localHostControllerInfo;
    private volatile IgnoredDomainResourceRoot rootResource;
    private IgnoredClonedProfileRegistry ignoredClonedProfileRegistry = new IgnoredClonedProfileRegistry();

    public IgnoredDomainResourceRegistry(LocalHostControllerInfo localHostControllerInfo) {
        this.localHostControllerInfo = localHostControllerInfo;
    }

    /**
     * Returns whether this host should ignore operations from the master domain controller that target
     * the given address.
     *
     * @param address the resource address. Cannot be {@code null}
     *
     * @return {@code true} if the operation should be ignored; {@code false} otherwise
     */
    public boolean isResourceExcluded(final PathAddress address) {
        if (!localHostControllerInfo.isMasterDomainController() && address.size() > 0) {
            IgnoredDomainResourceRoot root = this.rootResource;
            PathElement firstElement = address.getElement(0);
            IgnoreDomainResourceTypeResource typeResource = root == null ? null : root.getChildInternal(firstElement.getKey());
            if (typeResource != null) {
                if (typeResource.hasName(firstElement.getValue())) {
                    return true;
                }
            }
        }
        return false;
    }

    public void registerResources(final ManagementResourceRegistration parentRegistration) {
        parentRegistration.registerSubModel(new ResourceDefinition());
    }

    public Resource.ResourceEntry getRootResource() {
        IgnoredDomainResourceRoot root = new IgnoredDomainResourceRoot(this);
        this.rootResource = root;
        return root;
    }

    public ModelNode getIgnoredResourcesAsModel() {
        IgnoredDomainResourceRoot root = this.rootResource;
        ModelNode model =  (root == null ? new ModelNode() : Resource.Tools.readModel(root));
        return model;
    }

    void publish(IgnoredDomainResourceRoot root) {
        this.rootResource = root;
    }

    boolean isMaster() {
        return localHostControllerInfo.isMasterDomainController();
    }

    public IgnoredClonedProfileRegistry getIgnoredClonedProfileRegistry() {
        return ignoredClonedProfileRegistry;
    }

    private class ResourceDefinition extends SimpleResourceDefinition {

        public ResourceDefinition() {
            super(IgnoredDomainResourceRoot.PATH_ELEMENT, HostResolver.getResolver(ModelDescriptionConstants.IGNORED_RESOURCES));
        }

        @Override
        public void registerChildren(ManagementResourceRegistration resourceRegistration) {
            resourceRegistration.registerSubModel(new IgnoredDomainTypeResourceDefinition());
        }
    }

    /**
     * <p>This class is for internal use only.</p>
     *
     * <p>The main purpose of this registry is to deal with the situation where a profile is explicitly ignored on the slave,
     * and that profile is cloned. So say that the {@code ignored} profile is ignored on the slave, and a call comes to
     * {@code /profile=ignore:clone(to-profile=new)}. Since the {@code ignored} profile is ignored on the slave, the
     * {@code clone} operation never gets called, so no {@code new} profile gets created on the slave. Hence, we need
     * to ignore all subsequent operation invocations on the slave for the {@code new} profile. This class maintains the
     * runtime registry of profiles resulting from clone operations on profiles that were ignored on this slave.</p>
     *
     * <p>The situation above where the {@code new} profile does not get created on the slave due to the {@ignored} profile
     * being explicitly ignored on the slave, will result in the server being put into the {@code reload-required} state
     * since according to the explicit ignores it should really be part of the slave's domain model, and a reload of the
     * slave will download that.
     *
     * <p>If the {@new} profile was also explicitly ignored, we do not add it to the runtime registry and do not put the
     * server into the {@code reload-required} state. The settings in the slave model deal with the ignores for us in
     * that case.</p>
     *
     * <p>Finally in the example above, a call to {@code /profile=new:remove} will remove the {@code new} profile from the
     * runtime registry of profiles resulting from clone operations on profiles that were ignored on this slave.</p>
     *
     * <p>The registry is transactional, using a transaction local copy of the changes made, and should be published/rolled back
     * on transaction completion.</p>
     */
    public class IgnoredClonedProfileRegistry {
        private volatile Set<String> ignoredClonedProfiles = Collections.synchronizedSet(new HashSet<>());
        private volatile Set<String> currentTxIgnoredClonedProfiles;
        private volatile boolean reloadRequired;

        private IgnoredClonedProfileRegistry() {
        }

        /**
         * Checks if an operation should be ignored, and updates the runtime registry and host state as required. Use
         * {@link #isReloadRequired()} to check if the host state should be set to {@code reload-required}.
         *
         * @param operation the operation to check
         * @return whether the operation should be ignored
         */
        public boolean checkIgnoredProfileClone(ModelNode operation) {
            if (!localHostControllerInfo.isMasterDomainController()) {
                PathAddress addr = PathAddress.pathAddress(operation.get(OP_ADDR));
                if (addr.size() > 0) {
                    PathElement first = addr.getElement(0);
                    if (first.getKey().equals(PROFILE)) {
                        String name = operation.get(OP).asString();
                        if (name.equals(CLONE) && isResourceExcluded(addr)) {
                            //We are cloning an ignored profile
                            final String profileName = operation.get(TO_PROFILE).asString();

                            if (!isResourceExcluded(PathAddress.pathAddress(PROFILE, profileName))) {
                                //The new profile is not explicitly ignored, so  add the new profile to the runtime registry
                                //and indicate that the host should be put into the reload-required state.
                                getIgnoredClonedProfiles(true).add(profileName);
                                reloadRequired = true;
                                return true;
                            }
                        } else if (name.equals(REMOVE) && addr.size() == 1) {
                            //We are removing a profile, remove it from the runtime registry if it is there
                            return getIgnoredClonedProfiles(true).remove(first.getValue());
                        } else {
                            //Ignore depending on if it is in the runtime registry
                            return getIgnoredClonedProfiles(false).contains(first.getValue());
                        }
                    }
                }
            }
            return false;
        }

        /**
         * Callback for starting applying a fresh domain model from the DC. This will clear the runtime registry
         */
        public void initializeModelSync() {
            if (!localHostControllerInfo.isMasterDomainController()) {
                //We are resyncing the model, so clear the runtime registry
                getIgnoredClonedProfiles(true).clear();
            }
        }

        /**
         * Callback for when the controller transaction completes. This will publish the changes to the runtime registry
         * if the transaction was committed, and roll them back if it was rolled back.
         *
         * @param rollback {@code true} if the changes should be rolled back, {@code false} if they should be committed.
         */
        public void complete(boolean rollback) {
            if (!localHostControllerInfo.isMasterDomainController()) {
                if (!rollback) {
                    if (currentTxIgnoredClonedProfiles != null) {
                        ignoredClonedProfiles = currentTxIgnoredClonedProfiles;
                    }
                }
                currentTxIgnoredClonedProfiles = null;
                reloadRequired = false;
            }
        }

        /**
         * Check if the changes to the registry should cause the slave to be put into the {@code reload-required} state.
         *
         * @return {@code true} if the host should be put into the {@code reload-required} state, {@code false} otherwise.
         */
        public boolean isReloadRequired() {
            return reloadRequired;
        }

        private Set<String> getIgnoredClonedProfiles(boolean write) {
            if (currentTxIgnoredClonedProfiles != null) {
                return currentTxIgnoredClonedProfiles;
            }
            if (write) {
                currentTxIgnoredClonedProfiles = Collections.synchronizedSet(new HashSet<>(ignoredClonedProfiles));
                return currentTxIgnoredClonedProfiles;
            }
            return ignoredClonedProfiles;
        }

    }
}
