/**
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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.hadoop.ozone.om;

import com.google.common.base.Strings;
import org.apache.hadoop.ozone.OzoneAcl;
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.helpers.OmBucketInfo;
import org.apache.hadoop.ozone.om.helpers.OmPrefixInfo;
import org.apache.hadoop.ozone.om.helpers.OzoneAclUtil;
import org.apache.hadoop.ozone.security.acl.OzoneObj;
import org.apache.hadoop.ozone.security.acl.RequestContext;
import org.apache.hadoop.ozone.util.RadixNode;
import org.apache.hadoop.ozone.util.RadixTree;
import org.apache.hadoop.utils.db.Table.KeyValue;
import org.apache.hadoop.utils.db.TableIterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.BUCKET_NOT_FOUND;
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.PREFIX_NOT_FOUND;
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.VOLUME_NOT_FOUND;
import static org.apache.hadoop.ozone.om.lock.OzoneManagerLock.Resource.PREFIX_LOCK;
import static org.apache.hadoop.ozone.security.acl.OzoneObj.ResourceType.PREFIX;

/**
 * Implementation of PrefixManager.
 */
public class PrefixManagerImpl implements PrefixManager {
  private static final Logger LOG =
      LoggerFactory.getLogger(PrefixManagerImpl.class);

  private static final List<OzoneAcl> EMPTY_ACL_LIST = new ArrayList<>();
  private final OMMetadataManager metadataManager;

  // In-memory prefix tree to optimize ACL evaluation
  private RadixTree<OmPrefixInfo> prefixTree;

  // TODO: This isRatisEnabled check will be removed as part of HDDS-1909,
  //  where we integrate both HA and Non-HA code.
  private boolean isRatisEnabled;

  public PrefixManagerImpl(OMMetadataManager metadataManager,
      boolean isRatisEnabled) {
    this.isRatisEnabled = isRatisEnabled;
    this.metadataManager = metadataManager;
    loadPrefixTree();
  }

  private void loadPrefixTree() {
    prefixTree = new RadixTree<>();
    try (TableIterator<String, ? extends
        KeyValue<String, OmPrefixInfo>> iterator =
             getMetadataManager().getPrefixTable().iterator()) {
      iterator.seekToFirst();
      while (iterator.hasNext()) {
        KeyValue<String, OmPrefixInfo> kv = iterator.next();
        prefixTree.insert(kv.getKey(), kv.getValue());
      }
    } catch (IOException ex) {
      LOG.error("Fail to load prefix tree");
    }
  }


  @Override
  public OMMetadataManager getMetadataManager() {
    return metadataManager;
  }

  /**
   * Add acl for Ozone object. Return true if acl is added successfully else
   * false.
   *
   * @param obj Ozone object for which acl should be added.
   * @param acl ozone acl top be added.
   * @throws IOException if there is error.
   */
  @Override
  public boolean addAcl(OzoneObj obj, OzoneAcl acl) throws IOException {
    validateOzoneObj(obj);

    String prefixPath = obj.getPath();
    metadataManager.getLock().acquireLock(PREFIX_LOCK, prefixPath);
    try {
      OmPrefixInfo prefixInfo =
          metadataManager.getPrefixTable().get(prefixPath);

      OMPrefixAclOpResult omPrefixAclOpResult = addAcl(obj, acl, prefixInfo);

      return omPrefixAclOpResult.isOperationsResult();
    } catch (IOException ex) {
      if (!(ex instanceof OMException)) {
        LOG.error("Add acl operation failed for prefix path:{} acl:{}",
            prefixPath, acl, ex);
      }
      throw ex;
    } finally {
      metadataManager.getLock().releaseLock(PREFIX_LOCK, prefixPath);
    }
  }

  /**
   * Remove acl for Ozone object. Return true if acl is removed successfully
   * else false.
   *
   * @param obj Ozone object.
   * @param acl Ozone acl to be removed.
   * @throws IOException if there is error.
   */
  @Override
  public boolean removeAcl(OzoneObj obj, OzoneAcl acl) throws IOException {
    validateOzoneObj(obj);
    String prefixPath = obj.getPath();
    metadataManager.getLock().acquireLock(PREFIX_LOCK, prefixPath);
    try {
      OmPrefixInfo prefixInfo =
          metadataManager.getPrefixTable().get(prefixPath);
      OMPrefixAclOpResult omPrefixAclOpResult = removeAcl(obj, acl, prefixInfo);

      if (!omPrefixAclOpResult.isOperationsResult()) {
        LOG.debug("acl {} does not exist for prefix path {} ", acl, prefixPath);
        return false;
      }

      return omPrefixAclOpResult.isOperationsResult();

    } catch (IOException ex) {
      if (!(ex instanceof OMException)) {
        LOG.error("Remove prefix acl operation failed for prefix path:{}" +
            " acl:{}", prefixPath, acl, ex);
      }
      throw ex;
    } finally {
      metadataManager.getLock().releaseLock(PREFIX_LOCK, prefixPath);
    }
  }

