/**
 * 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.sentry.provider.db.generic.service.persistent;

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.jdo.PersistenceManager;
import javax.jdo.Query;

import org.apache.hadoop.conf.Configuration;
import org.apache.sentry.SentryUserException;
import org.apache.sentry.core.common.Action;
import org.apache.sentry.core.common.Authorizable;
import org.apache.sentry.core.common.BitFieldAction;
import org.apache.sentry.core.common.BitFieldActionFactory;
import org.apache.sentry.core.model.kafka.KafkaActionFactory;
import org.apache.sentry.core.model.search.SearchActionFactory;
import org.apache.sentry.core.model.sqoop.SqoopActionFactory;
import org.apache.sentry.provider.db.generic.service.persistent.PrivilegeObject.Builder;
import org.apache.sentry.provider.db.service.model.MSentryGMPrivilege;
import org.apache.sentry.provider.db.service.model.MSentryRole;

import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.sentry.service.thrift.ServiceConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class used do some operations related privilege and make the results
 * persistence
 */
public class PrivilegeOperatePersistence {
  private static final Logger LOGGER = LoggerFactory.getLogger(PrivilegeOperatePersistence.class);
  private static final Map<String, BitFieldActionFactory> actionFactories = Maps.newHashMap();
  static{
    actionFactories.put("solr", new SearchActionFactory());
    actionFactories.put("sqoop", new SqoopActionFactory());
    actionFactories.put("kafka", KafkaActionFactory.getInstance());
  }

  private final Configuration conf;

  public PrivilegeOperatePersistence(Configuration conf) {
    this.conf = conf;
  }

  public boolean checkPrivilegeOption(Set<MSentryRole> roles, PrivilegeObject privilege, PersistenceManager pm) {
    MSentryGMPrivilege requestPrivilege = convertToPrivilege(privilege);
    boolean hasGrant = false;
    //get persistent privileges by roles
    Query query = pm.newQuery(MSentryGMPrivilege.class);
    StringBuilder filters = new StringBuilder();
    if (roles != null && roles.size() > 0) {
      query.declareVariables("org.apache.sentry.provider.db.service.model.MSentryRole role");
      List<String> rolesFiler = new LinkedList<String>();
      for (MSentryRole role : roles) {
        rolesFiler.add("role.roleName == \"" + role.getRoleName() + "\" ");
      }
      filters.append("roles.contains(role) " + "&& (" + Joiner.on(" || ").join(rolesFiler) + ")");
    }
    query.setFilter(filters.toString());

    List<MSentryGMPrivilege> tPrivileges = (List<MSentryGMPrivilege>)query.execute();
    for (MSentryGMPrivilege tPrivilege : tPrivileges) {
      if (tPrivilege.getGrantOption() && tPrivilege.implies(requestPrivilege)) {
        hasGrant = true;
        break;
      }
    }
    return hasGrant;
  }
  public void grantPrivilege(PrivilegeObject privilege,MSentryRole role, PersistenceManager pm) throws SentryUserException {
    MSentryGMPrivilege mPrivilege = convertToPrivilege(privilege);
    grantRolePartial(mPrivilege, role, pm);
  }

