/*
 * 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.servicecomb.authentication.provider;

import java.beans.PropertyDescriptor;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.lang3.StringUtils;
import org.apache.servicecomb.core.Const;
import org.apache.servicecomb.registry.api.registry.Microservice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.netflix.config.ConcurrentCompositeConfiguration;
import com.netflix.config.DynamicPropertyFactory;

/**
 * Add black / white list control to service access
 */
public class AccessController {
  static class ConfigurationItem {
    static final String CATEGORY_PROPERTY = "property";

    String category;

    String propertyName;

    String rule;
  }

  private static final Logger LOG = LoggerFactory.getLogger(AccessController.class);

  private static final String KEY_WHITE_LIST_PREFIX = "servicecomb.publicKey.accessControl.white";

  private static final String KEY_BLACK_LIST_PREFIX = "servicecomb.publicKey.accessControl.black";

  private static final String KEY_PROPERTY_NAME = "%s.%s.propertyName";

  private static final String KEY_CATEGORY = "%s.%s.category";

  private static final String KEY_RULE_POSTFIX = ".rule";

  private Map<String, ConfigurationItem> whiteList = new HashMap<>();

  private Map<String, ConfigurationItem> blackList = new HashMap<>();

  public AccessController() {
    loadConfigurations(KEY_BLACK_LIST_PREFIX);
    loadConfigurations(KEY_WHITE_LIST_PREFIX);
  }

  public boolean isAllowed(Microservice microservice) {
    return whiteAllowed(microservice) && !blackDenied(microservice);
  }

  private boolean whiteAllowed(Microservice microservice) {
    if (whiteList.isEmpty()) {
      return true;
    }
    return matchFound(microservice, whiteList);
  }

  private boolean blackDenied(Microservice microservice) {
    if (blackList.isEmpty()) {
      return false;
    }
    return matchFound(microservice, blackList);
  }

  private boolean matchFound(Microservice microservice, Map<String, ConfigurationItem> ruleList) {
    boolean matched = false;
    for (ConfigurationItem item : ruleList.values()) {
      if (ConfigurationItem.CATEGORY_PROPERTY.equals(item.category)) {
        // we support to configure properties, e.g. serviceName, appId, environment, alias, version and so on, also support key in properties.
        if (matchMicroserviceField(microservice, item) || matchMicroserviceProperties(microservice, item)) {
          return true;
        }
      }
    }
    return matched;
  }

  private boolean matchMicroserviceProperties(Microservice microservice, ConfigurationItem item) {
    Map<String, String> properties = microservice.getProperties();
    for (Entry<String, String> entry : properties.entrySet()) {
      if (!entry.getKey().equals(item.propertyName)) {
        continue;
      }
      return isPatternMatch(entry.getValue(), item.rule);
    }
    return false;
  }

  private boolean matchMicroserviceField(Microservice microservice, ConfigurationItem item) {
    Object fieldValue = null;
    try {
      fieldValue = new PropertyDescriptor(item.propertyName, Microservice.class).getReadMethod().invoke(microservice);
    } catch (Exception e) {
      if (DynamicPropertyFactory.getInstance().getBooleanProperty(Const.PRINT_SENSITIVE_ERROR_MESSAGE,
          false).get()) {
        LOG.warn("can't find propertyname: {} in microservice field, will search in microservice properties.",
            item.propertyName, e);
      } else {
        LOG.warn("can't find propertyname: {} in microservice field, will search in microservice properties.",
            item.propertyName);
      }
      return false;
    }
    if (fieldValue.getClass().getName().equals(String.class.getName())) {
      return isPatternMatch((String) fieldValue, item.rule);
    }
    return false;
  }

  private boolean isPatternMatch(String value, String pattern) {
    if (pattern.startsWith("*")) {
      return value.endsWith(pattern.substring(1));
    }
    if (pattern.endsWith("*")) {
      return value.startsWith(pattern.substring(0, pattern.length() - 1));
    }
    return value.equals(pattern);
  }

  private void loadConfigurations(String prefix) {
    ConcurrentCompositeConfiguration config = (ConcurrentCompositeConfiguration) DynamicPropertyFactory
        .getBackingConfigurationSource();
    loadConfigurations(config, prefix);
    config.addConfigurationListener(event -> {
      if (event.getPropertyName().startsWith(prefix)) {
        LOG.info("Access rule have been changed. Reload configurations. Event=" + event.getType());
        loadConfigurations(config, prefix);
      }
    });
  }

  private void loadConfigurations(ConcurrentCompositeConfiguration config, String prefix) {
    Map<String, ConfigurationItem> configurations = new HashMap<>();
    Iterator<String> configsItems = config.getKeys(prefix);
    while (configsItems.hasNext()) {
      String pathKey = configsItems.next();
      if (pathKey.endsWith(KEY_RULE_POSTFIX)) {
        ConfigurationItem configurationItem = new ConfigurationItem();
        String rule = DynamicPropertyFactory.getInstance()
            .getStringProperty(pathKey, null).get();
        if (StringUtils.isEmpty(rule)) {
          continue;
        }
        configurationItem.rule = rule;
        String pathKeyItem = pathKey
            .substring(prefix.length() + 1, pathKey.length() - KEY_RULE_POSTFIX.length());
        configurationItem.propertyName = DynamicPropertyFactory.getInstance()
            .getStringProperty(String.format(KEY_PROPERTY_NAME, prefix, pathKeyItem), null).get();
        if (StringUtils.isEmpty(configurationItem.propertyName)) {
          continue;
        }
        configurationItem.category = DynamicPropertyFactory.getInstance()
            .getStringProperty(String.format(KEY_CATEGORY, prefix, pathKeyItem), null).get();
        if (StringUtils.isEmpty(configurationItem.category)) {
          continue;
        }
        configurations.put(pathKeyItem, configurationItem);
      }
    }

    if (KEY_WHITE_LIST_PREFIX.equals(prefix)) {
      this.whiteList = configurations;
      logConfigurations(configurations, true);
    } else {
      this.blackList = configurations;
      logConfigurations(configurations, false);
    }
  }

  private void logConfigurations(Map<String, ConfigurationItem> configurations, boolean isWhite) {
    configurations.entrySet().forEach(stringConfigurationItemEntry -> {
      ConfigurationItem item = stringConfigurationItemEntry.getValue();
      LOG.info((isWhite ? "White list " : "Black list ") + "config item: key=" + stringConfigurationItemEntry.getKey()
          + ";category=" + item.category
          + ";propertyName=" + item.propertyName
          + ";rule=" + item.rule);
    });
  }
}
