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

import java.util.Arrays;
import java.util.List;

import javax.annotation.Nullable;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.api.mgmt.entitlement.EntitlementClass;
import org.apache.brooklyn.api.mgmt.entitlement.EntitlementContext;
import org.apache.brooklyn.api.mgmt.entitlement.EntitlementManager;
import org.apache.brooklyn.api.objs.EntityAdjunct;
import org.apache.brooklyn.api.policy.Policy;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.config.Sanitizer;
import org.apache.brooklyn.core.internal.BrooklynProperties;
import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
import org.apache.brooklyn.core.mgmt.persist.DeserializingClassRenamesProvider;
import org.apache.brooklyn.util.core.ClassLoaderUtils;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.javalang.Reflections;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.Beta;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.reflect.TypeToken;

/** @since 0.7.0 */
@Beta
public class Entitlements {

    private static final Logger log = LoggerFactory.getLogger(Entitlements.class);
    
    // ------------------- individual permissions
    
    // TODO applies to bundles and registered types; should pass object or probably better add more entitlements?
    public static EntitlementClass<String> SEE_CATALOG_ITEM = new BasicEntitlementClassDefinition<String>("catalog.see", String.class); 
    public static EntitlementClass<Object> ADD_CATALOG_ITEM = new BasicEntitlementClassDefinition<Object>("catalog.add", Object.class); 
    public static EntitlementClass<StringAndArgument> MODIFY_CATALOG_ITEM = new BasicEntitlementClassDefinition<StringAndArgument>("catalog.modify", StringAndArgument.class); 
    
    public static EntitlementClass<Entity> SEE_ENTITY = new BasicEntitlementClassDefinition<Entity>("entity.see", Entity.class);
    public static EntitlementClass<Entity> RENAME_ENTITY = new BasicEntitlementClassDefinition<Entity>("entity.rename", Entity.class);
    public static EntitlementClass<EntityAndItem<String>> SEE_SENSOR = new BasicEntitlementClassDefinition<EntityAndItem<String>>("sensor.see", EntityAndItem.typeToken(String.class));
    public static EntitlementClass<EntityAndItem<String>> SEE_CONFIG = new BasicEntitlementClassDefinition<EntityAndItem<String>>("config.see", EntityAndItem.typeToken(String.class));
    public static EntitlementClass<TaskAndItem<String>> SEE_ACTIVITY_STREAMS = new BasicEntitlementClassDefinition<TaskAndItem<String>>("activity.streams.see", TaskAndItem.typeToken(String.class));
    // string is effector name; argument may be a map or a list, depending how the args were supplied
    public static EntitlementClass<EntityAndItem<StringAndArgument>> INVOKE_EFFECTOR = new BasicEntitlementClassDefinition<EntityAndItem<StringAndArgument>>("effector.invoke", EntityAndItem.typeToken(StringAndArgument.class));
    public static EntitlementClass<Entity> MODIFY_ENTITY = new BasicEntitlementClassDefinition<Entity>("entity.modify", Entity.class);

    // Adjunct entitlements
    public static EntitlementClass<EntityAdjunct> DELETE_ADJUNCT = new BasicEntitlementClassDefinition<>("adjunct.delete", EntityAdjunct.class);

    // Location entitlements
    public static EntitlementClass<StringAndArgument> ADD_LOCATION = new BasicEntitlementClassDefinition<>("location.add", StringAndArgument.class);
    public static EntitlementClass<StringAndArgument> DELETE_LOCATION = new BasicEntitlementClassDefinition<>("location.delete", StringAndArgument.class);
    public static EntitlementClass<StringAndArgument> SEE_LOCATION = new BasicEntitlementClassDefinition<>("location.see", StringAndArgument.class);

    // Policy entitlements
    public static EntitlementClass<StringAndArgument> ADD_POLICY = new BasicEntitlementClassDefinition<>("policy.add", StringAndArgument.class);
    public static EntitlementClass<Policy> DELETE_POLICY = new BasicEntitlementClassDefinition<>("policy.delete", Policy.class);
    public static EntitlementClass<Policy> START_POLICY = new BasicEntitlementClassDefinition<>("policy.start", Policy.class);
    public static EntitlementClass<Policy> STOP_POLICY = new BasicEntitlementClassDefinition<>("policy.stop", Policy.class);

