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

package org.jboss.as.controller.access.constraint;

import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.jboss.as.controller.logging.ControllerLogger;
import org.jboss.as.controller.access.Action;
import org.jboss.as.controller.access.HostEffect;
import org.jboss.as.controller.access.JmxAction;
import org.jboss.as.controller.access.JmxTarget;
import org.jboss.as.controller.access.TargetAttribute;
import org.jboss.as.controller.access.TargetResource;
import org.jboss.as.controller.access.rbac.StandardRole;

/**
 * Constraint related to whether the target resource is associated with one or more managed domain hosts.
 *
 * @author Brian Stansberry (c) 2013 Red Hat Inc.
 */
public class HostEffectConstraint extends AbstractConstraint implements Constraint, ScopingConstraint {


    public static final ScopingConstraintFactory FACTORY = new Factory();

    private static final HostEffectConstraint GLOBAL_USER = new HostEffectConstraint(true);
    private static final HostEffectConstraint GLOBAL_REQUIRED = new HostEffectConstraint(false);

    private final boolean user;
    private final boolean global;
    private final HostsHolder hostsHolder;
    private final boolean readOnly;
    private final HostEffectConstraint readOnlyConstraint;

    private HostEffectConstraint(final boolean user) {
        super();
        this.user = user;
        this.global = true;
        this.readOnly = false;
        this.readOnlyConstraint = null;
        this.hostsHolder = new HostsHolder();
    }

    private HostEffectConstraint(Set<String> allowed) {
        super();
        this.user = false;
        this.global = false;
        this.hostsHolder = new HostsHolder(allowed);
        this.readOnly = false;
        this.readOnlyConstraint = null;
    }

    public HostEffectConstraint(List<String> allowed) {
        super();
        this.user = true;
        this.global = false;
        this.hostsHolder = new HostsHolder(allowed);
        this.readOnly = false;
        this.readOnlyConstraint = new HostEffectConstraint(this.hostsHolder, true);
    }

    /**
     * Creates the constraint the standard constraint will return from {@link #getOutofScopeReadConstraint()}
     * Only call from {@link HostEffectConstraint#HostEffectConstraint(java.util.List)}
     */
    private HostEffectConstraint(HostsHolder hostsHolder, boolean readOnly) {
        super();
        this.user = true;
        this.global = false;
        this.hostsHolder = hostsHolder;
        this.readOnly = readOnly;
        this.readOnlyConstraint = null;
    }

    public void setAllowedHosts(List<String> allowed) {
        assert !global : "constraint is global";
        assert readOnlyConstraint != null : "invalid cast";
        this.hostsHolder.specific = new LinkedHashSet<String>(allowed);
    }

    @Override
    public boolean violates(Constraint other, Action.ActionEffect actionEffect) {
        if (other instanceof HostEffectConstraint) {
            HostEffectConstraint hec = (HostEffectConstraint) other;
            Set<String> ourSpecific = hostsHolder.specific;
            Set<String> hecSpecific = hec.hostsHolder.specific;
            if (user) {
                assert !hec.user : "illegal comparison";
                if (readOnly) {
                    // Allow global or any matching server group
                    if (!hec.global) {
                        boolean anyMatch = anyMatch(ourSpecific, hecSpecific);
                        if (!anyMatch) ControllerLogger.ACCESS_LOGGER.tracef("read-only host constraint violated " +
                                "for action %s due to no match between hosts %s and allowed hosts %s",
                                actionEffect, hecSpecific, ourSpecific);
                        return !anyMatch;
                    }
                } else if (!global) {
                    if (hec.global) {
                        // Only the readOnlyConstraint gets global
                        ControllerLogger.ACCESS_LOGGER.tracef("host constraint violated for action %s due to " +
                                "requirement for access to global resources", actionEffect);
                        return true;
                    } else {
                        if (actionEffect == Action.ActionEffect.WRITE_RUNTIME || actionEffect == Action.ActionEffect.WRITE_CONFIG) {
                            //  Writes must not effect other groups
                            boolean containsAll = ourSpecific.containsAll(hecSpecific);
                            if (!containsAll) {
                                ControllerLogger.ACCESS_LOGGER.tracef("host constraint violated for action %s due to " +
                                        "mismatch of hosts %s vs hosts %s", actionEffect, hecSpecific, ourSpecific);
                            }
                            return !containsAll;
                        } else {
                            // Reads ok as long as one of our groups match
                            boolean anyMatch = anyMatch(ourSpecific, hecSpecific);
                            if (!anyMatch) ControllerLogger.ACCESS_LOGGER.tracef("host constraint violated " +
                                    "for action %s due to no match between hosts %s and allowed hosts %s",
                                    actionEffect, hecSpecific, ourSpecific);
                            return !anyMatch;
                        }
                    } // else fall through
                }
            } else {
                assert hec.user : "illegal comparison";
                return other.violates(this, actionEffect);
            }
        }
        return false;
    }

    private boolean anyMatch(Set<String> ourSpecific, Set<String> hecSpecific) {

        boolean matched = false;
        for (String ourGroup : ourSpecific) {
            if (hecSpecific.contains(ourGroup)) {
                matched = true;
                break;
            }
        }
        if (!matched) {
            // WFLY-2089
            matched = hecSpecific.size() == 1 && hecSpecific.contains("*");
        }
        return matched;
    }


    @Override
    public boolean replaces(Constraint other) {
        return other instanceof HostEffectConstraint && (readOnly || readOnlyConstraint != null);
    }

    // Scoping Constraint

    @Override
    public ScopingConstraintFactory getFactory() {
        assert readOnlyConstraint != null : "invalid cast";
        return FACTORY;
    }

    @Override
    public Constraint getStandardConstraint() {
        assert readOnlyConstraint != null : "invalid cast";
        return this;
    }

    @Override
    public Constraint getOutofScopeReadConstraint() {
        assert readOnlyConstraint != null : "invalid cast";
        return readOnlyConstraint;
    }

    private static class HostsHolder {
        private volatile Set<String> specific = new LinkedHashSet<String>();
        private HostsHolder() {
            // no-op
        }
        private HostsHolder(Collection<String> hosts) {
            this.specific.addAll(hosts);
        }
    }

    private static class Factory extends AbstractConstraintFactory implements ScopingConstraintFactory {

        @Override
        public Constraint getStandardUserConstraint(StandardRole role, Action.ActionEffect actionEffect) {
            return GLOBAL_USER;
        }

        @Override
        public Constraint getRequiredConstraint(Action.ActionEffect actionEffect, Action action, TargetAttribute target) {
            return getRequiredConstraint(target.getHostEffect());
        }

        @Override
        public Constraint getRequiredConstraint(Action.ActionEffect actionEffect, Action action, TargetResource target) {
            return getRequiredConstraint(target.getHostEffect());
        }

        private Constraint getRequiredConstraint(HostEffect hostEffect) {
            if (hostEffect == null || hostEffect.isHostEffectGlobal()) {
                return GLOBAL_REQUIRED;
            }
            return new HostEffectConstraint(hostEffect.getAffectedHosts());
        }

        @Override
        public Constraint getRequiredConstraint(Action.ActionEffect actionEffect, JmxAction action, JmxTarget target) {
            return getRequiredConstraint(target.getHostEffect());
        }

        @Override
        protected int internalCompare(AbstractConstraintFactory other) {
            // We prefer going first
            return this.equals(other) ? 0 : -1;
        }
    }
}
