/*
 * 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.geode.management.internal.beans;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.logging.log4j.Logger;

import org.apache.geode.cache.Region;
import org.apache.geode.cache.query.CqClosedException;
import org.apache.geode.cache.query.CqException;
import org.apache.geode.cache.query.Index;
import org.apache.geode.cache.query.QueryService;
import org.apache.geode.cache.query.internal.CqStateImpl;
import org.apache.geode.cache.query.internal.cq.CqService;
import org.apache.geode.cache.query.internal.cq.InternalCqQuery;
import org.apache.geode.cache.query.internal.cq.ServerCQ;
import org.apache.geode.cache.server.CacheServer;
import org.apache.geode.cache.server.ServerLoad;
import org.apache.geode.cache.server.ServerLoadProbe;
import org.apache.geode.cache.server.internal.ServerMetricsImpl;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.internal.admin.ClientHealthMonitoringRegion;
import org.apache.geode.internal.admin.remote.ClientHealthStats;
import org.apache.geode.internal.cache.CacheServerImpl;
import org.apache.geode.internal.cache.InternalCache;
import org.apache.geode.internal.cache.ha.HARegionQueue;
import org.apache.geode.internal.cache.tier.Acceptor;
import org.apache.geode.internal.cache.tier.InternalClientMembership;
import org.apache.geode.internal.cache.tier.sockets.AcceptorImpl;
import org.apache.geode.internal.cache.tier.sockets.CacheClientNotifier;
import org.apache.geode.internal.cache.tier.sockets.CacheClientProxy;
import org.apache.geode.internal.cache.tier.sockets.ClientProxyMembershipID;
import org.apache.geode.internal.cache.tier.sockets.ServerConnection;
import org.apache.geode.internal.process.PidUnavailableException;
import org.apache.geode.internal.process.ProcessUtils;
import org.apache.geode.internal.serialization.KnownVersion;
import org.apache.geode.logging.internal.log4j.api.LogService;
import org.apache.geode.management.ClientHealthStatus;
import org.apache.geode.management.ClientQueueDetail;
import org.apache.geode.management.ServerLoadData;
import org.apache.geode.management.internal.ManagementConstants;
import org.apache.geode.management.internal.beans.stats.MBeanStatsMonitor;
import org.apache.geode.management.internal.beans.stats.StatType;
import org.apache.geode.management.internal.beans.stats.StatsAverageLatency;
import org.apache.geode.management.internal.beans.stats.StatsKey;
import org.apache.geode.management.internal.beans.stats.StatsRate;
import org.apache.geode.management.membership.ClientMembershipListener;

/**
 * Represents the GemFire CacheServer . Provides data and notifications about server,
 * subscriptions,durable queues and indices
 */
public class CacheServerBridge extends ServerBridge {

  private static final Logger logger = LogService.getLogger();

  private CacheServer cacheServer;

  private InternalCache cache;

  private QueryService qs;

  private StatsRate clientNotificationRate;

  private StatsAverageLatency clientNotificatioAvgLatency;

  protected StatsRate queryRequestRate;

  private MemberMBeanBridge memberMBeanBridge;

  private ClientMembershipListener membershipListener;

  public static final ThreadLocal<KnownVersion> clientVersion = new ThreadLocal<>();

  protected static int identifyPid() {
    try {
      return ProcessUtils.identifyPid();
    } catch (PidUnavailableException e) {
      if (logger.isDebugEnabled()) {
        logger.debug(e.getMessage(), e);
      }
      return 0;
    }
  }

  public CacheServerBridge(final InternalCache cache, final CacheServer cacheServer) {
    super(cacheServer);
    this.cacheServer = cacheServer;
    this.cache = cache;
    qs = cache.getQueryService();

    initializeCacheServerStats();
  }

  // For testing only
  public CacheServerBridge(final InternalCache cache, final CacheServer cacheServer,
      final AcceptorImpl acceptor, final MBeanStatsMonitor monitor) {
    super(acceptor, monitor);
    this.cacheServer = cacheServer;
    this.cache = cache;
    qs = cache.getQueryService();

    initializeCacheServerStats();
  }

