/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     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.apache.brooklyn.core.mgmt.internal;

import static com.google.common.base.Preconditions.checkNotNull;

import java.net.URI;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

import org.apache.brooklyn.api.catalog.BrooklynCatalog;
import org.apache.brooklyn.api.effector.Effector;
import org.apache.brooklyn.api.entity.Application;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.drivers.EntityDriverManager;
import org.apache.brooklyn.api.entity.drivers.downloads.DownloadResolverManager;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.location.LocationRegistry;
import org.apache.brooklyn.api.mgmt.AccessController;
import org.apache.brooklyn.api.mgmt.EntityManager;
import org.apache.brooklyn.api.mgmt.ExecutionContext;
import org.apache.brooklyn.api.mgmt.ExecutionManager;
import org.apache.brooklyn.api.mgmt.LocationManager;
import org.apache.brooklyn.api.mgmt.Scratchpad;
import org.apache.brooklyn.api.mgmt.SubscriptionContext;
import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.api.mgmt.entitlement.EntitlementManager;
import org.apache.brooklyn.api.mgmt.ha.HighAvailabilityManager;
import org.apache.brooklyn.api.mgmt.ha.HighAvailabilityMode;
import org.apache.brooklyn.api.mgmt.ha.ManagementNodeState;
import org.apache.brooklyn.api.mgmt.ha.ManagementPlaneSyncRecord;
import org.apache.brooklyn.api.mgmt.ha.ManagementPlaneSyncRecordPersister;
import org.apache.brooklyn.api.mgmt.rebind.ChangeListener;
import org.apache.brooklyn.api.mgmt.rebind.PersistenceExceptionHandler;
import org.apache.brooklyn.api.mgmt.rebind.RebindExceptionHandler;
import org.apache.brooklyn.api.mgmt.rebind.RebindManager;
import org.apache.brooklyn.api.mgmt.rebind.mementos.BrooklynMementoPersister;
import org.apache.brooklyn.api.mgmt.rebind.mementos.BrooklynMementoRawData;
import org.apache.brooklyn.api.objs.BrooklynObject;
import org.apache.brooklyn.api.typereg.BrooklynTypeRegistry;
import org.apache.brooklyn.config.StringConfigMap;
import org.apache.brooklyn.core.catalog.internal.CatalogInitialization;
import org.apache.brooklyn.core.entity.AbstractEntity;
import org.apache.brooklyn.core.internal.BrooklynProperties;
import org.apache.brooklyn.core.internal.storage.BrooklynStorage;
import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
import org.apache.brooklyn.core.mgmt.ha.OsgiManager;
import org.apache.brooklyn.core.mgmt.usage.UsageManager;
import org.apache.brooklyn.core.objs.proxy.InternalEntityFactory;
import org.apache.brooklyn.core.objs.proxy.InternalLocationFactory;
import org.apache.brooklyn.core.objs.proxy.InternalPolicyFactory;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

public class NonDeploymentManagementContext implements ManagementContextInternal {

    private static final Logger log = LoggerFactory.getLogger(NonDeploymentManagementContext.class);
    
    public enum NonDeploymentManagementContextMode {
        PRE_MANAGEMENT,
        MANAGEMENT_REBINDING,
        MANAGEMENT_STARTING,
        MANAGEMENT_STARTED,
        MANAGEMENT_STOPPING,
        MANAGEMENT_STOPPED;
        
        public boolean isPreManaged() {
            return this == PRE_MANAGEMENT || this == MANAGEMENT_REBINDING;
        }
    }
    
    private final AbstractEntity entity;
    private NonDeploymentManagementContextMode mode;
    private ManagementContextInternal initialManagementContext;
    
    private final QueueingSubscriptionManager qsm;
    private final BasicSubscriptionContext subscriptionContext;
    private NonDeploymentEntityManager entityManager;
    private NonDeploymentLocationManager locationManager;
    private NonDeploymentAccessManager accessManager;
    private NonDeploymentUsageManager usageManager;