    public static EntitlementClass<Void> SYSTEM_ADMIN = new BasicEntitlementClassDefinition<>("system.admin", Void.class);
    public static EntitlementClass<Void> HA_STATS = new BasicEntitlementClassDefinition<>("system.ha.stats", Void.class);
    public static EntitlementClass<Void> HA_ADMIN = new BasicEntitlementClassDefinition<>("system.ha.admin", Void.class);
    public static EntitlementClass<Void> SHUTDOWN = new BasicEntitlementClassDefinition<>("system.shutdown", Void.class);
    public static EntitlementClass<Void> USAGE = new BasicEntitlementClassDefinition<>("system.usage", Void.class);



    /**
     * Permission to deploy an application, where parameter is some representation
     * of the app to be deployed (spec instance or yaml plan)
     */
    public static EntitlementClass<Object> DEPLOY_APPLICATION = new BasicEntitlementClassDefinition<Object>("app.deploy", Object.class);

    /**
     * Catch-all for catalog, locations, scripting, usage, etc - exporting persistence, shutting down, etc;
     * this is significantly more powerful than {@link #SERVER_STATUS}.
     * NB: this may be refactored and deprecated in future
     */
    public static EntitlementClass<Void> SEE_ALL_SERVER_INFO = new BasicEntitlementClassDefinition<Void>("server.info.all.see", Void.class);

    /**
     * Permission to see general server status info: basically HA status; not nearly as much as {@link #SEE_ALL_SERVER_INFO}
     */
    public static EntitlementClass<Void> SERVER_STATUS = new BasicEntitlementClassDefinition<Void>("server.status", Void.class);
    
    /**
     * Permission to run untrusted code or embedded scripts at the server.
     * A secondary check is required for any operation which could potentially grant root-level access.
     */
    public static EntitlementClass<Void> ROOT = new BasicEntitlementClassDefinition<Void>("root", Void.class);

    @SuppressWarnings("unchecked")
    public enum EntitlementClassesEnum {
        ENTITLEMENT_SEE_CATALOG_ITEM(SEE_CATALOG_ITEM) { public <T> T handle(EntitlementClassesHandler<T> handler, Object argument) { return handler.handleSeeCatalogItem((String)argument); } },
        ENTITLEMENT_ADD_CATALOG_ITEM(ADD_CATALOG_ITEM) { public <T> T handle(EntitlementClassesHandler<T> handler, Object argument) { return handler.handleAddCatalogItem(argument); } },
        ENTITLEMENT_MODIFY_CATALOG_ITEM(MODIFY_CATALOG_ITEM) { public <T> T handle(EntitlementClassesHandler<T> handler, Object argument) { return handler.handleModifyCatalogItem((StringAndArgument)argument); } },
        
        ENTITLEMENT_SEE_ENTITY(SEE_ENTITY) { public <T> T handle(EntitlementClassesHandler<T> handler, Object argument) { return handler.handleSeeEntity((Entity)argument); } },
        ENTITLEMENT_SEE_SENSOR(SEE_SENSOR) { public <T> T handle(EntitlementClassesHandler<T> handler, Object argument) { return handler.handleSeeSensor((EntityAndItem<String>)argument); } },
        ENTITLEMENT_INVOKE_EFFECTOR(INVOKE_EFFECTOR) { public <T> T handle(EntitlementClassesHandler<T> handler, Object argument) { return handler.handleInvokeEffector((EntityAndItem<StringAndArgument>)argument); } },
        ENTITLEMENT_MODIFY_ENTITY(MODIFY_ENTITY) { public <T> T handle(EntitlementClassesHandler<T> handler, Object argument) { return handler.handleModifyEntity((Entity)argument); } },
        
        ENTITLEMENT_DEPLOY_APPLICATION(DEPLOY_APPLICATION) { public <T> T handle(EntitlementClassesHandler<T> handler, Object argument) { return handler.handleDeployApplication(argument); } },
        
        ENTITLEMENT_SEE_ALL_SERVER_INFO(SEE_ALL_SERVER_INFO) { public <T> T handle(EntitlementClassesHandler<T> handler, Object argument) { return handler.handleSeeAllServerInfo(); } },
        ENTITLEMENT_SERVER_STATUS(SERVER_STATUS) { public <T> T handle(EntitlementClassesHandler<T> handler, Object argument) { return handler.handleSeeServerStatus(); } },
        ENTITLEMENT_ROOT(ROOT) { public <T> T handle(EntitlementClassesHandler<T> handler, Object argument) { return handler.handleRoot(); } },
        ;
        
        private EntitlementClass<?> entitlementClass;

        EntitlementClassesEnum(EntitlementClass<?> specificClass) {
            this.entitlementClass = specificClass;
        }
        public EntitlementClass<?> getEntitlementClass() {
            return entitlementClass;
        }

        public abstract <T> T handle(EntitlementClassesHandler<T> handler, Object argument);
        