  // For testing only
  public CacheServerBridge() {
    super();
    initializeCacheServerStats();
  }

  public void setMemberMBeanBridge(MemberMBeanBridge memberMBeanBridge) {
    this.memberMBeanBridge = memberMBeanBridge;
  }

  @Override
  public void stopMonitor() {
    super.stopMonitor();
    monitor.stopListener();
  }

  private void initializeCacheServerStats() {

    clientNotificationRate =
        new StatsRate(StatsKey.NUM_CLIENT_NOTIFICATION_REQUEST, StatType.INT_TYPE, monitor);

    clientNotificatioAvgLatency = new StatsAverageLatency(StatsKey.NUM_CLIENT_NOTIFICATION_REQUEST,
        StatType.INT_TYPE, StatsKey.CLIENT_NOTIFICATION_PROCESS_TIME, monitor);

    queryRequestRate = new StatsRate(StatsKey.QUERY_REQUESTS, StatType.INT_TYPE, monitor);
  }

  /**
   * Returns the configured buffer size of the socket connection for this server
   **/
  public int getSocketBufferSize() {
    return cacheServer.getSocketBufferSize();
  }

  public boolean getTcpNoDelay() {
    return cacheServer.getTcpNoDelay();
  }

  /** port of the server **/
  public int getPort() {
    return cacheServer.getPort();
  }

  public int getCapacity() {
    if (cacheServer.getClientSubscriptionConfig() != null) {
      return cacheServer.getClientSubscriptionConfig().getCapacity();
    }
    return 0;
  }

  /** disk store name for overflow **/
  public String getDiskStoreName() {
    if (cacheServer.getClientSubscriptionConfig() != null) {
      return cacheServer.getClientSubscriptionConfig().getDiskStoreName();
    }

    return null;
  }

  /** Returns the maximum allowed client connections **/
  public int getMaxConnections() {
    return cacheServer.getMaxConnections();
  }

  /**
   * Get the frequency in milliseconds to poll the load probe on this cache server.
   **/
  public long getLoadPollInterval() {
    return cacheServer.getLoadPollInterval();
  }

  /** Get the load probe for this cache server **/
  public ServerLoadData fetchLoadProbe() {
    ServerLoadProbe probe = cacheServer.getLoadProbe();
    ServerLoad load = probe.getLoad(new ServerMetricsImpl(cacheServer.getMaxConnections()));
    return new ServerLoadData(load.getConnectionLoad(), load.getSubscriptionConnectionLoad(),
        load.getLoadPerConnection(), load.getLoadPerSubscriptionConnection());
  }

  /**
   * Returns the maximum number of threads allowed in this server to service client requests.
   **/
  public int getMaxThreads() {
    return cacheServer.getMaxThreads();
  }

  /** Sets maximum number of messages that can be enqueued in a client-queue. **/
  public int getMaximumMessageCount() {
    return cacheServer.getMaximumMessageCount();
  }

  /** Returns the maximum amount of time between client pings **/
  public int getMaximumTimeBetweenPings() {
    return cacheServer.getMaximumTimeBetweenPings();
  }

  /**
   * Returns the time (in seconds ) after which a message in the client queue will expire.
   **/
  public int getMessageTimeToLive() {
    return cacheServer.getMessageTimeToLive();
  }

  /** is the server running **/
  public boolean isRunning() {
    return cacheServer.isRunning();
  }

  /**
   * Returns the eviction policy that is executed when capacity of the client queue is reached
   **/
  public String getEvictionPolicy() {
    if (cacheServer.getClientSubscriptionConfig() != null) {
      return cacheServer.getClientSubscriptionConfig().getEvictionPolicy();
    }

    return null;
  }

  /**
   * The hostname or IP address to pass to the client as the loca- tion where the server is
   * listening. When the server connects to the locator it tells the locator the host and port where
   * it is listening for client connections. If the host the server uses by default is one that the
   * client can’t translate into an IP address, the client will have no route to the server’s host
   * and won’t be able to find the server. For this situation, you must supply the server’s
   * alternate hostname for the locator to pass to the cli- ent. If null, the server’s bind-address
   * (page 177) setting is used. Default: null.
   **/
  public String getHostnameForClients() {
    return cacheServer.getHostnameForClients();
  }