    public NonDeploymentManagementContext(AbstractEntity entity, NonDeploymentManagementContextMode mode) {
        this.entity = checkNotNull(entity, "entity");
        this.mode = checkNotNull(mode, "mode");
        qsm = new QueueingSubscriptionManager();
        
        // For subscription flags, see AbstractManagementContext.getSubscriptionContext. This is 
        // needed for callbacks, to ensure the correct entity context is set.
        Map<String, ?> subscriptionFlags = ImmutableMap.of("tags", ImmutableList.of(BrooklynTaskTags.tagForContextEntity(entity)));
        subscriptionContext = new BasicSubscriptionContext(subscriptionFlags, qsm, entity);
        entityManager = new NonDeploymentEntityManager(null);
        locationManager = new NonDeploymentLocationManager(null);
        accessManager = new NonDeploymentAccessManager(null);
        usageManager = new NonDeploymentUsageManager(null);
    }

    @Override
    public String getManagementPlaneId() {
        return (initialManagementContext == null) ? null : initialManagementContext.getManagementPlaneId();
    }
    
    @Override
    public String getManagementNodeId() {
        return (initialManagementContext == null) ? null : initialManagementContext.getManagementNodeId();
    }

    @Override
    public Maybe<URI> getManagementNodeUri() {
        return (initialManagementContext == null) ? Maybe.<URI>absent() : initialManagementContext.getManagementNodeUri();
    }

    public void setManagementContext(ManagementContextInternal val) {
        this.initialManagementContext = checkNotNull(val, "initialManagementContext");
        this.entityManager = new NonDeploymentEntityManager(val);
        this.locationManager = new NonDeploymentLocationManager(val);
        this.accessManager = new NonDeploymentAccessManager(val);
        this.usageManager = new NonDeploymentUsageManager(val);
    }

    @Override
    public String toString() {
        return Objects.toStringHelper(this).add("entity", entity.getId()).add("mode", mode).toString();
    }
    
    public void setMode(NonDeploymentManagementContextMode mode) {
        this.mode = checkNotNull(mode, "mode");
    }
    public NonDeploymentManagementContextMode getMode() {
        return mode;
    }
    
    @Override
    public Collection<Application> getApplications() {
        return Collections.emptyList();
    }

    @Override
    public boolean isRunning() {
        // Assume that the real management context has not been terminated, so always true
        return true;
    }
    
    @Override
    public boolean isStartupComplete() {
        // This mgmt context is only used by items who are not yet fully started.
        // It's slightly misleading as this does not refer to the main mgmt context.
        // OTOH it probably won't be used.  TBC.  -Alex, Apr 2015
        return false;
    }

    @Override
    public InternalEntityFactory getEntityFactory() {
        checkInitialManagementContextReal();
        return initialManagementContext.getEntityFactory();
    }

    @Override
    public InternalLocationFactory getLocationFactory() {
        checkInitialManagementContextReal();
        return initialManagementContext.getLocationFactory();
    }

    @Override
    public InternalPolicyFactory getPolicyFactory() {
        checkInitialManagementContextReal();
        return initialManagementContext.getPolicyFactory();
    }

    @Override
    public EntityManager getEntityManager() {
        return entityManager;
    }
    
    @Override
    public LocationManager getLocationManager() {
        return locationManager;
    }

    @Override
    public AccessManager getAccessManager() {
        return accessManager;
    }

    @Override
    public UsageManager getUsageManager() {
        return usageManager;
    }
    
    @Override
    public Maybe<OsgiManager> getOsgiManager() {
        switch (mode) {
        case PRE_MANAGEMENT:
        case MANAGEMENT_STARTING:
        case MANAGEMENT_STARTED:
            checkInitialManagementContextReal();
            return initialManagementContext.getOsgiManager();
        default:
            return Maybe.absent("Entity " + entity + " is no longer managed; OSGi context no longer available");
        }
    }

