/*
 * 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.gobblin.metrics.graphite;

import java.io.IOException;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Optional;

import org.apache.gobblin.configuration.ConfigurationKeys;
import org.apache.gobblin.metrics.GobblinTrackingEvent;
import org.apache.gobblin.metrics.MetricContext;
import org.apache.gobblin.metrics.event.MultiPartEvent;
import org.apache.gobblin.metrics.event.EventSubmitter;
import org.apache.gobblin.metrics.event.JobEvent;
import org.apache.gobblin.metrics.event.TaskEvent;
import org.apache.gobblin.metrics.reporter.EventReporter;

import static org.apache.gobblin.metrics.event.TimingEvent.METADATA_DURATION;


/**
 *
 * {@link org.apache.gobblin.metrics.reporter.EventReporter} that emits {@link org.apache.gobblin.metrics.GobblinTrackingEvent} events
 * as timestamped name - value pairs through the Graphite protocol
 *
 * @author Lorand Bendig
 *
 */
public class GraphiteEventReporter extends EventReporter {

  private final GraphitePusher graphitePusher;
  private final boolean emitValueAsKey;

  private static final String EMTPY_VALUE = "0";
  private static final Logger LOGGER = LoggerFactory.getLogger(GraphiteEventReporter.class);
  private String prefix;

  public GraphiteEventReporter(Builder<?> builder) throws IOException {
    super(builder);
    if (builder.graphitePusher.isPresent()) {
      this.graphitePusher = builder.graphitePusher.get();
    } else {
      this.graphitePusher =
          this.closer.register(new GraphitePusher(builder.hostname, builder.port, builder.connectionType));
    }
    this.emitValueAsKey = builder.emitValueAsKey;
    this.prefix = builder.prefix;
  }

  @Override
  public void reportEventQueue(Queue<GobblinTrackingEvent> queue) {

    GobblinTrackingEvent nextEvent;
    try {
      while (null != (nextEvent = queue.poll())) {
        pushEvent(nextEvent);
      }
      this.graphitePusher.flush();
    } catch (IOException e) {
      LOGGER.error("Error sending event to Graphite", e);
      try {
        this.graphitePusher.flush();
      } catch (IOException e1) {
        LOGGER.error("Unable to flush previous events to Graphite", e);
      }
    }
  }

  /**
   * Extracts the event and its metadata from {@link GobblinTrackingEvent} and creates
   * timestamped name value pairs
   *
   * @param event {@link GobblinTrackingEvent} to be reported
   * @throws IOException
   */
  private void pushEvent(GobblinTrackingEvent event) throws IOException {

    Map<String, String> metadata = event.getMetadata();
    String name = getMetricName(metadata, event.getName());
    long timestamp = event.getTimestamp() / 1000l;
    MultiPartEvent multipartEvent = MultiPartEvent.getEvent(metadata.get(EventSubmitter.EVENT_TYPE));
    if (multipartEvent == null) {
      graphitePusher.push(JOINER.join(prefix, name), EMTPY_VALUE, timestamp);
    }
    else {
      for (String field : multipartEvent.getMetadataFields()) {
        String value = metadata.get(field);
        if (value == null) {
          graphitePusher.push(JOINER.join(prefix, name, field), EMTPY_VALUE, timestamp);
        } else {
          if (emitAsKey(field)) {
            // metric value is emitted as part of the keys
            graphitePusher.push(JOINER.join(prefix, name, field, value), EMTPY_VALUE, timestamp);
          } else {
            graphitePusher.push(JOINER.join(prefix, name, field), convertValue(field, value), timestamp);
          }
        }
      }
    }
  }

  private String convertValue(String field, String value) {
    return METADATA_DURATION.equals(field) ?
        Double.toString(convertDuration(TimeUnit.MILLISECONDS.toNanos(Long.parseLong(value)))) : value;
  }

  /**
   * Non-numeric event values may be emitted as part of the key by applying them to the end of the key if
   * {@link ConfigurationKeys#METRICS_REPORTING_GRAPHITE_EVENTS_VALUE_AS_KEY} is set. Thus such events can be still
   * reported even when the backend doesn't accept text values through Graphite
   *
   * @param field name of the metric's metadata fields
   * @return true if event value is emitted in the key
   */
  private boolean emitAsKey(String field) {
    return emitValueAsKey
        && (field.equals(TaskEvent.METADATA_TASK_WORKING_STATE) || field.equals(JobEvent.METADATA_JOB_STATE));
  }

  /**
   * Returns a new {@link GraphiteEventReporter.Builder} for {@link GraphiteEventReporter}.
   * Will automatically add all Context tags to the reporter.
   *
   * @param context the {@link org.apache.gobblin.metrics.MetricContext} to report
   * @return GraphiteEventReporter builder
   * @deprecated this method is bugged. Use {@link GraphiteEventReporter.Factory#forContext} instead.
   */
  @Deprecated
  public static Builder<? extends Builder> forContext(MetricContext context) {
    return new BuilderImpl(context);
  }

  public static class BuilderImpl extends Builder<BuilderImpl> {

    private BuilderImpl(MetricContext context) {
      super(context);
    }

    @Override
    protected BuilderImpl self() {
      return this;
    }
  }

  public static class Factory {
    /**
     * Returns a new {@link GraphiteEventReporter.Builder} for {@link GraphiteEventReporter}.
     * Will automatically add all Context tags to the reporter.
     *
     * @param context the {@link org.apache.gobblin.metrics.MetricContext} to report
     * @return GraphiteEventReporter builder
     */
    public static BuilderImpl forContext(MetricContext context) {
      return new BuilderImpl(context);
    }
  }

  /**
   * Builder for {@link GraphiteEventReporter}.
   * Defaults to no filter, reporting rates in seconds and times in milliseconds using TCP connection
   */
  public static abstract class Builder<T extends EventReporter.Builder<T>> extends EventReporter.Builder<T> {
    protected String hostname;
    protected int port;
    protected GraphiteConnectionType connectionType;
    protected Optional<GraphitePusher> graphitePusher;
    protected boolean emitValueAsKey;
    protected String prefix;

    protected Builder(MetricContext context) {
      super(context);
      this.graphitePusher = Optional.absent();
      this.connectionType = GraphiteConnectionType.TCP;
    }

    /**
     * Set {@link org.apache.gobblin.metrics.graphite.GraphitePusher} to use.
     */
    public T withGraphitePusher(GraphitePusher pusher) {
      this.graphitePusher = Optional.of(pusher);
      return self();
    }

    /**
     * Set connection parameters for the {@link org.apache.gobblin.metrics.graphite.GraphitePusher} creation
     */
    public T withConnection(String hostname, int port) {
      this.hostname = hostname;
      this.port = port;
      return self();
    }

    public T withPrefix(String prefix) {
      this.prefix = prefix;
      return self();
    }

    /**
     * Set {@link org.apache.gobblin.metrics.graphite.GraphiteConnectionType} to use.
     */
    public T withConnectionType(GraphiteConnectionType connectionType) {
      this.connectionType = connectionType;
      return self();
    }

    /**
     * Set flag that forces the reporter to emit non-numeric event values as part of the key
     */
    public T withEmitValueAsKey(boolean emitValueAsKey) {
      this.emitValueAsKey = emitValueAsKey;
      return self();
    }

    /**
     * Builds and returns {@link GraphiteEventReporter}.
     *
     * @return GraphiteEventReporter
     */
    public GraphiteEventReporter build() throws IOException {
      return new GraphiteEventReporter(this);
    }
  }

}