  /**
   * The hostname or IP address that the server is to listen on for client connections. If null, the
   * server listens on the machine’s default address. Default: null.
   */
  public String getBindAddress() {
    return cacheServer.getBindAddress();
  }


  public String[] getContinuousQueryList() {
    CqService cqService = cache.getCqService();
    if (cqService != null) {
      Collection<? extends InternalCqQuery> allCqs = cqService.getAllCqs();
      if (allCqs != null && allCqs.size() > 0) {
        String[] allCqStr = new String[allCqs.size()];
        int i = 0;
        for (InternalCqQuery cq : allCqs) {
          allCqStr[i] = cq.getName();
          i++;
        }
        return allCqStr;
      }
    }

    return ManagementConstants.NO_DATA_STRING;
  }

  /**
   * Gets currently executing query count
   */
  public long getRegisteredCQCount() {
    CqService cqService = cache.getCqService();
    if (cqService != null) {
      Collection<? extends InternalCqQuery> allCqs = cqService.getAllCqs();
      return allCqs != null && allCqs.size() > 0 ? allCqs.size() : 0;
    }
    return 0;
  }

  public String[] getIndexList() {
    Collection<Index> idxs = qs.getIndexes();
    if (!idxs.isEmpty()) {
      Iterator<Index> idx = idxs.iterator();
      String[] indexList = new String[idxs.size()];
      int i = 0;
      while (idx.hasNext()) {
        Index index = idx.next();
        indexList[i] = index.getName();
        i++;

      }
      return indexList;
    }

    return ManagementConstants.NO_DATA_STRING;
  }

  /**
   * @return a list of client Ids connected to this particular server instance
   */
  public String[] listClientIds() throws Exception {
    String[] allConnectedClientStr = null;
    Map<String, ClientConnInfo> uniqueIds = getUniqueClientIds();
    if (uniqueIds.size() > 0) {
      allConnectedClientStr = new String[uniqueIds.size()];
      int j = 0;
      for (String clientId : uniqueIds.keySet()) {
        allConnectedClientStr[j] = clientId;
        j++;
      }
      return allConnectedClientStr;
    } else {
      return new String[0];
    }
  }

  private Map<String, ClientConnInfo> getUniqueClientIds() {
    Map<String, ClientConnInfo> uniqueIds = null;

    ServerConnection[] serverConnections = acceptor.getAllServerConnectionList();
    Collection<CacheClientProxy> clientProxies =
        acceptor.getCacheClientNotifier().getClientProxies();

    if (clientProxies.size() > 0) {
      uniqueIds = new HashMap<>();

      for (CacheClientProxy p : clientProxies) {
        ClientConnInfo clientConInfo =
            new ClientConnInfo(p.getProxyID(), p.getSocketHost(), p.getRemotePort(), p.isPrimary());
        uniqueIds.put(p.getProxyID().getDSMembership(), clientConInfo);
      }
    }

    if (serverConnections != null && serverConnections.length > 0) {
      if (uniqueIds == null) {
        uniqueIds = new HashMap<>();
      }
      for (ServerConnection conn : serverConnections) {
        ClientProxyMembershipID clientId = conn.getProxyID();
        if (clientId != null) { // Check added to fix bug 51987
          if (uniqueIds.get(clientId.getDSMembership()) == null) {
            ClientConnInfo clientConInfo = new ClientConnInfo(conn.getProxyID(),
                conn.getSocketHost(), conn.getSocketPort(), false);
            uniqueIds.put(clientId.getDSMembership(), clientConInfo);
          }
        }
      }
    }

    if (uniqueIds == null) {
      return Collections.emptyMap();
    }
    return uniqueIds;
  }

  private static class ClientConnInfo {

    private final ClientProxyMembershipID clientId;

    private final String hostName;

    private final int port;

    boolean isPrimary;