  private void grantRolePartial(MSentryGMPrivilege grantPrivilege,
      MSentryRole role,PersistenceManager pm) {
    /**
     * If Grant is for ALL action and other actions belongs to ALL action already exists..
     * need to remove it and GRANT ALL action
     */
    String component = grantPrivilege.getComponentName();
    BitFieldAction action = getAction(component, grantPrivilege.getAction());
    BitFieldAction allAction = getAction(component, Action.ALL);

    if (action.implies(allAction)) {
      /**
       * ALL action is a multi-bit set action that includes some actions such as INSERT,SELECT and CREATE.
       */
      List<? extends BitFieldAction> actions = getActionFactory(component).getActionsByCode(allAction.getActionCode());
      for (BitFieldAction ac : actions) {
        grantPrivilege.setAction(ac.getValue());
        MSentryGMPrivilege existPriv = getPrivilege(grantPrivilege, pm);
        if (existPriv != null && role.getGmPrivileges().contains(existPriv)) {
          /**
           * force to load all roles related this privilege
           * avoid the lazy-loading risk,such as:
           * if the roles field of privilege aren't loaded, then the roles is a empty set
           * privilege.removeRole(role) and pm.makePersistent(privilege)
           * will remove other roles that shouldn't been removed
           */
          pm.retrieve(existPriv);
          existPriv.removeRole(role);
          pm.makePersistent(existPriv);
        }
      }
    } else {
      /**
       * If ALL Action already exists..
       * do nothing.
       */
      grantPrivilege.setAction(allAction.getValue());
      MSentryGMPrivilege allPrivilege = getPrivilege(grantPrivilege, pm);
      if (allPrivilege != null && role.getGmPrivileges().contains(allPrivilege)) {
        return;
      }
    }

    /**
     * restore the action
     */
    grantPrivilege.setAction(action.getValue());
    /**
     * check the privilege is exist or not
     */
    MSentryGMPrivilege mPrivilege = getPrivilege(grantPrivilege, pm);
    if (mPrivilege == null) {
      mPrivilege = grantPrivilege;
    }
    mPrivilege.appendRole(role);
    pm.makePersistent(mPrivilege);
  }


  public void revokePrivilege(PrivilegeObject privilege,MSentryRole role, PersistenceManager pm) throws SentryUserException {
    MSentryGMPrivilege mPrivilege = getPrivilege(convertToPrivilege(privilege), pm);
    if (mPrivilege == null) {
      mPrivilege = convertToPrivilege(privilege);
    } else {
      mPrivilege = (MSentryGMPrivilege) pm.detachCopy(mPrivilege);
    }

    Set<MSentryGMPrivilege> privilegeGraph = Sets.newHashSet();
    privilegeGraph.addAll(populateIncludePrivileges(Sets.newHashSet(role), mPrivilege, pm));

    /**
     * Get the privilege graph
     * populateIncludePrivileges will get the privileges that needed revoke
     */
    for (MSentryGMPrivilege persistedPriv : privilegeGraph) {
      /**
       * force to load all roles related this privilege
       * avoid the lazy-loading risk,such as:
       * if the roles field of privilege aren't loaded, then the roles is a empty set
       * privilege.removeRole(role) and pm.makePersistent(privilege)
       * will remove other roles that shouldn't been removed
       */
      revokeRolePartial(mPrivilege, persistedPriv, role, pm);
    }
    pm.makePersistent(role);
  }

  /**
   * Explore Privilege graph and collect privileges that are belong to the specific privilege
   */
  @SuppressWarnings("unchecked")
  private Set<MSentryGMPrivilege> populateIncludePrivileges(Set<MSentryRole> roles,
      MSentryGMPrivilege parent, PersistenceManager pm) {
    Set<MSentryGMPrivilege> childrens = Sets.newHashSet();

    Query query = pm.newQuery(MSentryGMPrivilege.class);
    StringBuilder filters = new StringBuilder();
    //add populateIncludePrivilegesQuery
    filters.append(MSentryGMPrivilege.populateIncludePrivilegesQuery(parent));
    // add filter for role names
    if (roles != null && roles.size() > 0) {
      query.declareVariables("org.apache.sentry.provider.db.service.model.MSentryRole role");
      List<String> rolesFiler = new LinkedList<String>();
      for (MSentryRole role : roles) {
        rolesFiler.add("role.roleName == \"" + role.getRoleName() + "\" ");
      }
      filters.append("&& roles.contains(role) " + "&& (" + Joiner.on(" || ").join(rolesFiler) + ")");
    }
    query.setFilter(filters.toString());

    List<MSentryGMPrivilege> privileges = (List<MSentryGMPrivilege>)query.execute();
    childrens.addAll(privileges);
    return childrens;
  }

