package org.apache.helix.rest.server;


/*
 * 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.
 */

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.helix.BaseDataAccessor;
import org.apache.helix.ConfigAccessor;
import org.apache.helix.HelixAdmin;
import org.apache.helix.HelixDataAccessor;
import org.apache.helix.HelixException;
import org.apache.helix.SystemPropertyKeys;
import org.apache.helix.manager.zk.ZKHelixAdmin;
import org.apache.helix.manager.zk.ZKHelixDataAccessor;
import org.apache.helix.manager.zk.ZkBaseDataAccessor;
import org.apache.helix.manager.zk.ZkBucketDataAccessor;
import org.apache.helix.msdcommon.exception.InvalidRoutingDataException;
import org.apache.helix.rest.metadatastore.ZkMetadataStoreDirectory;
import org.apache.helix.task.TaskDriver;
import org.apache.helix.tools.ClusterSetup;
import org.apache.helix.zookeeper.api.client.HelixZkClient;
import org.apache.helix.zookeeper.api.client.RealmAwareZkClient;
import org.apache.helix.zookeeper.constant.RoutingDataReaderType;
import org.apache.helix.zookeeper.datamodel.ZNRecord;
import org.apache.helix.zookeeper.datamodel.serializer.ByteArraySerializer;
import org.apache.helix.zookeeper.datamodel.serializer.ZNRecordSerializer;
import org.apache.helix.zookeeper.impl.client.FederatedZkClient;
import org.apache.helix.zookeeper.impl.client.ZkClient;
import org.apache.helix.zookeeper.impl.factory.DedicatedZkClientFactory;
import org.apache.helix.zookeeper.impl.factory.SharedZkClientFactory;
import org.apache.helix.zookeeper.routing.RoutingDataManager;
import org.apache.helix.zookeeper.zkclient.IZkChildListener;
import org.apache.helix.zookeeper.zkclient.IZkDataListener;
import org.apache.helix.zookeeper.zkclient.IZkStateListener;
import org.apache.helix.zookeeper.zkclient.serialize.ZkSerializer;
import org.apache.zookeeper.Watcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


public class ServerContext implements IZkDataListener, IZkChildListener, IZkStateListener {
  private static final Logger LOG = LoggerFactory.getLogger(ServerContext.class);

  private final String _zkAddr;
  private final String _msdsEndpoint;
  private final boolean _isMultiZkEnabled;
  private volatile RealmAwareZkClient _zkClient;
  private volatile RealmAwareZkClient _byteArrayZkClient;

  private volatile ZKHelixAdmin _zkHelixAdmin;
  private volatile ClusterSetup _clusterSetup;
  private volatile ConfigAccessor _configAccessor;
  // A lazily-initialized base data accessor that reads/writes byte array to ZK
  // TODO: Only read (deserialize) is supported at this time. This baseDataAccessor should support write (serialize) as needs arise
  private volatile ZkBaseDataAccessor<byte[]> _byteArrayZkBaseDataAccessor;
  // 1 Cluster name will correspond to 1 helix data accessor
  private final Map<String, HelixDataAccessor> _helixDataAccessorPool;
  // 1 Cluster name will correspond to 1 task driver
  private final Map<String, TaskDriver> _taskDriverPool;
  // Create ZkBucketDataAccessor for ReadOnlyWagedRebalancer.
  private volatile ZkBucketDataAccessor _zkBucketDataAccessor;

  /**
   * Multi-ZK support
   */
  private final ZkMetadataStoreDirectory _zkMetadataStoreDirectory;
  // Create a dedicated ZkClient for listening to data changes in routing data
  private RealmAwareZkClient _zkClientForRoutingDataListener;

  public ServerContext(String zkAddr) {
    this(zkAddr, false, null);
  }

