/*
 * 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.rest.util;

import static com.google.common.collect.Iterables.transform;
import static org.apache.brooklyn.rest.util.WebResourceUtils.notFound;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.Future;

import javax.ws.rs.core.MediaType;

import org.apache.brooklyn.api.catalog.BrooklynCatalog;
import org.apache.brooklyn.api.entity.Application;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.location.LocationRegistry;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.api.objs.EntityAdjunct;
import org.apache.brooklyn.api.policy.Policy;
import org.apache.brooklyn.api.sensor.Enricher;
import org.apache.brooklyn.api.sensor.Feed;
import org.apache.brooklyn.api.typereg.BrooklynTypeRegistry;
import org.apache.brooklyn.api.typereg.RegisteredType;
import org.apache.brooklyn.camp.brooklyn.BrooklynCampConstants;
import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslComponent;
import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslComponent.Scope;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.entity.trait.Startable;
import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
import org.apache.brooklyn.core.mgmt.entitlement.Entitlements;
import org.apache.brooklyn.core.mgmt.entitlement.Entitlements.StringAndArgument;
import org.apache.brooklyn.core.objs.BrooklynTypes;
import org.apache.brooklyn.enricher.stock.Enrichers;
import org.apache.brooklyn.entity.stock.BasicApplication;
import org.apache.brooklyn.rest.domain.ApplicationSpec;
import org.apache.brooklyn.rest.domain.EntitySpec;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.collections.MutableSet;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.javalang.Reflections;
import org.apache.brooklyn.util.javalang.coerce.PrimitiveStringTypeCoercions;
import org.apache.brooklyn.util.net.Urls;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.io.Files;

public class BrooklynRestResourceUtils {

    private static final Logger log = LoggerFactory.getLogger(BrooklynRestResourceUtils.class);

    private final ManagementContext mgmt;
    
    public BrooklynRestResourceUtils(ManagementContext mgmt) {
        Preconditions.checkNotNull(mgmt, "mgmt");
        this.mgmt = mgmt;
    }

    public BrooklynTypeRegistry getTypeRegistry() {
        return mgmt.getTypeRegistry();
    }
    
    public BrooklynCatalog getCatalog() {
        return mgmt.getCatalog();
    }
    
    public ClassLoader getCatalogClassLoader() {
        return mgmt.getCatalogClassLoader();
    }
    
    public LocationRegistry getLocationRegistry() {
        return mgmt.getLocationRegistry();
    }

    /** finds the policy indicated by the given ID or name.
     * @see {@link #getEntity(String,String)}; it then searches the policies of that
     * entity for one whose ID or name matches that given.
     * <p>
     * 
     * @throws 404 or 412 (unless input is null in which case output is null) */
    public Policy getPolicy(String application, String entity, String policy) {
        return getPolicy(getEntity(application, entity), policy);
    }
    
    /** finds the policy indicated by the given ID or name.
     * @see {@link #getEntity(String,String)}; it then searches the policies of that
     * entity for one whose ID or name matches that given.
     * <p>
     * 
     * @throws 404 or 412 (unless input is null in which case output is null) */
    public EntityAdjunct getAdjunct(String application, String entity, String adjunct) {
        return getAdjunct(getEntity(application, entity), adjunct);
    }

    /** finds the policy indicated by the given ID or name.
     * @see {@link #getPolicy(String,String,String)}.
     * <p>
     * 
     * @throws 404 or 412 (unless input is null in which case output is null) */
    public Policy getPolicy(Entity entity, String policy) {
        if (policy==null) return null;

        for (Policy p: entity.policies()) {
            if (policy.equals(p.getId())) return p;
        }
        for (Policy p: entity.policies()) {
            if (policy.equals(p.getDisplayName())) return p;
        }
        
        throw WebResourceUtils.notFound("Cannot find policy '%s' in entity '%s'", policy, entity);
    }
    
    /** finds the policy indicated by the given ID or name.
     * @see {@link #getAdjunct(String,String,String)}.
     * <p>
     * 
     * @throws 404 or 412 (unless input is null in which case output is null) */
    public EntityAdjunct getAdjunct(Entity entity, String adjunct) {
        if (adjunct==null) return null;

        for (Policy p: entity.policies()) {
            if (adjunct.equals(p.getId())) return p;
        }
        for (Enricher p: entity.enrichers()) {
            if (adjunct.equals(p.getId())) return p;
        }
        for (Feed p: ((EntityInternal)entity).feeds()) {
            if (adjunct.equals(p.getId())) return p;
        }
        
        for (Policy p: entity.policies()) {
            if (adjunct.equals(p.getDisplayName())) return p;
        }
        for (Enricher p: entity.enrichers()) {
            if (adjunct.equals(p.getDisplayName())) return p;
        }
        for (Feed p: ((EntityInternal)entity).feeds()) {
            if (adjunct.equals(p.getDisplayName())) return p;
        }
        
        throw WebResourceUtils.notFound("Cannot find adjunct '%s' in entity '%s'", adjunct, entity);
    }

    /** finds the entity indicated by the given ID or name
     * <p>
     * prefers ID based lookup in which case appId is optional, and if supplied will be enforced.
     * optionally the name can be supplied, for cases when paths should work across versions,
     * in which case names will be searched recursively (and the application is required). 
     * 
     * @throws 404 or 412 (unless input is null in which case output is null) */
    public Entity getEntity(String application, String entity) {
        if (entity==null) return null;
        Application app = application!=null ? getApplication(application) : null;
        Entity e = mgmt.getEntityManager().getEntity(entity);
        
        if (e!=null) {
            if (!Entitlements.isEntitled(mgmt.getEntitlementManager(), Entitlements.SEE_ENTITY, e)) {
                throw WebResourceUtils.notFound("Cannot find entity '%s': no known ID and application not supplied for searching", entity);
            }
            
            if (app==null || app.equals(findTopLevelApplication(e))) return e;
            throw WebResourceUtils.preconditionFailed("Application '%s' specified does not match application '%s' to which entity '%s' (%s) is associated", 
                    application, e.getApplication()==null ? null : e.getApplication().getId(), entity, e);
        }
        if (application==null)
            throw WebResourceUtils.notFound("Cannot find entity '%s': no known ID and application not supplied for searching", entity);
        
        assert app!=null : "null app should not be returned from getApplication";
        e = searchForEntityNamed(app, entity);
        if (e!=null) return e;
        throw WebResourceUtils.notFound("Cannot find entity '%s' in application '%s' (%s)", entity, application, app);
    }
    
    private Application findTopLevelApplication(Entity e) {
        // For nested apps, e.getApplication() can return its direct parent-app rather than the root app
        // (particularly if e.getApplication() was called before the parent-app was wired up to its parent,
        // because that call causes the application to be cached).
        // Therefore we continue to walk the hierarchy until we find an "orphaned" application at the top.
        
        Application app = e.getApplication();
        while (app != null && !app.equals(app.getApplication())) {
            app = app.getApplication();
        }
        return app;
    }

    /** looks for the given application instance, first by ID then by name
     * 
     * @throws 404 if not found, or not entitled
     */
    public Application getApplication(String application) {
        Entity e = mgmt.getEntityManager().getEntity(application);
        if (!Entitlements.isEntitled(mgmt.getEntitlementManager(), Entitlements.SEE_ENTITY, e)) {
            throw notFound("Application '%s' not found", application);
        }
        
        if (e != null && e instanceof Application) return (Application) e;
        for (Application app : mgmt.getApplications()) {
            if (app.getId().equals(application)) return app;
            if (application.equalsIgnoreCase(app.getDisplayName())) return app;
        }
        
        throw notFound("Application '%s' not found", application);
    }

    /** walks the hierarchy (depth-first) at root (often an Application) looking for
     * an entity matching the given ID or name; returns the first such entity, or null if none found
     **/
    public Entity searchForEntityNamed(Entity root, String entity) {
        if (root.getId().equals(entity) || entity.equals(root.getDisplayName())) return root;
        for (Entity child: root.getChildren()) {
            Entity result = searchForEntityNamed(child, entity);
            if (result!=null) return result;
        }
        return null;
    }

    private class FindItemAndClass {
        String catalogItemId;
        Class<? extends Entity> clazz;
        
        @SuppressWarnings({ "unchecked" })
        private FindItemAndClass inferFrom(String type) {
            RegisteredType item = mgmt.getTypeRegistry().get(type);
            
            if (item != null) {
                return setAs(
                    mgmt.getTypeRegistry().createSpec(item, null, org.apache.brooklyn.api.entity.EntitySpec.class).getType(),
                    item.getId());
            } else {
                try {
                    setAs(
                        (Class<? extends Entity>) getCatalog().getRootClassLoader().loadClass(type),
                        null);
                    log.info("Catalog does not contain item for type {}; loaded class directly instead", type);
                    return this;
                } catch (ClassNotFoundException e2) {
                    log.warn("No catalog item for type {}, and could not load class directly; rethrowing", type);
                    throw new NoSuchElementException("Unable to find catalog item for type "+type);
                }
            }
        }

        private FindItemAndClass setAs(Class<? extends Entity> clazz, String catalogItemId) {
            this.clazz = clazz;
            this.catalogItemId = catalogItemId;
            return this;
        }
    }
    
    public Application create(ApplicationSpec spec) {
        log.warn("Using deprecated functionality (as of 0.9.0), ApplicationSpec style (pre CAMP plans). " +
                    "Transition to actively supported spec plans.");
        log.debug("REST creating application instance for {}", spec);
        
        if (!Entitlements.isEntitled(mgmt.getEntitlementManager(), Entitlements.DEPLOY_APPLICATION, spec)) {
            throw WebResourceUtils.forbidden("User '%s' is not authorized to deploy application %s",
                Entitlements.getEntitlementContext().user(), spec);
        }
        
        final String type = spec.getType();
        final String name = spec.getName();
        final Map<String,String> configO = spec.getConfig();
        final Set<EntitySpec> entities = (spec.getEntities() == null) ? ImmutableSet.<EntitySpec>of() : spec.getEntities();
        
        final Application instance;

        // Load the class; first try to use the appropriate catalog item; but then allow anything that is on the classpath
        FindItemAndClass itemAndClass;
        if (Strings.isEmpty(type)) {
            itemAndClass = new FindItemAndClass().setAs(BasicApplication.class, null);
        } else {
            itemAndClass = new FindItemAndClass().inferFrom(type);
        }
        
        if (!Entitlements.isEntitled(mgmt.getEntitlementManager(), Entitlements.INVOKE_EFFECTOR, null)) {
            throw WebResourceUtils.forbidden("User '%s' is not authorized to create application from applicationSpec %s",
                Entitlements.getEntitlementContext().user(), spec);
        }

        try {
            if (Application.class.isAssignableFrom(itemAndClass.clazz)) {
                org.apache.brooklyn.api.entity.EntitySpec<?> coreSpec = toCoreEntitySpec(itemAndClass.clazz, name, configO, itemAndClass.catalogItemId);
                configureRenderingMetadata(spec, coreSpec);
                for (EntitySpec entitySpec : entities) {
                    log.info("REST creating instance for entity {}", entitySpec.getType());
                    coreSpec.child(toCoreEntitySpec(entitySpec));
                }

                log.info("REST placing '{}' under management", spec.getName() != null ? spec.getName() : spec);
                instance = (Application) mgmt.getEntityManager().createEntity(coreSpec);

            } else if (Entity.class.isAssignableFrom(itemAndClass.clazz)) {
                if (entities.size() > 0)
                    log.warn("Cannot supply additional entities when using a non-application entity; ignoring in spec {}", spec);

                org.apache.brooklyn.api.entity.EntitySpec<?> coreSpec = toCoreEntitySpec(BasicApplication.class, name, configO, itemAndClass.catalogItemId);
                configureRenderingMetadata(spec, coreSpec);

                coreSpec.child(toCoreEntitySpec(itemAndClass.clazz, name, configO, itemAndClass.catalogItemId)
                    .configure(BrooklynCampConstants.PLAN_ID, "soleChildId"));
                coreSpec.enricher(Enrichers.builder()
                    .propagatingAllBut(Attributes.SERVICE_UP, Attributes.SERVICE_NOT_UP_INDICATORS, 
                        Attributes.SERVICE_STATE_ACTUAL, Attributes.SERVICE_STATE_EXPECTED, 
                        Attributes.SERVICE_PROBLEMS)
                        .from(new DslComponent(Scope.CHILD, "soleChildId").newTask())
                        .build());

                log.info("REST placing '{}' under management", spec.getName());
                instance = (Application) mgmt.getEntityManager().createEntity(coreSpec);

            } else {
                throw new IllegalArgumentException("Class " + itemAndClass.clazz + " must extend one of ApplicationBuilder, Application or Entity");
            }

            return instance;

        } catch (Exception e) {
            log.error("REST failed to create application: " + e, e);
            throw Exceptions.propagate(e);
        }
    }
    
    public Task<?> start(Application app, ApplicationSpec spec) {
        return start(app, getLocationsManaged(spec));
    }

    public Task<?> start(Application app, List<? extends Location> locations) {
        return Entities.invokeEffector(app, app, Startable.START,
                MutableMap.of("locations", locations));
    }

    public List<Location> getLocationsManaged(ApplicationSpec spec) {
        // Start all the managed entities by asking the app instance to start in background
        Function<String, Location> buildLocationFromId = new Function<String, Location>() {
            @Override
            public Location apply(String id) {
                id = fixLocation(id);
                return getLocationRegistry().getLocationManaged(id);
            }
        };

        ArrayList<Location> locations = Lists.newArrayList(transform(spec.getLocations(), buildLocationFromId));
        return locations;
    }

    private org.apache.brooklyn.api.entity.EntitySpec<? extends Entity> toCoreEntitySpec(org.apache.brooklyn.rest.domain.EntitySpec spec) {
        String type = spec.getType();
        String name = spec.getName();
        Map<String, String> config = (spec.getConfig() == null) ? Maps.<String,String>newLinkedHashMap() : Maps.newLinkedHashMap(spec.getConfig());

        FindItemAndClass itemAndClass = new FindItemAndClass().inferFrom(type);
        
        final Class<? extends Entity> clazz = itemAndClass.clazz;
        org.apache.brooklyn.api.entity.EntitySpec<? extends Entity> result;
        if (clazz.isInterface()) {
            result = org.apache.brooklyn.api.entity.EntitySpec.create(clazz);
        } else {
            result = org.apache.brooklyn.api.entity.EntitySpec.create(Entity.class).impl(clazz).additionalInterfaces(Reflections.getAllInterfaces(clazz));
        }
        result.catalogItemId(itemAndClass.catalogItemId);
        if (!Strings.isEmpty(name)) result.displayName(name);
        result.configure( convertFlagsToKeys(result.getType(), config) );
        configureRenderingMetadata(spec, result);
        return result;
    }
    
    protected void configureRenderingMetadata(ApplicationSpec input, org.apache.brooklyn.api.entity.EntitySpec<?> entity) {
        entity.configure(getRenderingConfigurationFor(input.getType()));
    }

    protected void configureRenderingMetadata(EntitySpec input, org.apache.brooklyn.api.entity.EntitySpec<?> entity) {
        entity.configure(getRenderingConfigurationFor(input.getType()));
    }

    protected Map<?, ?> getRenderingConfigurationFor(String catalogId) {
        MutableMap<Object, Object> result = MutableMap.of();
        RegisteredType item = mgmt.getTypeRegistry().get(catalogId);
        if (item==null) return result;
        
        result.addIfNotNull("iconUrl", item.getIconUrl());
        return result;
    }
    
    @SuppressWarnings({ "rawtypes", "unchecked" })
    private <T extends Entity> org.apache.brooklyn.api.entity.EntitySpec<?> toCoreEntitySpec(Class<T> clazz, String name, Map<String,String> configO, String catalogItemId) {
        Map<String, String> config = (configO == null) ? Maps.<String,String>newLinkedHashMap() : Maps.newLinkedHashMap(configO);
        
        org.apache.brooklyn.api.entity.EntitySpec<? extends Entity> result;
        if (clazz.isInterface()) {
            result = org.apache.brooklyn.api.entity.EntitySpec.create(clazz);
        } else {
            // If this is a concrete class, particularly for an Application class, we want the proxy
            // to expose all interfaces it implements.
            Class interfaceclazz = (Application.class.isAssignableFrom(clazz)) ? Application.class : Entity.class;
            Set<Class<?>> additionalInterfaceClazzes = Reflections.getInterfacesIncludingClassAncestors(clazz);
            result = org.apache.brooklyn.api.entity.EntitySpec.create(interfaceclazz).impl(clazz).additionalInterfaces(additionalInterfaceClazzes);
        }
        
        result.catalogItemId(catalogItemId);
        if (!Strings.isEmpty(name)) result.displayName(name);
        result.configure( convertFlagsToKeys(result.getImplementation(), config) );
        return result;
    }

    private Map<?,?> convertFlagsToKeys(Class<? extends Entity> javaType, Map<?, ?> config) {
        if (config==null || config.isEmpty() || javaType==null) return config;
        
        Map<String, ConfigKey<?>> configKeys = BrooklynTypes.getDefinedConfigKeys(javaType);
        Map<Object,Object> result = new LinkedHashMap<Object,Object>();
        for (Map.Entry<?,?> entry: config.entrySet()) {
            log.debug("Setting key {} to {} for REST creation of {}", new Object[] { entry.getKey(), entry.getValue(), javaType});
            Object key = configKeys.get(entry.getKey());
            if (key==null) {
                log.warn("Unrecognised config key {} passed to {}; will be treated as flag (and likely ignored)", entry.getKey(), javaType);
                key = entry.getKey();
            }
            result.put(key, entry.getValue());
        }
        return result;
    }
    
    public Task<?> destroy(final Application application) {
        return mgmt.getExecutionManager().submit(
                MutableMap.of("displayName", "destroying "+application,
                        "description", "REST call to destroy application "+application.getDisplayName()+" ("+application+")"),
                new Runnable() {
            @Override
            public void run() {
                ((EntityInternal)application).destroy();
                mgmt.getEntityManager().unmanage(application);
            }
        });
    }
    
    public Task<?> expunge(final Entity entity, final boolean release) {
        if (mgmt.getEntitlementManager().isEntitled(Entitlements.getEntitlementContext(),
                Entitlements.INVOKE_EFFECTOR, Entitlements.EntityAndItem.of(entity, 
                    StringAndArgument.of("expunge", MutableMap.of("release", release))))) {
            Map<String, Object> flags = MutableMap.<String, Object>of("displayName", "expunging " + entity, "description", "REST call to expunge entity "
                    + entity.getDisplayName() + " (" + entity + ")");
            if (Entitlements.getEntitlementContext() != null) {
                flags.put("tags", MutableSet.of(BrooklynTaskTags.tagForEntitlement(Entitlements.getEntitlementContext())));
            }
            return mgmt.getExecutionManager().submit(
                    flags, new Runnable() {
                        @Override
                        public void run() {
                            if (release)
                                Entities.destroyCatching(entity);
                            else
                                mgmt.getEntityManager().unmanage(entity);
                        }
                    });
        }
        throw WebResourceUtils.forbidden("User '%s' is not authorized to expunge entity %s",
                    Entitlements.getEntitlementContext().user(), entity);
    }

    @Deprecated
    public static String fixLocation(String locationId) {
        if (locationId.startsWith("/locations/") || locationId.startsWith("/v1/locations/")) {
            log.warn("REST API using legacy URI syntax for location: "+locationId);
            locationId = Strings.removeFromStart(locationId, "/v1/locations/");
            locationId = Strings.removeFromStart(locationId, "/locations/");
        }
        return locationId;
    }

    public Object getObjectValueForDisplay(Object value) {
        if (value==null) return null;
        // currently everything converted to string, expanded if it is a "done" future
        if (value instanceof Future) {
            if (((Future<?>)value).isDone()) {
                try {
                    value = ((Future<?>)value).get();
                } catch (Exception e) {
                    value = ""+value+" (error evaluating: "+e+")";
                }
            }
        }
        
        if (PrimitiveStringTypeCoercions.isPrimitiveOrBoxer(value.getClass())) return value;
        return value.toString();
    }

    // currently everything converted to string, expanded if it is a "done" future
    public String getStringValueForDisplay(Object value) {
        if (value==null) return null;
        return ""+getObjectValueForDisplay(value);
    }

    /** true if the URL points to content which must be resolved on the server-side (i.e. classpath)
     *  and which is safe to do so (currently just images, though in future perhaps also javascript and html plugins)
     *  <p>
     *  note we do not let caller access classpath through this mechanism, 
     *  just those which are supplied by the platform administrator e.g. as an icon url */
    public boolean isUrlServerSideAndSafe(String url) {
        if (Strings.isEmpty(url)) return false;
        String ext = Files.getFileExtension(url);
        if (Strings.isEmpty(ext)) return false;
        MediaType mime = WebResourceUtils.getImageMediaTypeFromExtension(ext);
        if (mime==null) return false;
        
        return !Urls.isUrlWithProtocol(url) || url.startsWith("classpath:");
    }

    
    public Iterable<Entity> descendantsOfAnyType(String application, String entity) {
        List<Entity> result = Lists.newArrayList();
        Entity e = getEntity(application, entity);
        gatherAllDescendants(e, result);
        return result;
    }
    
    private static void gatherAllDescendants(Entity e, List<Entity> result) {
        if (result.add(e)) {
            for (Entity ee: e.getChildren())
                gatherAllDescendants(ee, result);
        }
    }

    public Iterable<Entity> descendantsOfType(String application, String entity, final String typeRegex) {
        Iterable<Entity> result = descendantsOfAnyType(application, entity);
        return Iterables.filter(result, new Predicate<Entity>() {
            @Override
            public boolean apply(Entity entity) {
                if (entity==null) return false;
                return (entity.getEntityType().getName().matches(typeRegex));
            }
        });
    }

    public void reloadBrooklynProperties() {
        mgmt.reloadBrooklynProperties();
    }
}