    public ClientConnInfo(ClientProxyMembershipID clientId, String hostName, int port,
        boolean isPrimary) {
      this.clientId = clientId;
      this.hostName = hostName;
      this.port = port;
      this.isPrimary = isPrimary;
    }

    public String getHostName() {
      return hostName;
    }

    public ClientProxyMembershipID getClientId() {
      return clientId;
    }

    public String toString() {
      return "[" + clientId + "; port=" + port + "; primary="
          + isPrimary + "]";
    }
  }

  public KnownVersion getClientVersion(ClientConnInfo connInfo) {
    if (cache.getCacheServers().size() == 0) {
      return null;
    }

    CacheServerImpl server = (CacheServerImpl) cache.getCacheServers().iterator().next();

    if (server == null) {
      return null;
    }

    Acceptor acceptor = server.getAcceptor();
    if (acceptor == null) {
      return null;
    }

    ServerConnection[] serverConnections = acceptor.getAllServerConnectionList();

    boolean flag = connInfo.toString().contains("primary=true");

    for (ServerConnection conn : serverConnections) {
      ClientProxyMembershipID cliIdFrmProxy = conn.getProxyID();
      ClientConnInfo cci =
          new ClientConnInfo(conn.getProxyID(), conn.getSocketHost(), conn.getSocketPort(), flag);
      if (connInfo.toString().equals(cci.toString())) {
        return cliIdFrmProxy.getClientVersion();
      }
    }

    // check form ccp
    ClientProxyMembershipID proxyId = connInfo.getClientId();
    CacheClientProxy proxy = CacheClientNotifier.getInstance().getClientProxy(proxyId);
    if (proxy != null) {
      return proxy.getVersion();
    } else {
      return null;
    }
  }

  public ClientHealthStatus showClientStats(String clientId) throws Exception {
    try {
      Map<String, ClientConnInfo> uniqueClientIds = getUniqueClientIds();
      ClientConnInfo clientConnInfo = uniqueClientIds.get(clientId);
      return getClientHealthStatus(clientConnInfo);
    } finally {
      CacheServerBridge.clientVersion.set(null);
    }
  }

  public ClientHealthStatus[] showAllClientStats() throws Exception {
    try {
      List<ClientHealthStatus> clientHealthStatusList = null;
      Map<String, ClientConnInfo> uniqueClientIds = getUniqueClientIds();
      if (!uniqueClientIds.isEmpty()) {
        clientHealthStatusList = new ArrayList<>();

        for (Map.Entry<String, ClientConnInfo> p : uniqueClientIds.entrySet()) {
          ClientHealthStatus status = getClientHealthStatus(p.getValue());
          if (status != null) {
            clientHealthStatusList.add(status);
          }
        }
        ClientHealthStatus[] statusArr = new ClientHealthStatus[clientHealthStatusList.size()];
        return clientHealthStatusList.toArray(statusArr);
      }
      return new ClientHealthStatus[0];
    } finally {
      CacheServerBridge.clientVersion.set(null);
    }
  }