  /**
   * Roles can be granted multi-bit set action like ALL action on resource object.
   * Take solr component for example, When a role has been granted ALL action but
   * QUERY or UPDATE or CREATE are revoked, we need to remove the ALL
   * privilege and add left privileges like UPDATE and CREATE(QUERY was revoked) or
   * QUERY and UPDATE(CREATEE was revoked).
   */
  private void revokeRolePartial(MSentryGMPrivilege revokePrivilege,
      MSentryGMPrivilege persistedPriv, MSentryRole role,
      PersistenceManager pm) {
    String component = revokePrivilege.getComponentName();
    BitFieldAction revokeaction = getAction(component, revokePrivilege.getAction());
    BitFieldAction persistedAction = getAction(component, persistedPriv.getAction());
    BitFieldAction allAction = getAction(component, Action.ALL);

    if (revokeaction.implies(allAction)) {
      /**
       * if revoke action is ALL, directly revoke its children privileges and itself
       */
      persistedPriv.removeRole(role);
      pm.makePersistent(persistedPriv);
    } else {
      /**
       * if persisted action is ALL, it only revoke the requested action and left partial actions
       * like the requested action is SELECT, the UPDATE and CREATE action are left
       */
      if (persistedAction.implies(allAction)) {
        /**
         * revoke the ALL privilege
         */
        persistedPriv.removeRole(role);
        pm.makePersistent(persistedPriv);

        List<? extends BitFieldAction> actions = getActionFactory(component).getActionsByCode(allAction.getActionCode());
        for (BitFieldAction ac: actions) {
          if (ac.getActionCode() != revokeaction.getActionCode()) {
            /**
             * grant the left privileges to role
             */
            MSentryGMPrivilege tmpPriv = new MSentryGMPrivilege(persistedPriv);
            tmpPriv.setAction(ac.getValue());
            MSentryGMPrivilege leftPersistedPriv = getPrivilege(tmpPriv, pm);
            if (leftPersistedPriv == null) {
              //leftPersistedPriv isn't exist
              leftPersistedPriv = tmpPriv;
              role.appendGMPrivilege(leftPersistedPriv);
            }
            leftPersistedPriv.appendRole(role);
            pm.makePersistent(leftPersistedPriv);
          }
        }
      } else if (revokeaction.implies(persistedAction)) {
        /**
         * if the revoke action is equal to the persisted action and they aren't ALL action
         * directly remove the role from privilege
         */
        persistedPriv.removeRole(role);
        pm.makePersistent(persistedPriv);
      }
      /**
       * if the revoke action is not equal to the persisted action,
       * do nothing
       */
    }
  }

  /**
   * Drop any role related to the requested privilege and its children privileges
   */
  public void dropPrivilege(PrivilegeObject privilege,PersistenceManager pm) {
    MSentryGMPrivilege requestPrivilege = convertToPrivilege(privilege);

    if (Strings.isNullOrEmpty(privilege.getAction())) {
      requestPrivilege.setAction(getAction(privilege.getComponent(), Action.ALL).getValue());
    }
    /**
     * Get the privilege graph
     * populateIncludePrivileges will get the privileges that need dropped,
     */
    Set<MSentryGMPrivilege> privilegeGraph = Sets.newHashSet();
    privilegeGraph.addAll(populateIncludePrivileges(null, requestPrivilege, pm));

    for (MSentryGMPrivilege mPrivilege : privilegeGraph) {
      /**
       * force to load all roles related this privilege
       * avoid the lazy-loading
       */
      pm.retrieve(mPrivilege);
      Set<MSentryRole> roles = mPrivilege.getRoles();
      for (MSentryRole role : roles) {
        revokeRolePartial(requestPrivilege, mPrivilege, role, pm);
      }
    }
  }

  private MSentryGMPrivilege convertToPrivilege(PrivilegeObject privilege) {
    return new MSentryGMPrivilege(privilege.getComponent(),
        privilege.getService(), privilege.getAuthorizables(),
        privilege.getAction(), privilege.getGrantOption());
  }

