package couchdb

import (
	"encoding/json"
	"fmt"
	"net/http"
	"sync"
	"time"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/plugins/inputs"
)

type (
	metaData struct {
		Current *float64 `json:"current"`
		Sum     *float64 `json:"sum"`
		Mean    *float64 `json:"mean"`
		Stddev  *float64 `json:"stddev"`
		Min     *float64 `json:"min"`
		Max     *float64 `json:"max"`
		Value   *float64 `json:"value"`
	}

	oldValue struct {
		Value metaData `json:"value"`
		metaData
	}

	couchdb struct {
		AuthCacheHits       metaData            `json:"auth_cache_hits"`
		AuthCacheMisses     metaData            `json:"auth_cache_misses"`
		DatabaseWrites      metaData            `json:"database_writes"`
		DatabaseReads       metaData            `json:"database_reads"`
		OpenDatabases       metaData            `json:"open_databases"`
		OpenOsFiles         metaData            `json:"open_os_files"`
		RequestTime         oldValue            `json:"request_time"`
		HttpdRequestMethods httpdRequestMethods `json:"httpd_request_methods"`
		HttpdStatusCodes    httpdStatusCodes    `json:"httpd_status_codes"`
	}

	httpdRequestMethods struct {
		Put    metaData `json:"PUT"`
		Get    metaData `json:"GET"`
		Copy   metaData `json:"COPY"`
		Delete metaData `json:"DELETE"`
		Post   metaData `json:"POST"`
		Head   metaData `json:"HEAD"`
	}

	httpdStatusCodes struct {
		Status200 metaData `json:"200"`
		Status201 metaData `json:"201"`
		Status202 metaData `json:"202"`
		Status301 metaData `json:"301"`
		Status304 metaData `json:"304"`
		Status400 metaData `json:"400"`
		Status401 metaData `json:"401"`
		Status403 metaData `json:"403"`
		Status404 metaData `json:"404"`
		Status405 metaData `json:"405"`
		Status409 metaData `json:"409"`
		Status412 metaData `json:"412"`
		Status500 metaData `json:"500"`
	}

	httpd struct {
		BulkRequests             metaData `json:"bulk_requests"`
		Requests                 metaData `json:"requests"`
		TemporaryViewReads       metaData `json:"temporary_view_reads"`
		ViewReads                metaData `json:"view_reads"`
		ClientsRequestingChanges metaData `json:"clients_requesting_changes"`
	}

	Stats struct {
		Couchdb             couchdb             `json:"couchdb"`
		HttpdRequestMethods httpdRequestMethods `json:"httpd_request_methods"`
		HttpdStatusCodes    httpdStatusCodes    `json:"httpd_status_codes"`
		Httpd               httpd               `json:"httpd"`
	}

	CouchDB struct {
		Hosts         []string `toml:"hosts"`
		BasicUsername string   `toml:"basic_username"`
		BasicPassword string   `toml:"basic_password"`

		client *http.Client
	}
)

func (*CouchDB) Description() string {
	return "Read CouchDB Stats from one or more servers"
}

func (*CouchDB) SampleConfig() string {
	return `
  ## Works with CouchDB stats endpoints out of the box
  ## Multiple Hosts from which to read CouchDB stats:
  hosts = ["http://localhost:8086/_stats"]

  ## Use HTTP Basic Authentication.
  # basic_username = "telegraf"
  # basic_password = "p@ssw0rd"
`
}

func (c *CouchDB) Gather(accumulator telegraf.Accumulator) error {
	var wg sync.WaitGroup
	for _, u := range c.Hosts {
		wg.Add(1)
		go func(host string) {
			defer wg.Done()
			if err := c.fetchAndInsertData(accumulator, host); err != nil {
				accumulator.AddError(fmt.Errorf("[host=%s]: %s", host, err))
			}
		}(u)
	}

	wg.Wait()

	return nil
}