  private ClientHealthStatus getClientHealthStatus(ClientConnInfo connInfo) {
    ClientProxyMembershipID proxyId = connInfo.getClientId();
    CacheClientProxy proxy = CacheClientNotifier.getInstance().getClientProxy(proxyId);

    if (proxy != null && !proxy.isConnected() && !proxyId.isDurable()) {
      return null;
    }

    CacheServerBridge.clientVersion.set(getClientVersion(connInfo));

    int clientCQCount = 0;
    CqService cqService = cache.getCqService();
    if (cqService != null) {
      List<ServerCQ> cqs = cqService.getAllClientCqs(proxyId);
      clientCQCount = cqs.size();
    }

    ClientHealthStatus status = new ClientHealthStatus();

    Region clientHealthMonitoringRegion = ClientHealthMonitoringRegion.getInstance(cache);
    String clientName = proxyId.getDSMembership();
    DistributedMember clientMemberId = proxyId.getDistributedMember();
    status.setClientId(connInfo.toString());
    status.setName(clientName);
    status.setHostName(connInfo.getHostName());
    status.setClientCQCount(clientCQCount);

    // Only available for clients having subscription enabled true
    if (proxy != null) {
      status.setUpTime(proxy.getUpTime());
      status.setQueueSize(proxy.getQueueSizeStat());
      status.setConnected(proxy.isConnected());
      status.setSubscriptionEnabled(true);
    } else {
      status.setConnected(true);
      status.setSubscriptionEnabled(false);
    }

    ClientHealthStats stats = (ClientHealthStats) clientHealthMonitoringRegion.get(clientMemberId);
    if (stats == null) {
      // Clients older than Geode 1.8 put strings in the region, rather than ids
      // See GEODE-5157
      stats = (ClientHealthStats) clientHealthMonitoringRegion.get(clientName);
    }

    if (stats != null) {
      status.setCpus(stats.getCpus());
      status.setNumOfCacheListenerCalls(stats.getNumOfCacheListenerCalls());
      status.setNumOfGets((int) stats.getNumOfGets());
      status.setNumOfMisses((int) stats.getNumOfMisses());
      status.setNumOfPuts((int) stats.getNumOfPuts());
      status.setNumOfThreads(stats.getNumOfThreads());
      status.setProcessCpuTime(stats.getProcessCpuTime());
      status.setPoolStats(stats.getPoolStats());
    }
    return status;
  }

  /**
   * closes a continuous query and releases all the resources associated with it.
   *
   */
  public void closeContinuousQuery(String queryName) throws Exception {
    CqService cqService = cache.getCqService();
    if (cqService != null) {
      Collection<? extends InternalCqQuery> allCqs = cqService.getAllCqs();
      for (InternalCqQuery query : allCqs) {
        if (query.getName().equals(queryName)) {
          try {
            query.close();
            return;
          } catch (CqClosedException e) {
            throw new Exception(e.getMessage());
          } catch (CqException e) {
            throw new Exception(e.getMessage());
          }
        }
      }
    }
  }

  /**
   * Execute a continuous query
   *
   */
  public void executeContinuousQuery(String queryName) throws Exception {
    CqService cqService = cache.getCqService();
    if (cqService != null) {
      Collection<? extends InternalCqQuery> allCqs = cqService.getAllCqs();
      for (InternalCqQuery query : allCqs) {
        if (query.getName().equals(queryName)) {
          try {
            cqService.resumeCQ(CqStateImpl.RUNNING, (ServerCQ) query);
            return;
          } catch (CqClosedException e) {
            throw new Exception(e.getMessage());
          }
        }
      }
    }
  }

  /**
   * Stops a given query witout releasing any of the resources associated with it.
   *
   */
  public void stopContinuousQuery(String queryName) throws Exception {
    CqService cqService = cache.getCqService();
    if (cqService != null) {
      Collection<? extends InternalCqQuery> allCqs = cqService.getAllCqs();
      for (InternalCqQuery query : allCqs) {
        if (query.getName().equals(queryName)) {
          try {
            query.stop();
            return;
          } catch (CqClosedException e) {
            throw new Exception(e.getMessage());
          } catch (CqException e) {
            throw new Exception(e.getMessage());
          }

        }
      }
    }
  }

  /**
   * remove a given index
   *
   */
  public void removeIndex(String indexName) throws Exception {
    try {
      Collection<Index> idxs = qs.getIndexes();
      if (!idxs.isEmpty()) {
        for (final Index index : idxs) {
          if (index.getName().equals(indexName)) {
            qs.removeIndex(index);
          }
          return;
        }
      }
    } catch (Exception e) {
      throw new Exception(e.getMessage());
    }
  }

  public int getIndexCount() {
    return qs.getIndexes().size();
  }

  public int getNumClientNotificationRequests() {
    return getStatistic(StatsKey.NUM_CLIENT_NOTIFICATION_REQUEST).intValue();
  }

  public long getClientNotificationAvgLatency() {
    return clientNotificatioAvgLatency.getAverageLatency();
  }

  public float getClientNotificationRate() {
    return clientNotificationRate.getRate();
  }

  public float getQueryRequestRate() {
    return queryRequestRate.getRate();
  }