  private MSentryGMPrivilege getPrivilege(MSentryGMPrivilege privilege, PersistenceManager pm) {
    Query query = pm.newQuery(MSentryGMPrivilege.class);
    query.setFilter(MSentryGMPrivilege.toQuery(privilege));
    query.setUnique(true);
    return (MSentryGMPrivilege)query.execute();
  }

  @SuppressWarnings("unchecked")
  public Set<PrivilegeObject> getPrivilegesByRole(Set<MSentryRole> roles, PersistenceManager pm) {
    Set<PrivilegeObject> privileges = Sets.newHashSet();
    if (roles == null || roles.size() == 0) {
      return privileges;
    }
    Query query = pm.newQuery(MSentryGMPrivilege.class);
    StringBuilder filters = new StringBuilder();
    // add filter for role names
    query.declareVariables("org.apache.sentry.provider.db.service.model.MSentryRole role");
    List<String> rolesFiler = new LinkedList<String>();
    for (MSentryRole role : roles) {
      rolesFiler.add("role.roleName == \"" + role.getRoleName() + "\" ");
    }
    filters.append("roles.contains(role) " + "&& (" + Joiner.on(" || ").join(rolesFiler) + ")");

    query.setFilter(filters.toString());
    List<MSentryGMPrivilege> mPrivileges = (List<MSentryGMPrivilege>) query.execute();
    if (mPrivileges == null || mPrivileges.isEmpty()) {
      return privileges;
    }
    for (MSentryGMPrivilege mPrivilege : mPrivileges) {
      privileges.add(new Builder()
                               .setComponent(mPrivilege.getComponentName())
                               .setService(mPrivilege.getServiceName())
                               .setAction(mPrivilege.getAction())
                               .setAuthorizables(mPrivilege.getAuthorizables())
                               .withGrantOption(mPrivilege.getGrantOption())
                               .build());
    }
    return privileges;
  }

  public Set<PrivilegeObject> getPrivilegesByProvider(String component,
      String service, Set<MSentryRole> roles,
      List<? extends Authorizable> authorizables, PersistenceManager pm) {
    Set<PrivilegeObject> privileges = Sets.newHashSet();
    if (roles == null || roles.isEmpty()) {
      return privileges;
    }

    MSentryGMPrivilege parentPrivilege = new MSentryGMPrivilege(component, service, authorizables, null, null);
    Set<MSentryGMPrivilege> privilegeGraph = Sets.newHashSet();
    privilegeGraph.addAll(populateIncludePrivileges(roles, parentPrivilege, pm));

    for (MSentryGMPrivilege mPrivilege : privilegeGraph) {
      privileges.add(new Builder()
                               .setComponent(mPrivilege.getComponentName())
                               .setService(mPrivilege.getServiceName())
                               .setAction(mPrivilege.getAction())
                               .setAuthorizables(mPrivilege.getAuthorizables())
                               .withGrantOption(mPrivilege.getGrantOption())
                               .build());
    }
    return privileges;
  }

  public Set<MSentryGMPrivilege> getPrivilegesByAuthorizable(String component,
      String service, Set<MSentryRole> roles,
      List<? extends Authorizable> authorizables, PersistenceManager pm) {

    Set<MSentryGMPrivilege> privilegeGraph = Sets.newHashSet();

    if (roles == null || roles.isEmpty()) {
      return privilegeGraph;
    }

    MSentryGMPrivilege parentPrivilege = new MSentryGMPrivilege(component, service, authorizables, null, null);
    privilegeGraph.addAll(populateIncludePrivileges(roles, parentPrivilege, pm));
    return privilegeGraph;
  }