        public static EntitlementClassesEnum of(EntitlementClass<?> entitlementClass) {
            for (EntitlementClassesEnum x: values()) {
                if (entitlementClass.equals(x.getEntitlementClass())) return x;
            }
            return null;
        }
    }
    
    public interface EntitlementClassesHandler<T> {
        public T handleSeeCatalogItem(String catalogItemId);
        public T handleSeeServerStatus();
        public T handleAddCatalogItem(Object catalogItemBeingAdded);
        public T handleModifyCatalogItem(StringAndArgument catalogItemIdAndModification);
        public T handleSeeEntity(Entity entity);
        public T handleSeeSensor(EntityAndItem<String> sensorInfo);
        public T handleInvokeEffector(EntityAndItem<StringAndArgument> effectorInfo);
        public T handleModifyEntity(Entity entity);
        public T handleDeployApplication(Object app);
        public T handleSeeAllServerInfo();
        public T handleRoot();
    }
    
    protected static class Pair<T1,T2> {
        protected final T1 p1;
        protected final T2 p2;
        protected Pair(T1 p1, T2 p2) {
            this.p1 = p1;
            this.p2 = p2;
        }
    }

    public static class EntityAndItem<T> extends Pair<Entity,T> {
        public static <TT> TypeToken<EntityAndItem<TT>> typeToken(Class<TT> type) {
            return new TypeToken<Entitlements.EntityAndItem<TT>>() {
                private static final long serialVersionUID = -738154831809025407L;
            };
        }
        public EntityAndItem(Entity entity, T item) { super (entity, item); }
        public Entity getEntity() { return p1; }
        public T getItem() { return p2; }
        public static <T> EntityAndItem<T> of(Entity entity, T item) {
            return new EntityAndItem<T>(entity, item);
        }
    }
    
    public static class TaskAndItem<T> extends Pair<Task<?>,T> {
        public static <TT> TypeToken<TaskAndItem<TT>> typeToken(Class<TT> type) {
            return new TypeToken<Entitlements.TaskAndItem<TT>>() {
                private static final long serialVersionUID = 3103447462213439135L;
            };
        }
        public TaskAndItem(Task<?> task, T item) { super(task, item); }
        public Task<?> getTask() { return p1; }
        public T getItem() { return p2; }
        public static <T> TaskAndItem<T> of(Task<?> task, T item) {
            return new TaskAndItem<T>(task, item);
        }
    }
    
    public static class StringAndArgument extends Pair<String,Object> {
        public StringAndArgument(String string, Object argument) { super(string, argument); }
        public String getString() { return p1; }
        public Object getArgument() { return p2; }
        public static StringAndArgument of(String string, Object argument) {
            return new StringAndArgument(string, argument);
        }
    }

    /** 
     * These lifecycle operations are currently treated as effectors. This may change in the future.
     * @since 0.7.0 */
    @Beta
    public static class LifecycleEffectors {
        public static final String DELETE = "delete";
    }
    
    // ------------- permission sets -------------
    
    /**
     * @return An entitlement manager allowing access to everything.
     */
    public static EntitlementManager root() {
        return new EntitlementManager() {
            @Override
            public <T> boolean isEntitled(EntitlementContext context, EntitlementClass<T> permission, T typeArgument) {
                return true;
            }
            @Override
            public String toString() {
                return "Entitlements.root";
            }
        };
    }

    /**
     * @return An entitlement manager allowing everything but {@link #ROOT} and {@link #SEE_ALL_SERVER_INFO}.
     */
    public static EntitlementManager user() {
        return new EntitlementManager() {
            @Override
            public <T> boolean isEntitled(EntitlementContext context, EntitlementClass<T> permission, T entitlementClassArgument) {
                return !SEE_ALL_SERVER_INFO.equals(permission) && !ROOT.equals(permission);
            }
            @Override
            public String toString() {
                return "Entitlements.user";
            }
        };
    }

    /**
     * @return An entitlement manager denying access to anything that requires entitlements.
     */
    public static EntitlementManager minimal() {
        return new EntitlementManager() {
            @Override
            public <T> boolean isEntitled(EntitlementContext context, EntitlementClass<T> permission, T typeArgument) {
                return false;
            }
            @Override
            public String toString() {
                return "Entitlements.minimal";
            }
        };
    }

    public static class FineGrainedEntitlements {
    
        private static final Joiner COMMA_JOINER = Joiner.on(',');

        public static EntitlementManager anyOf(final EntitlementManager... checkers) {
            return anyOf(Arrays.asList(checkers));
        }
        