    @Override
    public AccessController getAccessController() {
        return getAccessManager().getAccessController();
    }

    @Override
    public ExecutionManager getExecutionManager() {
        checkInitialManagementContextReal();
        return initialManagementContext.getExecutionManager();
    }

    @Override
    public QueueingSubscriptionManager getSubscriptionManager() {
        return qsm;
    }

    @Override
    public synchronized SubscriptionContext getSubscriptionContext(Entity entity) {
        if (!this.entity.equals(entity)) throw new IllegalStateException("Non-deployment context "+this+" can only use a single Entity: has "+this.entity+", but passed "+entity);
        if (mode==NonDeploymentManagementContextMode.MANAGEMENT_STOPPED)
            throw new IllegalStateException("Entity "+entity+" is no longer managed; subscription context not available");
        return subscriptionContext;
    }

    @Override
    public synchronized SubscriptionContext getSubscriptionContext(Location loc) {
        // Should never be called; the NonDeploymentManagementContext is associated with a particular entity, whereas
        // the #getSubscriptionContext(loc) should only be called in the context of a location.
        throw new UnsupportedOperationException();
    }

    @Override
    public ExecutionContext getExecutionContext(Entity entity) {
        if (!this.entity.equals(entity)) throw new IllegalStateException("Non-deployment context "+this+" can only use a single Entity: has "+this.entity+", but passed "+entity);
        if (mode==NonDeploymentManagementContextMode.MANAGEMENT_STOPPED)
            throw new IllegalStateException("Entity "+entity+" is no longer managed; execution context not available");
        checkInitialManagementContextReal();
        return initialManagementContext.getExecutionContext(entity);
    }

    @Override
    public ExecutionContext getServerExecutionContext() {
        return initialManagementContext.getServerExecutionContext();
    }

    // TODO the methods below should delegate to the application?
    @Override
    public EntityDriverManager getEntityDriverManager() {
        checkInitialManagementContextReal();
        return initialManagementContext.getEntityDriverManager();
    }

    @Override
    public DownloadResolverManager getEntityDownloadsManager() {
        checkInitialManagementContextReal();
        return initialManagementContext.getEntityDownloadsManager();
    }

    @Override
    public StringConfigMap getConfig() {
        checkInitialManagementContextReal();
        return initialManagementContext.getConfig();
    }

    @Override
    public BrooklynProperties getBrooklynProperties() {
        checkInitialManagementContextReal();
        return initialManagementContext.getBrooklynProperties();
    }

    @Override
    public Scratchpad getScratchpad() {
        checkInitialManagementContextReal();
        return initialManagementContext.getScratchpad();
    }

    @Override
    public BrooklynStorage getStorage() {
        checkInitialManagementContextReal();
        return initialManagementContext.getStorage();
    }
    
    @Override
    public RebindManager getRebindManager() {
        // There was a race where EffectorUtils on invoking an effector calls:
        //     mgmtSupport.getEntityChangeListener().onEffectorCompleted(eff);
        // but where the entity/app may be being unmanaged concurrently (e.g. calling app.stop()).
        // So now we allow the change-listener to be called.
        
        if (isInitialManagementContextReal()) {
            return initialManagementContext.getRebindManager();
        } else {
            return new NonDeploymentRebindManager();
        }
    }

    @Override
    public HighAvailabilityManager getHighAvailabilityManager() {
        if (isInitialManagementContextReal()) {
            return initialManagementContext.getHighAvailabilityManager();
        } else {
            return new NonDeploymentHighAvailabilityManager();
        }
    }

    @Override
    public LocationRegistry getLocationRegistry() {
        checkInitialManagementContextReal();
        return initialManagementContext.getLocationRegistry();
    }

    @Override
    public BrooklynCatalog getCatalog() {
        checkInitialManagementContextReal();
        return initialManagementContext.getCatalog();
    }