  /**
   * Acls to be set for given Ozone object. This operations reset ACL for given
   * object to list of ACLs provided in argument.
   *
   * @param obj Ozone object.
   * @param acls List of acls.
   * @throws IOException if there is error.
   */
  @Override
  public boolean setAcl(OzoneObj obj, List<OzoneAcl> acls) throws IOException {
    validateOzoneObj(obj);
    String prefixPath = obj.getPath();
    metadataManager.getLock().acquireLock(PREFIX_LOCK, prefixPath);
    try {
      OmPrefixInfo prefixInfo =
          metadataManager.getPrefixTable().get(prefixPath);

      OMPrefixAclOpResult omPrefixAclOpResult = setAcl(obj, acls, prefixInfo);

      return omPrefixAclOpResult.isOperationsResult();
    } catch (IOException ex) {
      if (!(ex instanceof OMException)) {
        LOG.error("Set prefix acl operation failed for prefix path:{} acls:{}",
            prefixPath, acls, ex);
      }
      throw ex;
    } finally {
      metadataManager.getLock().releaseLock(PREFIX_LOCK, prefixPath);
    }
  }

  /**
   * Returns list of ACLs for given Ozone object.
   *
   * @param obj Ozone object.
   * @throws IOException if there is error.
   */
  @Override
  public List<OzoneAcl> getAcl(OzoneObj obj) throws IOException {
    validateOzoneObj(obj);
    String prefixPath = obj.getPath();
    metadataManager.getLock().acquireLock(PREFIX_LOCK, prefixPath);
    try {
      String longestPrefix = prefixTree.getLongestPrefix(prefixPath);
      if (prefixPath.equals(longestPrefix)) {
        RadixNode<OmPrefixInfo> lastNode =
            prefixTree.getLastNodeInPrefixPath(prefixPath);
        if (lastNode != null && lastNode.getValue() != null) {
          return lastNode.getValue().getAcls();
        }
      }
    } finally {
      metadataManager.getLock().releaseLock(PREFIX_LOCK, prefixPath);
    }
    return EMPTY_ACL_LIST;
  }

  /**
   * Check access for given ozoneObject.
   *
   * @param ozObject object for which access needs to be checked.
   * @param context Context object encapsulating all user related information.
   * @return true if user has access else false.
   */
  @Override
  public boolean checkAccess(OzoneObj ozObject, RequestContext context)
      throws OMException {
    Objects.requireNonNull(ozObject);
    Objects.requireNonNull(context);

    String prefixPath = ozObject.getPath();
    metadataManager.getLock().acquireLock(PREFIX_LOCK, prefixPath);
    try {
      String longestPrefix = prefixTree.getLongestPrefix(prefixPath);
      if (prefixPath.equals(longestPrefix)) {
        RadixNode<OmPrefixInfo> lastNode =
            prefixTree.getLastNodeInPrefixPath(prefixPath);
        if (lastNode != null && lastNode.getValue() != null) {
          boolean hasAccess = OzoneAclUtil.checkAclRights(lastNode.getValue().
              getAcls(), context);
          LOG.debug("user:{} has access rights for ozObj:{} ::{} ",
              context.getClientUgi(), ozObject, hasAccess);
          return hasAccess;
        } else {
          return true;
        }
      } else {
        return true;
      }
    } finally {
      metadataManager.getLock().releaseLock(PREFIX_LOCK, prefixPath);
    }
  }

  @Override
  public List<OmPrefixInfo> getLongestPrefixPath(String path) {
    String prefixPath = prefixTree.getLongestPrefix(path);
    metadataManager.getLock().acquireLock(PREFIX_LOCK, prefixPath);
    try {
      return getLongestPrefixPathHelper(prefixPath);
    } finally {
      metadataManager.getLock().releaseLock(PREFIX_LOCK, prefixPath);
    }
  }

  /**
   * Get longest prefix path assuming caller take prefix lock.
   * @param prefixPath
   * @return list of prefix info.
   */
  private List<OmPrefixInfo> getLongestPrefixPathHelper(String prefixPath) {
    return prefixTree.getLongestPrefixPath(prefixPath).stream()
          .map(c -> c.getValue()).collect(Collectors.toList());
  }