        public static EntitlementManager anyOf(final Iterable<? extends EntitlementManager> checkers) {
            return new EntitlementManager() {
                @Override
                public <T> boolean isEntitled(EntitlementContext context, EntitlementClass<T> permission, T typeArgument) {
                    for (EntitlementManager checker: checkers)
                        if (checker.isEntitled(context, permission, typeArgument))
                            return true;
                    return false;
                }
                @Override
                public String toString() {
                    return "Entitlements.anyOf(" + COMMA_JOINER.join(checkers) + ")";
                }
            };
        }
        
        public static EntitlementManager allOf(final EntitlementManager... checkers) {
            return allOf(Arrays.asList(checkers));
        }
        
        public static EntitlementManager allOf(final Iterable<? extends EntitlementManager> checkers) {
            return new EntitlementManager() {
                @Override
                public <T> boolean isEntitled(EntitlementContext context, EntitlementClass<T> permission, T typeArgument) {
                    for (EntitlementManager checker: checkers)
                        if (checker.isEntitled(context, permission, typeArgument))
                            return true;
                    return false;
                }
                @Override
                public String toString() {
                    return "Entitlements.allOf(" + COMMA_JOINER.join(checkers) + ")";
                }
            };
        }

        public static <U> EntitlementManager allowing(EntitlementClass<U> permission, Predicate<U> test) {
            return new SinglePermissionEntitlementChecker<U>(permission, test);
        }

        public static <U> EntitlementManager allowing(EntitlementClass<U> permission) {
            return new SinglePermissionEntitlementChecker<U>(permission, Predicates.<U>alwaysTrue());
        }

        public static class SinglePermissionEntitlementChecker<U> implements EntitlementManager {
            final EntitlementClass<U> permission;
            final Predicate<U> test;
            
            protected SinglePermissionEntitlementChecker(EntitlementClass<U> permission, Predicate<U> test) {
                this.permission = permission;
                this.test = test;
            }
            
            @SuppressWarnings("unchecked")
            @Override
            public <T> boolean isEntitled(EntitlementContext context, EntitlementClass<T> permission, T typeArgument) {
                return Objects.equal(this.permission, permission) && test.apply((U) typeArgument);
            }
            @Override
            public String toString() {
                return "Entitlements.allowing(" + permission + " -> " + test + ")";
            }
        }

        private static class NonSecretPredicate implements Predicate<EntityAndItem<String>> {
            @Override
            public boolean apply(EntityAndItem<String> input) {
                return input != null && !Sanitizer.IS_SECRET_PREDICATE.apply(input.getItem());
            }

            @Override
            public String toString() {
                return "Predicates.nonSecret";
            }
        }

        public static EntitlementManager seeNonSecretSensors() {
            return allowing(SEE_SENSOR, new NonSecretPredicate());
        }
        
        public static EntitlementManager seeNonSecretConfig() {
            return allowing(SEE_CONFIG, new NonSecretPredicate());
        }
    }
    
    /** allow read-only */
    public static EntitlementManager readOnly() {
        return FineGrainedEntitlements.anyOf(
            FineGrainedEntitlements.allowing(SEE_ENTITY),
            FineGrainedEntitlements.allowing(SEE_ACTIVITY_STREAMS),
            FineGrainedEntitlements.allowing(SEE_CATALOG_ITEM),
            FineGrainedEntitlements.allowing(SERVER_STATUS),
            FineGrainedEntitlements.allowing(SEE_LOCATION),
            FineGrainedEntitlements.allowing(HA_STATS),
            FineGrainedEntitlements.seeNonSecretSensors(),
            FineGrainedEntitlements.seeNonSecretConfig()
        );
    }

    /** allow healthcheck */
    public static EntitlementManager serverStatusOnly() {
        return FineGrainedEntitlements.allowing(SERVER_STATUS);
    }

    // ------------- lookup conveniences -------------

    private static class PerThreadEntitlementContextHolder {
        public static final ThreadLocal<EntitlementContext> perThreadEntitlementsContextHolder = new ThreadLocal<EntitlementContext>();
    }

    /** 
     * Finds the currently applicable {@link EntitlementContext} by examining the current thread
     * then by investigating the current task, its submitter, etc. */
    // NOTE: entitlements are propagated to tasks whenever they are created, as tags
    // (see BrooklynTaskTags.tagForEntitlement and BasicExecutionContext.submitInternal).
    // It might be cheaper to only do this lookup, not to propagate as tags, and to ensure
    // all entitlement operations are wrapped in a task at source; but currently we do not
    // do that so we need at least to set entitlement on the outermost task.
    // Setting it on tasks submitted by a task is not strictly necessary (i.e. in BasicExecutionContext)
    // but seems cheap enough, and means checking entitlements is fast, if we choose to do that more often.
    public static EntitlementContext getEntitlementContext() {
        EntitlementContext context;
        context = PerThreadEntitlementContextHolder.perThreadEntitlementsContextHolder.get();
        if (context!=null) return context;
        
        Task<?> task = Tasks.current();
        while (task!=null) {
            context = BrooklynTaskTags.getEntitlement(task);
            if (context!=null) return context;
            task = task.getSubmittedByTask();
        }
        
        // no entitlements set -- assume entitlements not used, or system internal
        return null;
    }