    @Override
    public BrooklynTypeRegistry getTypeRegistry() {
        checkInitialManagementContextReal();
        return initialManagementContext.getTypeRegistry();
    }
    
    @Override
    public ClassLoader getCatalogClassLoader() {
        checkInitialManagementContextReal();
        return initialManagementContext.getCatalogClassLoader();
    }
    
    @Override
    public EntitlementManager getEntitlementManager() {
        checkInitialManagementContextReal();
        return initialManagementContext.getEntitlementManager();
    }
    
    @Override
    public <T> T invokeEffectorMethodSync(final Entity entity, final Effector<T> eff, final Map<String,?> args) throws ExecutionException {
        throw new IllegalStateException("Non-deployment context "+this+" is not valid for this operation: cannot invoke effector "+eff+" on entity "+entity);
    }
    
    @Override
    public <T> Task<T> invokeEffector(final Entity entity, final Effector<T> eff, @SuppressWarnings("rawtypes") final Map parameters) {
        throw new IllegalStateException("Non-deployment context "+this+" is not valid for this operation: cannot invoke effector "+eff+" on entity "+entity);
    }

    @Override
    public ClassLoader getBaseClassLoader() {
        checkInitialManagementContextReal();
        return initialManagementContext.getBaseClassLoader();
    }

    @Override
    public Iterable<URL> getBaseClassPathForScanning() {
        checkInitialManagementContextReal();
        return initialManagementContext.getBaseClassPathForScanning();
    }

    @Override
    public void addEntitySetListener(CollectionChangeListener<Entity> listener) {
        checkInitialManagementContextReal();
        initialManagementContext.addEntitySetListener(listener);
    }

    @Override
    public void removeEntitySetListener(CollectionChangeListener<Entity> listener) {
        checkInitialManagementContextReal();
        initialManagementContext.removeEntitySetListener(listener);
    }

    @Override
    public void terminate() {
        if (isInitialManagementContextReal()) {
            initialManagementContext.terminate();
        } else {
            // no-op; the non-deployment management context has nothing needing terminated
        }
    }

    @Override
    public long getTotalEffectorInvocations() {
        if (isInitialManagementContextReal()) {
            return initialManagementContext.getTotalEffectorInvocations();
        } else {
            return 0;
        }
    }

    @Override
    public void setBaseClassPathForScanning(Iterable<URL> urls) {
        checkInitialManagementContextReal();
        initialManagementContext.setBaseClassPathForScanning(urls);
    }

    @Override
    public void setManagementNodeUri(URI uri) {
        checkInitialManagementContextReal();
        initialManagementContext.setManagementNodeUri(uri);
    }

    @Override
    public void prePreManage(Entity entity) {
        // should throw?  but in 0.7.0-SNAPSHOT it was no-op
        log.warn("Ignoring call to prePreManage("+entity+") on "+this);
    }

    @Override
    public void prePreManage(Location location) {
        // should throw?  but in 0.7.0-SNAPSHOT it was no-op
        log.warn("Ignoring call to prePreManage("+location+") on "+this);
    }

    private boolean isInitialManagementContextReal() {
        return (initialManagementContext != null && !(initialManagementContext instanceof NonDeploymentManagementContext));
    }
    
    private void checkInitialManagementContextReal() {
        if (!isInitialManagementContextReal()) {
            throw new IllegalStateException("Non-deployment context "+this+" is not valid for this operation.");
        }
    }
    
    @Override
    public void reloadBrooklynProperties() {
        checkInitialManagementContextReal();
        initialManagementContext.reloadBrooklynProperties();
    }
    
    @Override
    public void addPropertiesReloadListener(PropertiesReloadListener listener) {
        checkInitialManagementContextReal();
        initialManagementContext.addPropertiesReloadListener(listener);
    }
    
    @Override
    public void removePropertiesReloadListener(PropertiesReloadListener listener) {
        checkInitialManagementContextReal();
        initialManagementContext.removePropertiesReloadListener(listener);
    }