  public void renamePrivilege(String component, String service,
      List<? extends Authorizable> oldAuthorizables, List<? extends Authorizable> newAuthorizables,
      String grantorPrincipal, PersistenceManager pm)
      throws SentryUserException {
    MSentryGMPrivilege oldPrivilege = new MSentryGMPrivilege(component, service, oldAuthorizables, null, null);
    oldPrivilege.setAction(getAction(component,Action.ALL).getValue());
    /**
     * Get the privilege graph
     * populateIncludePrivileges will get the old privileges that need dropped
     */
    Set<MSentryGMPrivilege> privilegeGraph = Sets.newHashSet();
    privilegeGraph.addAll(populateIncludePrivileges(null, oldPrivilege, pm));

    for (MSentryGMPrivilege dropPrivilege : privilegeGraph) {
      /**
       * construct the new privilege needed to add
       */
      List<Authorizable> authorizables = new ArrayList<Authorizable>(
          dropPrivilege.getAuthorizables());
      for (int i = 0; i < newAuthorizables.size(); i++) {
        authorizables.set(i, newAuthorizables.get(i));
      }
      MSentryGMPrivilege newPrivilge = new MSentryGMPrivilege(
          component,service, authorizables, dropPrivilege.getAction(),
          dropPrivilege.getGrantOption());

      /**
       * force to load all roles related this privilege
       * avoid the lazy-loading
       */
      pm.retrieve(dropPrivilege);

      Set<MSentryRole> roles = dropPrivilege.getRoles();
      for (MSentryRole role : roles) {
        revokeRolePartial(oldPrivilege, dropPrivilege, role, pm);
        grantRolePartial(newPrivilge, role, pm);
      }
    }
  }

  private BitFieldAction getAction(String component, String name) {
    BitFieldActionFactory actionFactory = getActionFactory(component);
    BitFieldAction action = actionFactory.getActionByName(name);
    if (action == null) {
      throw new RuntimeException("can't get BitFieldAction for name:" + name);
    }
    return action;
  }

  private BitFieldActionFactory getActionFactory(String component) {
    String caseInsensitiveComponent = component.toLowerCase();
    if (actionFactories.containsKey(caseInsensitiveComponent)) {
      return actionFactories.get(caseInsensitiveComponent);
    }
    BitFieldActionFactory actionFactory = createActionFactory(caseInsensitiveComponent);
    actionFactories.put(caseInsensitiveComponent, actionFactory);
    LOGGER.info("Action factory for component {} not found in cache. Loaded it from configuration as {}.",
                component, actionFactory.getClass().getName());
    return actionFactory;
  }

  private BitFieldActionFactory createActionFactory(String component) {
    String actionFactoryClassName =
      conf.get(String.format(ServiceConstants.ServerConfig.SENTRY_COMPONENT_ACTION_FACTORY_FORMAT, component));
    if (actionFactoryClassName == null) {
      throw new RuntimeException("ActionFactory not defined for component " + component +
                                   ". Please define the parameter " +
                                   "sentry." + component + ".action.factory in configuration");
    }
    Class<?> actionFactoryClass;
    try {
      actionFactoryClass = Class.forName(actionFactoryClassName);
    } catch (ClassNotFoundException e) {
      throw new RuntimeException("ActionFactory class " + actionFactoryClassName + " not found.");
    }
    if (!BitFieldActionFactory.class.isAssignableFrom(actionFactoryClass)) {
      throw new RuntimeException("ActionFactory class " + actionFactoryClassName + " must extend "
                                   + BitFieldActionFactory.class.getName());
    }
    BitFieldActionFactory actionFactory;
    try {
      Constructor<?> actionFactoryConstructor = actionFactoryClass.getDeclaredConstructor();
      actionFactoryConstructor.setAccessible(true);
      actionFactory = (BitFieldActionFactory) actionFactoryClass.newInstance();
    } catch (NoSuchMethodException | InstantiationException | IllegalAccessException e) {
      throw new RuntimeException("Could not instantiate actionFactory " + actionFactoryClassName +
                                   " for component: " + component, e);
    }
    return actionFactory;
  }
}