  /**
   * Initializes a ServerContext for this namespace.
   * @param zkAddr routing ZK address (on multi-zk mode)
   * @param isMultiZkEnabled boolean flag for whether multi-zk mode is enabled
   * @param msdsEndpoint if given, this server context will try to read routing data from this MSDS.
   */
  public ServerContext(String zkAddr, boolean isMultiZkEnabled, String msdsEndpoint) {
    _zkAddr = zkAddr;
    _isMultiZkEnabled = isMultiZkEnabled;
    _msdsEndpoint = msdsEndpoint; // only applicable on multi-zk mode

    // We should NOT initiate _zkClient and anything that depends on _zkClient in
    // constructor, as it is reasonable to start up HelixRestServer first and then
    // ZooKeeper. In this case, initializing _zkClient will fail and HelixRestServer
    // cannot be started correctly.
    _helixDataAccessorPool = new ConcurrentHashMap<>();
    _taskDriverPool = new ConcurrentHashMap<>();

    // Initialize the singleton ZkMetadataStoreDirectory instance to allow it to be closed later
    _zkMetadataStoreDirectory = ZkMetadataStoreDirectory.getInstance();
  }

  /**
   * Lazy initialization of RealmAwareZkClient used throughout the REST server.
   * @return
   */
  public RealmAwareZkClient getRealmAwareZkClient() {
    if (_zkClient == null) {
      synchronized (this) {
        if (_zkClient == null) {
          _zkClient = createRealmAwareZkClient(new ZNRecordSerializer());
        }
      }
    }
    return _zkClient;
  }

  /**
   * Returns a RealmAWareZkClient with ByteArraySerializer with double-checked locking.
   * @return
   */
  public RealmAwareZkClient getByteArrayRealmAwareZkClient() {
    if (_byteArrayZkClient == null) {
      synchronized (this) {
        if (_byteArrayZkClient == null) {
          _byteArrayZkClient = createRealmAwareZkClient(new ByteArraySerializer());
        }
      }
    }
    return _byteArrayZkClient;
  }

  /**
   * Main creation logic for RealmAwareZkClient.
   * @param zkSerializer the type of ZkSerializer to use
   * @return
   */
  private RealmAwareZkClient createRealmAwareZkClient(ZkSerializer zkSerializer) {
    // If the multi ZK config is enabled, use FederatedZkClient on multi-realm mode
    RealmAwareZkClient realmAwareZkClient;
    if (_isMultiZkEnabled || Boolean
        .parseBoolean(System.getProperty(SystemPropertyKeys.MULTI_ZK_ENABLED))) {
      try {
        initializeZkClientForRoutingData();
        RealmAwareZkClient.RealmAwareZkConnectionConfig.Builder connectionConfigBuilder =
            new RealmAwareZkClient.RealmAwareZkConnectionConfig.Builder();
        // If MSDS endpoint is set for this namespace, use that instead.
        if (_msdsEndpoint != null && !_msdsEndpoint.isEmpty()) {
          connectionConfigBuilder.setRoutingDataSourceEndpoint(_msdsEndpoint)
              .setRoutingDataSourceType(RoutingDataReaderType.HTTP.name());
        }
        realmAwareZkClient = new FederatedZkClient(connectionConfigBuilder.build(),
            new RealmAwareZkClient.RealmAwareZkClientConfig().setZkSerializer(zkSerializer));
        LOG.info("ServerContext: FederatedZkClient created successfully!");
      } catch (InvalidRoutingDataException | IllegalStateException e) {
        throw new HelixException("Failed to create FederatedZkClient!", e);
      }
    } else {
      // If multi ZK config is not set, just connect to the ZK address given
      HelixZkClient.ZkClientConfig clientConfig = new HelixZkClient.ZkClientConfig();
      clientConfig.setZkSerializer(zkSerializer);
      realmAwareZkClient = SharedZkClientFactory.getInstance()
          .buildZkClient(new HelixZkClient.ZkConnectionConfig(_zkAddr), clientConfig);
    }
    return realmAwareZkClient;
  }