    @Override
    public BrooklynObject lookup(String id) {
        checkInitialManagementContextReal();
        return initialManagementContext.lookup(id);
    }

    @Override
    public <T extends BrooklynObject> T lookup(String id, Class<T> type) {
        checkInitialManagementContextReal();
        return initialManagementContext.lookup(id, type);
    }

    @Override
    public List<Throwable> errors() {
        checkInitialManagementContextReal();
        return initialManagementContext.errors();
    }

    @Override
    public CatalogInitialization getCatalogInitialization() {
        checkInitialManagementContextReal();
        return initialManagementContext.getCatalogInitialization();
    }
    
    @Override
    public void setCatalogInitialization(CatalogInitialization catalogInitialization) {
        checkInitialManagementContextReal();
        initialManagementContext.setCatalogInitialization(catalogInitialization);
    }

    @Override
    public ExternalConfigSupplierRegistry getExternalConfigProviderRegistry() {
        checkInitialManagementContextReal();
        return initialManagementContext.getExternalConfigProviderRegistry();
    }

    /**
     * For when the initial management context is not "real"; the changeListener is a no-op, but everything else forbidden.
     * 
     * @author aled
     */
    private class NonDeploymentRebindManager implements RebindManager {

        @Override
        public ChangeListener getChangeListener() {
            return ChangeListener.NOOP;
        }

        @Override
        public void setPersister(BrooklynMementoPersister persister) {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }

        @Override
        public void setPersister(BrooklynMementoPersister persister, PersistenceExceptionHandler exceptionHandler) {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }

        @Override
        public BrooklynMementoPersister getPersister() {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }

        @Override
        public List<Application> rebind() {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }

        @Override
        public List<Application> rebind(ClassLoader classLoader) {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }

        @Override
        public List<Application> rebind(ClassLoader classLoader, RebindExceptionHandler exceptionHandler) {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        
        @Override
        public List<Application> rebind(ClassLoader classLoader, RebindExceptionHandler exceptionHandler, ManagementNodeState mode) {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }

        @Override
        public void startPersistence() {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        
        @Override
        public void stopPersistence() {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }

        @Override
        public void startReadOnly(ManagementNodeState state) {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        
        @Override
        public void stopReadOnly() {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }

        @Override
        public void start() {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        
        @Override
        public void stop() {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }

        @Override
        public void waitForPendingComplete(Duration timeout, boolean canTrigger) throws InterruptedException, TimeoutException {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        @Override
        public void forcePersistNow() {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        @Override
        public void forcePersistNow(boolean full, PersistenceExceptionHandler exceptionHandler) {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        @Override
        public BrooklynMementoRawData retrieveMementoRawData() {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        @Override
        public boolean isAwaitingInitialRebind() {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        
        @Override
        public Map<String, Object> getMetrics() {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
    }

    /**
     * For when the initial management context is not "real".
     * 
     * @author aled
     */
    private class NonDeploymentHighAvailabilityManager implements HighAvailabilityManager {
        @Override
        public ManagementNodeState getNodeState() {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        @Override
        public boolean isRunning() {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        @Override
        public HighAvailabilityManager setPersister(ManagementPlaneSyncRecordPersister persister) {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        @Override
        public void disabled() {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        @Override
        public void start(HighAvailabilityMode startMode) {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        @Override
        public void stop() {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        @Override
        public ManagementPlaneSyncRecordPersister getPersister() {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        @Override
        public ManagementPlaneSyncRecord getLastManagementPlaneSyncRecord() {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        @Override
        public ManagementPlaneSyncRecord loadManagementPlaneSyncRecord(boolean x) {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        @Override
        public void changeMode(HighAvailabilityMode startMode) {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        @Override
        public void setPriority(long priority) {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        @Override
        public long getPriority() {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        @Override
        public Map<String, Object> getMetrics() {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        @Override
        public void publishClearNonMaster() {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
        @Override
        public long getLastStateChange() {
            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
        }
    }
}