    public static void setEntitlementContext(EntitlementContext context) {
        EntitlementContext oldContext = PerThreadEntitlementContextHolder.perThreadEntitlementsContextHolder.get();
        if (oldContext!=null && context!=null) {
            log.warn("Changing entitlement context from "+oldContext+" to "+context+"; context should have been reset or extended, not replaced");
            log.debug("Trace for entitlement context duplicate overwrite", new Throwable("Trace for entitlement context overwrite"));
        }
        PerThreadEntitlementContextHolder.perThreadEntitlementsContextHolder.set(context);
    }
    
    public static void clearEntitlementContext() {
        PerThreadEntitlementContextHolder.perThreadEntitlementsContextHolder.set(null);
    }
    
    public static <T> boolean isEntitled(EntitlementManager checker, EntitlementClass<T> permission, T typeArgument) {
        return checker.isEntitled(getEntitlementContext(), permission, typeArgument);
    }

    /** throws {@link NotEntitledException} if entitlement not available for current {@link #getEntitlementContext()} */
    public static <T> void checkEntitled(EntitlementManager checker, EntitlementClass<T> permission, T typeArgument) {
        if (!isEntitled(checker, permission, typeArgument)) {
            throw new NotEntitledException(getEntitlementContext(), permission, typeArgument);
        }
    }
    
    // ----------------- initialization ----------------

    public final static String ENTITLEMENTS_CONFIG_PREFIX = "brooklyn.entitlements";

    public static final ConfigKey<String> GLOBAL_ENTITLEMENT_MANAGER = ConfigKeys.newStringConfigKey(ENTITLEMENTS_CONFIG_PREFIX + ".global",
        "Class for entitlements in effect globally; "
        + "short names 'minimal', 'readonly', 'user' or 'root' are permitted here, with the default 'root' giving full access to all declared users; "
        + "or supply the name of an "+EntitlementManager.class+" class to instantiate, taking a 1-arg BrooklynProperties constructor or a 0-arg constructor",
        "root");
    
    public static EntitlementManager newManager(ManagementContext mgmt, BrooklynProperties brooklynProperties) {
        return newGlobalManager(mgmt, brooklynProperties);
    }
    private static EntitlementManager newGlobalManager(ManagementContext mgmt, BrooklynProperties brooklynProperties) {
        return load(mgmt, brooklynProperties, brooklynProperties.getConfig(GLOBAL_ENTITLEMENT_MANAGER));
    }
    
    public static EntitlementManager load(@Nullable ManagementContext mgmt, BrooklynProperties brooklynProperties, String type) {
        if ("root".equalsIgnoreCase(type)) {
            return root();
        } else if ("readonly".equalsIgnoreCase(type) || "read_only".equalsIgnoreCase(type)) {
            return readOnly();
        } else if ("minimal".equalsIgnoreCase(type)) {
            return minimal();
        } else if ("user".equalsIgnoreCase(type)) {
            return user();
        }
        if (Strings.isNonBlank(type)) {
            try {
                Class<?> clazz = new ClassLoaderUtils(Entitlements.class, mgmt).loadClass(DeserializingClassRenamesProvider.INSTANCE.findMappedName(type));
                return (EntitlementManager) instantiate(clazz, ImmutableList.of(
                        new Object[] {mgmt, brooklynProperties},
                        new Object[] {mgmt},
                        new Object[] {brooklynProperties},
                        new Object[0]));
            } catch (Exception e) { 
                throw Exceptions.propagate(e); 
            }
        }
        throw new IllegalStateException("Invalid entitlement manager specified: '"+type+"'");
    }
    
    private static Object instantiate(Class<?> clazz, List<Object[]> constructorArgOptions) {
        try {
            for (Object[] constructorArgOption : constructorArgOptions) {
                Maybe<?> result = Reflections.invokeConstructorFromArgs(clazz, constructorArgOption);
                if (result.isPresent()) return result.get();
            }
        } catch (Exception e) { 
            throw Exceptions.propagate(e); 
        }
        throw new IllegalStateException("No matching constructor to instantiate "+clazz);
    }
}