func (c *CouchDB) fetchAndInsertData(accumulator telegraf.Accumulator, host string) error {
	if c.client == nil {
		c.client = &http.Client{
			Transport: &http.Transport{
				ResponseHeaderTimeout: 3 * time.Second,
			},
			Timeout: 4 * time.Second,
		}
	}

	req, err := http.NewRequest("GET", host, nil)
	if err != nil {
		return err
	}

	if c.BasicUsername != "" || c.BasicPassword != "" {
		req.SetBasicAuth(c.BasicUsername, c.BasicPassword)
	}

	response, err := c.client.Do(req)
	if err != nil {
		return err
	}
	defer response.Body.Close()

	if response.StatusCode != 200 {
		return fmt.Errorf("failed to get stats from couchdb: HTTP responded %d", response.StatusCode)
	}

	stats := Stats{}
	decoder := json.NewDecoder(response.Body)
	if err := decoder.Decode(&stats); err != nil {
		return fmt.Errorf("failed to decode stats from couchdb: HTTP body %q", response.Body)
	}

	fields := map[string]interface{}{}

	// for couchdb 2.0 API changes
	requestTime := metaData{
		Current: stats.Couchdb.RequestTime.Current,
		Sum:     stats.Couchdb.RequestTime.Sum,
		Mean:    stats.Couchdb.RequestTime.Mean,
		Stddev:  stats.Couchdb.RequestTime.Stddev,
		Min:     stats.Couchdb.RequestTime.Min,
		Max:     stats.Couchdb.RequestTime.Max,
	}

	httpdRequestMethodsPut := stats.HttpdRequestMethods.Put
	httpdRequestMethodsGet := stats.HttpdRequestMethods.Get
	httpdRequestMethodsCopy := stats.HttpdRequestMethods.Copy
	httpdRequestMethodsDelete := stats.HttpdRequestMethods.Delete
	httpdRequestMethodsPost := stats.HttpdRequestMethods.Post
	httpdRequestMethodsHead := stats.HttpdRequestMethods.Head

	httpdStatusCodesStatus200 := stats.HttpdStatusCodes.Status200
	httpdStatusCodesStatus201 := stats.HttpdStatusCodes.Status201
	httpdStatusCodesStatus202 := stats.HttpdStatusCodes.Status202
	httpdStatusCodesStatus301 := stats.HttpdStatusCodes.Status301
	httpdStatusCodesStatus304 := stats.HttpdStatusCodes.Status304
	httpdStatusCodesStatus400 := stats.HttpdStatusCodes.Status400
	httpdStatusCodesStatus401 := stats.HttpdStatusCodes.Status401
	httpdStatusCodesStatus403 := stats.HttpdStatusCodes.Status403
	httpdStatusCodesStatus404 := stats.HttpdStatusCodes.Status404
	httpdStatusCodesStatus405 := stats.HttpdStatusCodes.Status405
	httpdStatusCodesStatus409 := stats.HttpdStatusCodes.Status409
	httpdStatusCodesStatus412 := stats.HttpdStatusCodes.Status412
	httpdStatusCodesStatus500 := stats.HttpdStatusCodes.Status500
	// check if couchdb2.0 is used
	if stats.Couchdb.HttpdRequestMethods.Get.Value != nil {
		requestTime = stats.Couchdb.RequestTime.Value

		httpdRequestMethodsPut = stats.Couchdb.HttpdRequestMethods.Put
		httpdRequestMethodsGet = stats.Couchdb.HttpdRequestMethods.Get
		httpdRequestMethodsCopy = stats.Couchdb.HttpdRequestMethods.Copy
		httpdRequestMethodsDelete = stats.Couchdb.HttpdRequestMethods.Delete
		httpdRequestMethodsPost = stats.Couchdb.HttpdRequestMethods.Post
		httpdRequestMethodsHead = stats.Couchdb.HttpdRequestMethods.Head

		httpdStatusCodesStatus200 = stats.Couchdb.HttpdStatusCodes.Status200
		httpdStatusCodesStatus201 = stats.Couchdb.HttpdStatusCodes.Status201
		httpdStatusCodesStatus202 = stats.Couchdb.HttpdStatusCodes.Status202
		httpdStatusCodesStatus301 = stats.Couchdb.HttpdStatusCodes.Status301
		httpdStatusCodesStatus304 = stats.Couchdb.HttpdStatusCodes.Status304
		httpdStatusCodesStatus400 = stats.Couchdb.HttpdStatusCodes.Status400
		httpdStatusCodesStatus401 = stats.Couchdb.HttpdStatusCodes.Status401
		httpdStatusCodesStatus403 = stats.Couchdb.HttpdStatusCodes.Status403
		httpdStatusCodesStatus404 = stats.Couchdb.HttpdStatusCodes.Status404
		httpdStatusCodesStatus405 = stats.Couchdb.HttpdStatusCodes.Status405
		httpdStatusCodesStatus409 = stats.Couchdb.HttpdStatusCodes.Status409
		httpdStatusCodesStatus412 = stats.Couchdb.HttpdStatusCodes.Status412
		httpdStatusCodesStatus500 = stats.Couchdb.HttpdStatusCodes.Status500
	}

	// CouchDB meta stats:
	c.generateFields(fields, "couchdb_auth_cache_misses", stats.Couchdb.AuthCacheMisses)
	c.generateFields(fields, "couchdb_database_writes", stats.Couchdb.DatabaseWrites)
	c.generateFields(fields, "couchdb_open_databases", stats.Couchdb.OpenDatabases)
	c.generateFields(fields, "couchdb_auth_cache_hits", stats.Couchdb.AuthCacheHits)
	c.generateFields(fields, "couchdb_request_time", requestTime)
	c.generateFields(fields, "couchdb_database_reads", stats.Couchdb.DatabaseReads)
	c.generateFields(fields, "couchdb_open_os_files", stats.Couchdb.OpenOsFiles)

	// http request methods stats:
	c.generateFields(fields, "httpd_request_methods_put", httpdRequestMethodsPut)
	c.generateFields(fields, "httpd_request_methods_get", httpdRequestMethodsGet)
	c.generateFields(fields, "httpd_request_methods_copy", httpdRequestMethodsCopy)
	c.generateFields(fields, "httpd_request_methods_delete", httpdRequestMethodsDelete)
	c.generateFields(fields, "httpd_request_methods_post", httpdRequestMethodsPost)
	c.generateFields(fields, "httpd_request_methods_head", httpdRequestMethodsHead)

	// status code stats:
	c.generateFields(fields, "httpd_status_codes_200", httpdStatusCodesStatus200)
	c.generateFields(fields, "httpd_status_codes_201", httpdStatusCodesStatus201)
	c.generateFields(fields, "httpd_status_codes_202", httpdStatusCodesStatus202)
	c.generateFields(fields, "httpd_status_codes_301", httpdStatusCodesStatus301)
	c.generateFields(fields, "httpd_status_codes_304", httpdStatusCodesStatus304)
	c.generateFields(fields, "httpd_status_codes_400", httpdStatusCodesStatus400)
	c.generateFields(fields, "httpd_status_codes_401", httpdStatusCodesStatus401)
	c.generateFields(fields, "httpd_status_codes_403", httpdStatusCodesStatus403)
	c.generateFields(fields, "httpd_status_codes_404", httpdStatusCodesStatus404)
	c.generateFields(fields, "httpd_status_codes_405", httpdStatusCodesStatus405)
	c.generateFields(fields, "httpd_status_codes_409", httpdStatusCodesStatus409)
	c.generateFields(fields, "httpd_status_codes_412", httpdStatusCodesStatus412)
	c.generateFields(fields, "httpd_status_codes_500", httpdStatusCodesStatus500)

	// httpd stats:
	c.generateFields(fields, "httpd_clients_requesting_changes", stats.Httpd.ClientsRequestingChanges)
	c.generateFields(fields, "httpd_temporary_view_reads", stats.Httpd.TemporaryViewReads)
	c.generateFields(fields, "httpd_requests", stats.Httpd.Requests)
	c.generateFields(fields, "httpd_bulk_requests", stats.Httpd.BulkRequests)
	c.generateFields(fields, "httpd_view_reads", stats.Httpd.ViewReads)

	tags := map[string]string{
		"server": host,
	}
	accumulator.AddFields("couchdb", fields, tags)
	return nil
}

func (c *CouchDB) generateFields(fields map[string]interface{}, prefix string, obj metaData) {
	if obj.Value != nil {
		fields[prefix+"_value"] = *obj.Value
	}
	if obj.Current != nil {
		fields[prefix+"_current"] = *obj.Current
	}
	if obj.Sum != nil {
		fields[prefix+"_sum"] = *obj.Sum
	}
	if obj.Mean != nil {
		fields[prefix+"_mean"] = *obj.Mean
	}
	if obj.Stddev != nil {
		fields[prefix+"_stddev"] = *obj.Stddev
	}
	if obj.Min != nil {
		fields[prefix+"_min"] = *obj.Min
	}
	if obj.Max != nil {
		fields[prefix+"_max"] = *obj.Max
	}
}

func init() {
	inputs.Add("couchdb", func() telegraf.Input {
		return &CouchDB{
			client: &http.Client{
				Transport: &http.Transport{
					ResponseHeaderTimeout: 3 * time.Second,
				},
				Timeout: 4 * time.Second,
			},
		}
	})
}