  /**
   * Initialization logic for ZkClient for routing data listener.
   * NOTE: The initialization lifecycle of zkClientForRoutingDataListener is tied to the private
   * volatile zkClient.
   */
  private void initializeZkClientForRoutingData() {
    // Make sure the ServerContext is subscribed to routing data change so that it knows
    // when to reset ZkClients and Helix APIs
    if (_zkClientForRoutingDataListener == null) {
      // Routing data is always in the ZNRecord format
      _zkClientForRoutingDataListener = DedicatedZkClientFactory.getInstance()
          .buildZkClient(new HelixZkClient.ZkConnectionConfig(_zkAddr),
              new HelixZkClient.ZkClientConfig().setZkSerializer(new ZNRecordSerializer()));
    }
    // Refresh data subscription
    _zkClientForRoutingDataListener.unsubscribeAll();
    _zkClientForRoutingDataListener.subscribeRoutingDataChanges(this, this);
    LOG.info("ServerContext: subscribed to routing data in routing ZK at {}!", _zkAddr);
  }

  @Deprecated
  public ZkClient getZkClient() {
    return (ZkClient) getRealmAwareZkClient();
  }

  public HelixAdmin getHelixAdmin() {
    if (_zkHelixAdmin == null) {
      synchronized (this) {
        if (_zkHelixAdmin == null) {
          _zkHelixAdmin = new ZKHelixAdmin(getRealmAwareZkClient());
        }
      }
    }
    return _zkHelixAdmin;
  }

  public ClusterSetup getClusterSetup() {
    if (_clusterSetup == null) {
      synchronized (this) {
        if (_clusterSetup == null) {
          _clusterSetup = new ClusterSetup(getRealmAwareZkClient(), getHelixAdmin());
        }
      }
    }
    return _clusterSetup;
  }

  public TaskDriver getTaskDriver(String clusterName) {
    TaskDriver taskDriver = _taskDriverPool.get(clusterName);
    if (taskDriver == null) {
      synchronized (this) {
        if (!_taskDriverPool.containsKey(clusterName)) {
          _taskDriverPool.put(clusterName, new TaskDriver(getRealmAwareZkClient(), clusterName));
        }
        taskDriver = _taskDriverPool.get(clusterName);
      }
    }
    return taskDriver;
  }

  public ConfigAccessor getConfigAccessor() {
    if (_configAccessor == null) {
      synchronized (this) {
        if (_configAccessor == null) {
          _configAccessor = new ConfigAccessor(getRealmAwareZkClient());
        }
      }
    }
    return _configAccessor;
  }

  public HelixDataAccessor getDataAccessor(String clusterName) {
    HelixDataAccessor dataAccessor = _helixDataAccessorPool.get(clusterName);
    if (dataAccessor == null) {
      synchronized (this) {
        if (!_helixDataAccessorPool.containsKey(clusterName)) {
          ZkBaseDataAccessor<ZNRecord> baseDataAccessor =
              new ZkBaseDataAccessor<>(getRealmAwareZkClient());
          _helixDataAccessorPool.put(clusterName,
              new ZKHelixDataAccessor(clusterName, baseDataAccessor));
        }
        dataAccessor = _helixDataAccessorPool.get(clusterName);
      }
    }
    return dataAccessor;
  }

  /**
   * Returns a lazily-instantiated ZkBaseDataAccessor for the byte array type.
   * @return
   */
  public BaseDataAccessor<byte[]> getByteArrayZkBaseDataAccessor() {
    if (_byteArrayZkBaseDataAccessor == null) {
      synchronized (this) {
        if (_byteArrayZkBaseDataAccessor == null) {
          _byteArrayZkBaseDataAccessor = new ZkBaseDataAccessor<>(getByteArrayRealmAwareZkClient());
        }
      }
    }
    return _byteArrayZkBaseDataAccessor;
  }

  public ZkBucketDataAccessor getZkBucketDataAccessor() {
    // ZkBucketDataAccessor constructor will handle realmZK case (when _zkAddr is null)
    if (_zkBucketDataAccessor == null) {
      synchronized (this) {
        if (_zkBucketDataAccessor == null) {
          _zkBucketDataAccessor = new ZkBucketDataAccessor(getByteArrayRealmAwareZkClient());
        }
      }
    }
    return _zkBucketDataAccessor;
  }

  public void close() {
    if (_zkClient != null) {
      _zkClient.close();
    }
    if (_zkMetadataStoreDirectory != null) {
      _zkMetadataStoreDirectory.close();
    }
    if (_zkClientForRoutingDataListener != null) {
      _zkClientForRoutingDataListener.close();
    }
  }

