/*
 * 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.ambari.server.serveraction.kerberos;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;

import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.actionmanager.HostRoleStatus;
import org.apache.ambari.server.agent.CommandReport;
import org.apache.ambari.server.controller.AmbariManagementController;
import org.apache.ambari.server.serveraction.AbstractServerAction;
import org.apache.ambari.server.state.Cluster;
import org.apache.ambari.server.state.Clusters;
import org.apache.ambari.server.state.ConfigHelper;
import org.apache.ambari.server.state.SecurityType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.Inject;

/**
 * UpdateKerberosConfigServerAction is implementation of ServerAction that updates service configs
 * while enabling Kerberos
 */
public class UpdateKerberosConfigsServerAction extends AbstractServerAction {

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

  @Inject
  private AmbariManagementController controller;

  @Inject
  private ConfigHelper configHelper;

  /**
   * The KerberosConfigDataFileReaderFactory to use to obtain KerberosConfigDataFileReader instances
   */
  @Inject
  private KerberosConfigDataFileReaderFactory kerberosConfigDataFileReaderFactory;

  /**
   * Executes this ServerAction
   * <p/>
   * This is typically called by the ServerActionExecutor in it's own thread, but there is no
   * guarantee that this is the case.  It is expected that the ExecutionCommand and HostRoleCommand
   * properties are set before calling this method.
   *
   * @param requestSharedDataContext a Map to be used a shared data among all ServerActions related
   *                                 to a given request
   * @return a CommandReport declaring the status of the task
   * @throws org.apache.ambari.server.AmbariException
   * @throws InterruptedException
   */
  @Override
  public CommandReport execute(ConcurrentMap<String, Object> requestSharedDataContext)
      throws AmbariException, InterruptedException {

    CommandReport commandReport = null;

    String clusterName = getExecutionCommand().getClusterName();
    Clusters clusters = controller.getClusters();
    Cluster cluster = clusters.getCluster(clusterName);

    String authenticatedUserName = getCommandParameterValue(getCommandParameters(), KerberosServerAction.AUTHENTICATED_USER_NAME);
    String dataDirectoryPath = getCommandParameterValue(getCommandParameters(), KerberosServerAction.DATA_DIRECTORY);
    HashMap<String, Map<String, String>> propertiesToSet = new HashMap<>();
    HashMap<String, Collection<String>> propertiesToRemove = new HashMap<>();

    // If the data directory path is set, attempt to process further, else assume there is no work to do
    if (dataDirectoryPath != null) {
      File dataDirectory = new File(dataDirectoryPath);

      // If the data directory exists, attempt to process further, else assume there is no work to do
      if (dataDirectory.exists()) {
        KerberosConfigDataFileReader configReader = null;
        Set<String> configTypes = new HashSet<>();

        try {
          // If the config data file exists, iterate over the records to find the (explicit)
          // configuration settings to update
          File configFile = new File(dataDirectory, KerberosConfigDataFileReader.DATA_FILE_NAME);
          if (configFile.exists()) {
            configReader = kerberosConfigDataFileReaderFactory.createKerberosConfigDataFileReader(configFile);
            for (Map<String, String> record : configReader) {
              String configType = record.get(KerberosConfigDataFileReader.CONFIGURATION_TYPE);
              String configKey = record.get(KerberosConfigDataFileReader.KEY);
              String configOp = record.get(KerberosConfigDataFileReader.OPERATION);

              configTypes.add(configType);

              if (KerberosConfigDataFileReader.OPERATION_TYPE_REMOVE.equals(configOp)) {
                removeConfigTypeProp(propertiesToRemove, configType, configKey);
              } else {
                String configVal = record.get(KerberosConfigDataFileReader.VALUE);
                addConfigTypePropVal(propertiesToSet, configType, configKey, configVal);
              }
            }
          }

          // ensure that cluster-env/security_enabled have proper value
          final String securityEnabled = cluster.getSecurityType() == SecurityType.KERBEROS
              ? "true"
              : "false";

          if(!configTypes.contains("cluster-env")) {
            configTypes.add("cluster-env");
          }

          Map<String, String> clusterEnvProperties = propertiesToSet.get("cluster-env");
          if(clusterEnvProperties == null) {
            clusterEnvProperties = new HashMap<>();
            propertiesToSet.put("cluster-env", clusterEnvProperties);
          }

          clusterEnvProperties.put("security_enabled", securityEnabled);

          String configNote = getCommandParameterValue(getCommandParameters(), KerberosServerAction.UPDATE_CONFIGURATION_NOTE);

          if(configNote == null || configNote.isEmpty()) {
            configNote = cluster.getSecurityType() == SecurityType.KERBEROS
                ? "Enabling Kerberos"
                : "Disabling Kerberos";
          }

          configHelper.updateBulkConfigType(cluster, cluster.getDesiredStackVersion(), controller,
            configTypes, propertiesToSet, propertiesToRemove,
            authenticatedUserName, configNote);
        } catch (IOException e) {
          String message = "Could not update services configs to enable kerberos";
          actionLog.writeStdErr(message);
          LOG.error(message, e);
          commandReport = createCommandReport(1, HostRoleStatus.FAILED, "{}", actionLog.getStdOut(),
              actionLog.getStdErr());
        } finally {
          if (configReader != null && !configReader.isClosed()) {
            try {
              configReader.close();
            } catch (Throwable t) {
              // ignored
            }
          }
        }
      }
    }

    return (commandReport == null)
        ? createCommandReport(0, HostRoleStatus.COMPLETED, "{}", actionLog.getStdOut(), actionLog.getStdErr())
        : commandReport;
  }


  /**
   * Gets a property from the given commandParameters
   *
   * @param commandParameters map of command parameters
   * @param propertyName      property name to find value for
   * @return value of given proeprty name, would return <code>null</code>
   * if the provided commandParameters is null or  if the requested property is not found
   * in commandParams
   */
  private static String getCommandParameterValue(Map<String, String> commandParameters, String propertyName) {
    return ((commandParameters == null) || (propertyName == null)) ? null : commandParameters.get(propertyName);
  }

  /**
   * Adds a property to properties of a given service config type
   *
   * @param configurations a map of configurations
   * @param configType     service config type
   * @param prop           property to be added
   * @param val            value for the property
   */
  private void addConfigTypePropVal(HashMap<String, Map<String, String>> configurations, String configType, String prop, String val) {
    Map<String, String> configTypePropsVal = configurations.get(configType);
    if (configTypePropsVal == null) {
      configTypePropsVal = new HashMap<>();
      configurations.put(configType, configTypePropsVal);
    }
    configTypePropsVal.put(prop, val);
    actionLog.writeStdOut(String.format("Setting property %s/%s: %s", configType, prop, (val == null) ? "<null>" : val));
  }

  /**
   * Removes a property from the set of properties of a given service config type
   *
   * @param configurations a map of configurations
   * @param configType     service config type
   * @param prop           property to be removed
   */
  private void removeConfigTypeProp(HashMap<String, Collection<String>> configurations, String configType, String prop) {
    Collection<String> configTypeProps = configurations.get(configType);
    if (configTypeProps == null) {
      configTypeProps = new HashSet<>();
      configurations.put(configType, configTypeProps);
    }
    configTypeProps.add(prop);
    actionLog.writeStdOut(String.format("Removing property %s/%s", configType, prop));
  }
}