  /**
   * Helper method to validate ozone object.
   * @param obj
   * */
  public void validateOzoneObj(OzoneObj obj) throws OMException {
    Objects.requireNonNull(obj);

    if (!obj.getResourceType().equals(PREFIX)) {
      throw new IllegalArgumentException("Unexpected argument passed to " +
          "PrefixManager. OzoneObj type:" + obj.getResourceType());
    }
    String volume = obj.getVolumeName();
    String bucket = obj.getBucketName();
    String prefixName = obj.getPrefixName();

    if (Strings.isNullOrEmpty(volume)) {
      throw new OMException("Volume name is required.", VOLUME_NOT_FOUND);
    }
    if (Strings.isNullOrEmpty(bucket)) {
      throw new OMException("Bucket name is required.", BUCKET_NOT_FOUND);
    }
    if (Strings.isNullOrEmpty(prefixName)) {
      throw new OMException("Prefix name is required.", PREFIX_NOT_FOUND);
    }
    if (!prefixName.endsWith("/")) {
      throw new OMException("Invalid prefix name: " + prefixName,
          PREFIX_NOT_FOUND);
    }
  }

  public OMPrefixAclOpResult addAcl(OzoneObj ozoneObj, OzoneAcl ozoneAcl,
      OmPrefixInfo prefixInfo) throws IOException {

    if (prefixInfo == null) {
      prefixInfo = new OmPrefixInfo.Builder().setName(ozoneObj
          .getPath()).build();
    }
    boolean changed = prefixInfo.addAcl(ozoneAcl);
    if (changed) {
      // update the in-memory prefix tree
      prefixTree.insert(ozoneObj.getPath(), prefixInfo);

      if (!isRatisEnabled) {
        metadataManager.getPrefixTable().put(ozoneObj.getPath(), prefixInfo);
      }
    }
    return new OMPrefixAclOpResult(prefixInfo, changed);
  }

  public OMPrefixAclOpResult removeAcl(OzoneObj ozoneObj, OzoneAcl ozoneAcl,
      OmPrefixInfo prefixInfo) throws IOException {
    boolean removed = false;
    if (prefixInfo != null) {
      removed = prefixInfo.removeAcl(ozoneAcl);
    }

    // Nothing is matching to remove.
    if (removed) {
      // Update in-memory prefix tree.
      if (prefixInfo.getAcls().isEmpty()) {
        prefixTree.removePrefixPath(ozoneObj.getPath());
        if (!isRatisEnabled) {
          metadataManager.getPrefixTable().delete(ozoneObj.getPath());
        }
      } else {
        prefixTree.insert(ozoneObj.getPath(), prefixInfo);
        if (!isRatisEnabled) {
          metadataManager.getPrefixTable().put(ozoneObj.getPath(), prefixInfo);
        }
      }
    }
    return new OMPrefixAclOpResult(prefixInfo, removed);
  }

  public OMPrefixAclOpResult setAcl(OzoneObj ozoneObj, List<OzoneAcl> ozoneAcls,
      OmPrefixInfo prefixInfo) throws IOException {
    if (prefixInfo == null) {
      prefixInfo = new OmPrefixInfo.Builder().setName(ozoneObj
          .getPath()).build();
    }

    boolean changed = prefixInfo.setAcls(ozoneAcls);
    if (changed) {
      List<OzoneAcl> aclsToBeSet = prefixInfo.getAcls();
      // Inherit DEFAULT acls from prefix.
      boolean prefixParentFound = false;
      List<OmPrefixInfo> prefixList = getLongestPrefixPathHelper(
          prefixTree.getLongestPrefix(ozoneObj.getPath()));

      if (prefixList.size() > 0) {
        // Add all acls from direct parent to key.
        OmPrefixInfo parentPrefixInfo = prefixList.get(prefixList.size() - 1);
        if (parentPrefixInfo != null) {
          prefixParentFound = OzoneAclUtil.inheritDefaultAcls(aclsToBeSet,
              parentPrefixInfo.getAcls());
        }
      }

      // If no parent prefix is found inherit DEFAULT acls from bucket.
      if (!prefixParentFound) {
        String bucketKey = metadataManager.getBucketKey(ozoneObj
            .getVolumeName(), ozoneObj.getBucketName());
        OmBucketInfo bucketInfo = metadataManager.getBucketTable().
            get(bucketKey);
        if (bucketInfo != null) {
          OzoneAclUtil.inheritDefaultAcls(aclsToBeSet, bucketInfo.getAcls());
        }
      }

      prefixTree.insert(ozoneObj.getPath(), prefixInfo);
      if (!isRatisEnabled) {
        metadataManager.getPrefixTable().put(ozoneObj.getPath(), prefixInfo);
      }
    }
    return new OMPrefixAclOpResult(prefixInfo, changed);
  }

  /**
   * Result of the prefix acl operation.
   */
  public static class OMPrefixAclOpResult {
    private OmPrefixInfo omPrefixInfo;
    private boolean operationsResult;

    public OMPrefixAclOpResult(OmPrefixInfo omPrefixInfo,
        boolean operationsResult) {
      this.omPrefixInfo = omPrefixInfo;
      this.operationsResult = operationsResult;
    }

    public OmPrefixInfo getOmPrefixInfo() {
      return omPrefixInfo;
    }

    public boolean isOperationsResult() {
      return operationsResult;
    }
  }
}
