/*
 * 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
 *
 *   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.streams.monitoring.tasks;

import org.apache.streams.config.ComponentConfigurator;
import org.apache.streams.config.StreamsConfiguration;
import org.apache.streams.config.StreamsConfigurator;
import org.apache.streams.jackson.DatumStatusCounterDeserializer;
import org.apache.streams.jackson.MemoryUsageDeserializer;
import org.apache.streams.jackson.StreamsJacksonMapper;
import org.apache.streams.jackson.StreamsTaskCounterDeserializer;
import org.apache.streams.jackson.ThroughputQueueDeserializer;
import org.apache.streams.local.monitoring.MonitoringConfiguration;
import org.apache.streams.monitoring.persist.MessagePersister;
import org.apache.streams.monitoring.persist.impl.BroadcastMessagePersister;
import org.apache.streams.monitoring.persist.impl.LogstashUdpMessagePersister;
import org.apache.streams.monitoring.persist.impl.Slf4jMessagePersister;
import org.apache.streams.pojo.json.Broadcast;
import org.apache.streams.pojo.json.DatumStatusCounterBroadcast;
import org.apache.streams.pojo.json.MemoryUsageBroadcast;
import org.apache.streams.pojo.json.StreamsTaskCounterBroadcast;
import org.apache.streams.pojo.json.ThroughputQueueBroadcast;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.common.base.Preconditions;
import org.slf4j.Logger;

import java.lang.management.ManagementFactory;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.management.MBeanServer;
import javax.management.NotificationBroadcasterSupport;
import javax.management.ObjectName;

/**
 * This thread runs inside of a Streams runtime and periodically persists information
 * from relevant JMX beans.
 */
public class BroadcastMonitorThread extends NotificationBroadcasterSupport implements Runnable {

  private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger(BroadcastMonitorThread.class);
  private static MBeanServer server;

  private MonitoringConfiguration configuration;
  private URI broadcastUri = null;
  private MessagePersister messagePersister;
  private volatile boolean keepRunning;

  private static ObjectMapper objectMapper = StreamsJacksonMapper.getInstance();

  public BroadcastMonitorThread() {
    this(new ComponentConfigurator<>(MonitoringConfiguration.class).detectConfiguration());
  }

  /**
   * BroadcastMonitorThread constructor - uses supplied MonitoringConfiguration.
   * @param configuration MonitoringConfiguration
   */
  public BroadcastMonitorThread(MonitoringConfiguration configuration) {

    this.configuration = configuration;

    LOGGER.info("BroadcastMonitorThread created");

    initializeObjectMapper();

    prepare();

    LOGGER.info("BroadcastMonitorThread initialized");

  }

  /**
   * Initialize our object mapper with all of our bean's custom deserializers.
   * This way we can convert them to and from Strings dictated by our
   * POJOs which are generated from JSON schemas.
   */
  private void initializeObjectMapper() {
    SimpleModule simpleModule = new SimpleModule();

    simpleModule.addDeserializer(MemoryUsageBroadcast.class, new MemoryUsageDeserializer());
    simpleModule.addDeserializer(ThroughputQueueBroadcast.class, new ThroughputQueueDeserializer());
    simpleModule.addDeserializer(StreamsTaskCounterBroadcast.class, new StreamsTaskCounterDeserializer());
    simpleModule.addDeserializer(DatumStatusCounterBroadcast.class, new DatumStatusCounterDeserializer());

    objectMapper.registerModule(simpleModule);
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
  }

  /**
   * Get all relevant JMX beans, convert their values to strings, and then persist them.
   */
  @Override
  public void run() {
    LOGGER.info("BroadcastMonitorThread running");
    Preconditions.checkNotNull(configuration);
    Preconditions.checkNotNull(configuration.getMonitoringBroadcastIntervalMs());
    while (keepRunning) {
      try {
        List<String> messages = new ArrayList<>();
        Set<ObjectName> beans = server.queryNames(null, null);

        for (ObjectName name : beans) {
          String item = objectMapper.writeValueAsString(name);
          Broadcast broadcast = null;

          if (name.getKeyPropertyList().get("type") != null) {
            if (name.getKeyPropertyList().get("type").equals("ThroughputQueue")) {
              broadcast = objectMapper.readValue(item, ThroughputQueueBroadcast.class);
            } else if (name.getKeyPropertyList().get("type").equals("StreamsTaskCounter")) {
              broadcast = objectMapper.readValue(item, StreamsTaskCounterBroadcast.class);
            } else if (name.getKeyPropertyList().get("type").equals("DatumStatusCounter")) {
              broadcast = objectMapper.readValue(item, DatumStatusCounterBroadcast.class);
            } else if (name.getKeyPropertyList().get("type").equals("Memory")) {
              broadcast = objectMapper.readValue(item, MemoryUsageBroadcast.class);
            }

            if (broadcast != null) {
              messages.add(objectMapper.writeValueAsString(broadcast));
            }
          }
        }

        messagePersister.persistMessages(messages);
        Thread.sleep(configuration.getMonitoringBroadcastIntervalMs());
      } catch (InterruptedException ex) {
        LOGGER.debug("Broadcast Monitor Interrupted!");
        Thread.currentThread().interrupt();
        this.keepRunning = false;
      } catch (Exception ex) {
        LOGGER.error("Exception: {}", ex);
        this.keepRunning = false;
      }
    }
  }

  /**
   * prepare for execution.
   */
  public void prepare() {

    Preconditions.checkNotNull(configuration);
    Preconditions.checkNotNull(configuration.getMonitoringBroadcastIntervalMs());

    keepRunning = true;

    LOGGER.info("BroadcastMonitorThread setup " + this.configuration);

    server = ManagementFactory.getPlatformMBeanServer();

    if (this.configuration.getBroadcastURI() != null) {

      try {
        broadcastUri = new URI(configuration.getBroadcastURI());
      } catch (Exception ex) {
        LOGGER.error("invalid URI: ", ex);
      }

      if (broadcastUri != null) {
        switch (broadcastUri.getScheme()) {
          case "http":
            messagePersister = new BroadcastMessagePersister(broadcastUri.toString());
            break;
          case "udp":
            messagePersister = new LogstashUdpMessagePersister(broadcastUri.toString());
            break;
          default:
            LOGGER.error("You need to specify a broadcast URI with either a HTTP or UDP protocol defined.");
            throw new RuntimeException();
        }
      } else {
        messagePersister = new Slf4jMessagePersister();
      }
    } else {
      messagePersister = new Slf4jMessagePersister();
    }

  }

  public void shutdown() {
    this.keepRunning = false;
    LOGGER.debug("Shutting down BroadcastMonitor Thread");
  }

  public String getBroadcastUri() {
    return configuration.getBroadcastURI();
  }

  public long getWaitTime() {
    return configuration.getMonitoringBroadcastIntervalMs();
  }

}
