package instrumental

import (
	"fmt"
	"io"
	"log"
	"net"
	"regexp"
	"strings"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/internal"
	"github.com/influxdata/telegraf/plugins/outputs"
	"github.com/influxdata/telegraf/plugins/serializers"
	"github.com/influxdata/telegraf/plugins/serializers/graphite"
)

type Instrumental struct {
	Host       string
	ApiToken   string
	Prefix     string
	DataFormat string
	Template   string
	Timeout    internal.Duration
	Debug      bool

	conn net.Conn
}

const (
	DefaultHost     = "collector.instrumentalapp.com"
	HelloMessage    = "hello version go/telegraf/1.1\n"
	AuthFormat      = "authenticate %s\n"
	HandshakeFormat = HelloMessage + AuthFormat
)

var (
	StatIncludesBadChar = regexp.MustCompile("[^[:alnum:][:blank:]-_.]")
)

var sampleConfig = `
  ## Project API Token (required)
  api_token = "API Token" # required
  ## Prefix the metrics with a given name
  prefix = ""
  ## Stats output template (Graphite formatting)
  ## see https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_OUTPUT.md#graphite
  template = "host.tags.measurement.field"
  ## Timeout in seconds to connect
  timeout = "2s"
  ## Display Communcation to Instrumental
  debug = false
`

func (i *Instrumental) Connect() error {
	connection, err := net.DialTimeout("tcp", i.Host+":8000", i.Timeout.Duration)

	if err != nil {
		i.conn = nil
		return err
	}

	err = i.authenticate(connection)
	if err != nil {
		i.conn = nil
		return err
	}

	return nil
}

func (i *Instrumental) Close() error {
	i.conn.Close()
	i.conn = nil
	return nil
}

func (i *Instrumental) Write(metrics []telegraf.Metric) error {
	if i.conn == nil {
		err := i.Connect()
		if err != nil {
			return fmt.Errorf("FAILED to (re)connect to Instrumental. Error: %s\n", err)
		}
	}

	s, err := serializers.NewGraphiteSerializer(i.Prefix, i.Template)
	if err != nil {
		return err
	}

	var points []string
	var metricType string
	var toSerialize telegraf.Metric
	var newTags map[string]string

	for _, metric := range metrics {
		// Pull the metric_type out of the metric's tags. We don't want the type
		// to show up with the other tags pulled from the system, as they go in the
		// beginning of the line instead.
		// e.g we want:
		//
		//  increment some_prefix.host.tag1.tag2.tag3.field value timestamp
		//
		// vs
		//
		//  increment some_prefix.host.tag1.tag2.tag3.counter.field value timestamp
		//
		newTags = metric.Tags()
		metricType = newTags["metric_type"]
		delete(newTags, "metric_type")

		toSerialize, _ = telegraf.NewMetric(
			metric.Name(),
			newTags,
			metric.Fields(),
			metric.Time(),
		)

		stats, err := s.Serialize(toSerialize)
		if err != nil {
			log.Printf("Error serializing a metric to Instrumental: %s", err)
		}

		switch metricType {
		case "counter":
			fallthrough
		case "histogram":
			metricType = "increment"
		default:
			metricType = "gauge"
		}

		for _, stat := range stats {
			if !StatIncludesBadChar.MatchString(stat) {
				points = append(points, fmt.Sprintf("%s %s", metricType, stat))
			} else if i.Debug {
				log.Printf("Unable to send bad stat: %s", stat)
			}
		}
	}

	allPoints := strings.Join(points, "\n") + "\n"
	_, err = fmt.Fprintf(i.conn, allPoints)

	if i.Debug {
		log.Println(allPoints)
	}

	if err != nil {
		if err == io.EOF {
			i.Close()
		}

		return err
	}

	// force the connection closed after sending data
	// to deal with various disconnection scenarios and eschew holding
	// open idle connections en masse
	i.Close()

	return nil
}

func (i *Instrumental) Description() string {
	return "Configuration for sending metrics to an Instrumental project"
}

func (i *Instrumental) SampleConfig() string {
	return sampleConfig
}

func (i *Instrumental) authenticate(conn net.Conn) error {
	_, err := fmt.Fprintf(conn, HandshakeFormat, i.ApiToken)
	if err != nil {
		return err
	}

	// The response here will either be two "ok"s or an error message.
	responses := make([]byte, 512)
	if _, err = conn.Read(responses); err != nil {
		return err
	}

	if string(responses)[:6] != "ok\nok\n" {
		return fmt.Errorf("Authentication failed: %s", responses)
	}

	i.conn = conn
	return nil
}

func init() {
	outputs.Add("instrumental", func() telegraf.Output {
		return &Instrumental{
			Host:     DefaultHost,
			Template: graphite.DEFAULT_TEMPLATE,
		}
	})
}