  public long getTotalIndexMaintenanceTime() {
    return memberMBeanBridge.getTotalIndexMaintenanceTime();
  }

  public long getActiveCQCount() {
    CqService cqService = cache.getCqService();
    if (cqService != null && cqService.isRunning()) {
      return cqService.getCqStatistics().numCqsActive();
    }
    return 0;
  }

  public int getNumSubscriptions() {
    Map clientProxyMembershipIDMap = InternalClientMembership.getClientQueueSizes(cache);
    return clientProxyMembershipIDMap.keySet().size();
  }

  public void setClientMembershipListener(ClientMembershipListener membershipListener) {
    this.membershipListener = membershipListener;
  }

  public ClientMembershipListener getClientMembershipListener() {
    return membershipListener;
  }

  /**
   * @return Client Queue Details for all clients
   */
  public ClientQueueDetail[] getClientQueueDetails() throws Exception {
    List<ClientQueueDetail> clientQueueDetailList = null;
    try {
      if (acceptor != null && acceptor.getCacheClientNotifier() != null) {
        Collection<CacheClientProxy> clientProxies =
            acceptor.getCacheClientNotifier().getClientProxies();

        if (clientProxies.size() > 0) {
          clientQueueDetailList = new ArrayList<>();
        } else {
          return new ClientQueueDetail[0];
        }

        for (CacheClientProxy p : clientProxies) {
          ClientQueueDetail status = getClientQueueDetail(p);
          if (status != null) {
            clientQueueDetailList.add(status);
          }

        }
        ClientQueueDetail[] queueDetailArr = new ClientQueueDetail[clientQueueDetailList.size()];
        return clientQueueDetailList.toArray(queueDetailArr);

      }
      return new ClientQueueDetail[0];

    } catch (Exception e) {
      throw new Exception(e.getMessage());
    }
  }

  private ClientQueueDetail getClientQueueDetail(CacheClientProxy p) {
    ClientQueueDetail queueDetail = new ClientQueueDetail();
    ClientProxyMembershipID proxyID = p.getProxyID();

    if (!p.isConnected() && !proxyID.isDurable()) {
      return null;
    }
    queueDetail.setClientId(getClientIdFromCacheClientProxy(p));

    HARegionQueue queue = p.getHARegionQueue();
    if (queue == null) {
      return queueDetail;
    }
    queueDetail.setQueueSize(p.getQueueSizeStat());
    queueDetail.setEventsConflated(queue.getStatistics().getEventsConflated());
    queueDetail.setEventsEnqued(queue.getStatistics().getEventsEnqued());
    queueDetail.setEventsExpired(queue.getStatistics().getEventsExpired());
    queueDetail.setEventsRemoved(queue.getStatistics().getEventsRemoved());
    queueDetail.setEventsRemovedByQrm(queue.getStatistics().getEventsRemovedByQrm());
    queueDetail.setEventsTaken(queue.getStatistics().getEventsTaken());
    queueDetail.setMarkerEventsConflated(queue.getStatistics().getMarkerEventsConflated());
    queueDetail.setNumVoidRemovals(queue.getStatistics().getNumVoidRemovals());
    return queueDetail;
  }

  public ClientQueueDetail getClientQueueDetail(String clientId) throws Exception {
    try {
      if (acceptor != null && acceptor.getCacheClientNotifier() != null) {
        Collection<CacheClientProxy> clientProxies =
            acceptor.getCacheClientNotifier().getClientProxies();
        for (CacheClientProxy p : clientProxies) {
          String buffer = getClientIdFromCacheClientProxy(p);
          if (buffer.equals(clientId)) {
            return getClientQueueDetail(p);
          }
        }
      }
    } catch (Exception e) {
      throw new Exception(e.getMessage());
    }
    return null;
  }


  private static String getClientIdFromCacheClientProxy(CacheClientProxy p) {
    if (p == null) {
      return null;
    }
    return "[" + p.getProxyID() + ":port=" + p.getRemotePort()
        + ":primary=" + p.isPrimary() + "]";
  }

}