  @Override
  public void handleChildChange(String parentPath, List<String> currentChilds) {
    if (_zkClientForRoutingDataListener == null || _zkClientForRoutingDataListener.isClosed()) {
      return;
    }
    // Resubscribe
    _zkClientForRoutingDataListener.unsubscribeAll();
    _zkClientForRoutingDataListener.subscribeRoutingDataChanges(this, this);
    resetZkResources();
  }

  @Override
  public void handleDataChange(String dataPath, Object data) {
    if (_zkClientForRoutingDataListener == null || _zkClientForRoutingDataListener.isClosed()) {
      return;
    }
    resetZkResources();
  }

  @Override
  public void handleDataDeleted(String dataPath) {
    if (_zkClientForRoutingDataListener == null || _zkClientForRoutingDataListener.isClosed()) {
      return;
    }
    // Resubscribe
    _zkClientForRoutingDataListener.unsubscribeAll();
    _zkClientForRoutingDataListener.subscribeRoutingDataChanges(this, this);
    resetZkResources();
  }

  @Override
  public void handleStateChanged(Watcher.Event.KeeperState state) {
    if (_zkClientForRoutingDataListener == null || _zkClientForRoutingDataListener.isClosed()) {
      return;
    }
    // Resubscribe
    _zkClientForRoutingDataListener.unsubscribeAll();
    _zkClientForRoutingDataListener.subscribeRoutingDataChanges(this, this);
    resetZkResources();
  }

  @Override
  public void handleNewSession(String sessionId) {
    if (_zkClientForRoutingDataListener == null || _zkClientForRoutingDataListener.isClosed()) {
      return;
    }
    // Resubscribe
    _zkClientForRoutingDataListener.unsubscribeAll();
    _zkClientForRoutingDataListener.subscribeRoutingDataChanges(this, this);
    resetZkResources();
  }

  @Override
  public void handleSessionEstablishmentError(Throwable error) {
    if (_zkClientForRoutingDataListener == null || _zkClientForRoutingDataListener.isClosed()) {
      return;
    }
    // Resubscribe
    _zkClientForRoutingDataListener.unsubscribeAll();
    _zkClientForRoutingDataListener.subscribeRoutingDataChanges(this, this);
    resetZkResources();
  }

  /**
   * Resets all internally cached routing data by closing and nullifying both zkClient and
   * byteArrayZkClient and Helix APIs.
   * This operation is considered costly since it triggers reconnection of all I/O resources, but
   * this is okay because routing data update should be infrequent.
   */
  private void resetZkResources() {
    synchronized (this) {
      LOG.info("ServerContext: Resetting ZK resources due to routing data change! Routing ZK: {}",
          _zkAddr);
      try {
        // Reset RoutingDataManager's cache
        RoutingDataManager.getInstance().reset(true);

        // Close all ZkClients
        if (_zkClient != null && !_zkClient.isClosed()) {
          _zkClient.close();
        }
        if (_byteArrayZkClient != null && !_byteArrayZkClient.isClosed()) {
          _byteArrayZkClient.close();
        }
        _zkClient = null;
        _byteArrayZkClient = null;

        // Close all Helix APIs
        if (_zkHelixAdmin != null) {
          _zkHelixAdmin.close();
          _zkHelixAdmin = null;
        }
        if (_clusterSetup != null) {
          _clusterSetup.close();
          _clusterSetup = null;
        }
        if (_configAccessor != null) {
          _configAccessor.close();
          _configAccessor = null;
        }
        if (_byteArrayZkBaseDataAccessor != null) {
          _byteArrayZkBaseDataAccessor.close();
          _byteArrayZkBaseDataAccessor = null;
        }
        if (_zkBucketDataAccessor != null) {
          _zkBucketDataAccessor.close();
          _zkBucketDataAccessor = null;
        }
        _helixDataAccessorPool.clear();
        _taskDriverPool.clear();
      } catch (Exception e) {
        LOG.error("Failed to reset ZkClient and Helix APIs in ServerContext!", e);
      }
    }
  }
}
