/*
 * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
 * (the "License"). You may not use this work except in compliance with the License, which is
 * available at www.apache.org/licenses/LICENSE-2.0
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied, as more fully set forth in the License.
 *
 * See the NOTICE file distributed with this work for information regarding copyright ownership.
 */

package alluxio.membership;

import alluxio.annotation.SuppressFBWarnings;
import alluxio.cli.CommandUtils;
import alluxio.conf.AlluxioConfiguration;
import alluxio.conf.Configuration;
import alluxio.conf.PropertyKey;
import alluxio.exception.runtime.NotFoundRuntimeException;
import alluxio.util.HashUtils;
import alluxio.util.network.NetworkAddressUtils;
import alluxio.wire.WorkerIdentity;
import alluxio.wire.WorkerInfo;
import alluxio.wire.WorkerNetAddress;

import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.UUID;

/**
 * MembershipManager configured by a static file.
 */
public class StaticMembershipManager implements MembershipManager {
  private final List<WorkerInfo> mMembers;

  private final AlluxioConfiguration mConf;

  /**
   * @param conf
   * @return StaticMembershipManager
   */
  public static StaticMembershipManager create(AlluxioConfiguration conf) {
    // user conf/workers, use default port
    String workerListFile = conf.getString(
        PropertyKey.WORKER_STATIC_MEMBERSHIP_MANAGER_CONFIG_FILE);
    List<WorkerInfo> workers = parseWorkerAddresses(workerListFile, conf);
    return new StaticMembershipManager(conf, workers);
  }

  /**
   * CTOR for StaticMembershipManager.
   * @param conf
   * @throws IOException
   */
  @SuppressFBWarnings({"URF_UNREAD_FIELD"})
  StaticMembershipManager(AlluxioConfiguration conf, List<WorkerInfo> members) {
    mConf = conf;
    mMembers = members;
  }

  /**
   * Parse the worker addresses from given static config file.
   * The static file only gives the hostname, the rest config params
   * are inherited from given Configuration or default values.
   * @param configFile
   * @param conf
   * @return list of parsed WorkerInfos
   * @throws NotFoundRuntimeException if the config file is not found
   */
  private static List<WorkerInfo> parseWorkerAddresses(
      String configFile, AlluxioConfiguration conf) {
    List<WorkerInfo> workerInfos = new ArrayList<>();
    File file = new File(configFile);
    if (!file.exists()) {
      throw new NotFoundRuntimeException("Not found for static worker config file:" + configFile);
    }
    Set<String> workerHostnames = CommandUtils.readNodeList("", configFile);
    for (String workerHostname : workerHostnames) {
      WorkerNetAddress workerNetAddress = new WorkerNetAddress()
          .setHost(workerHostname)
          .setContainerHost(Configuration.global()
              .getOrDefault(PropertyKey.WORKER_CONTAINER_HOSTNAME, ""))
          .setRpcPort(conf.getInt(PropertyKey.WORKER_RPC_PORT))
          .setWebPort(conf.getInt(PropertyKey.WORKER_WEB_PORT))
          .setHttpServerPort(conf.getInt(PropertyKey.WORKER_HTTP_SERVER_PORT));
      //data port, these are initialized from configuration for client to deduce the
      //workeraddr related info, on worker side, it will be corrected by join().
      InetSocketAddress inetAddr;
      if (Configuration.global().getBoolean(PropertyKey.USER_NETTY_DATA_TRANSMISSION_ENABLED))  {
        inetAddr = NetworkAddressUtils.getBindAddress(
            NetworkAddressUtils.ServiceType.WORKER_DATA,
            Configuration.global());
        workerNetAddress.setNettyDataPort(inetAddr.getPort());
      } else {
        inetAddr = NetworkAddressUtils.getConnectAddress(
            NetworkAddressUtils.ServiceType.WORKER_RPC,
            Configuration.global());
      }
      workerNetAddress.setDataPort(inetAddr.getPort());
      // generate identities from hostnames
      // todo(bowen): consider change the configuration file to allow specify both the
      //  identity and the net address
      WorkerIdentity identity = WorkerIdentity.ParserV1.INSTANCE.fromUUID(
          UUID.nameUUIDFromBytes(workerHostname.getBytes(StandardCharsets.UTF_8)));
      WorkerInfo workerInfo = new WorkerInfo()
          .setIdentity(identity)
          .setAddress(workerNetAddress);
      workerInfos.add(workerInfo);
    }
    return workerInfos;
  }

  @Override
  public void join(WorkerInfo worker) throws IOException {
    // correct with the actual worker addr,
    // same settings such as ports will be applied to other members
    WorkerNetAddress addr = worker.getAddress();
    mMembers.stream().forEach(m -> m.getAddress()
        .setRpcPort(addr.getRpcPort())
        .setDataPort(addr.getDataPort())
        .setDomainSocketPath(addr.getDomainSocketPath())
        .setNettyDataPort(addr.getNettyDataPort())
        .setWebPort(addr.getWebPort())
        .setSecureRpcPort(addr.getSecureRpcPort())
        .setHttpServerPort(addr.getHttpServerPort()));
  }

  @Override
  public WorkerClusterView getAllMembers() throws IOException {
    return new WorkerClusterView(mMembers);
  }

  @Override
  public WorkerClusterView getLiveMembers() throws IOException {
    // all workers are considered by the static membership manager to be always live
    return new WorkerClusterView(mMembers);
  }

  @Override
  public WorkerClusterView getFailedMembers() throws IOException {
    return new WorkerClusterView(Collections.emptyList());
  }

  @Override
  public String showAllMembers() {
    String printFormat = "%s\t%s\t%s%n";
    StringBuilder sb = new StringBuilder(
        String.format(printFormat, "WorkerId", "Address", "Status"));
    try {
      for (WorkerInfo worker : getAllMembers()) {
        String entryLine = String.format(printFormat,
            HashUtils.hashAsStringMD5(worker.getAddress().dumpMainInfo()),
            worker.getAddress().getHost() + ":" + worker.getAddress().getRpcPort(),
            "N/A");
        sb.append(entryLine);
      }
    } catch (IOException ex) {
      // IGNORE
    }
    return sb.toString();
  }

  @Override
  public void stopHeartBeat(WorkerInfo worker) throws IOException {
    // NOTHING TO DO
  }

  @Override
  public void decommission(WorkerInfo worker) throws IOException {
    mMembers.remove(worker);
  }

  @Override
  public void close() throws Exception {
    // Nothing to close
  }
}
