package marklogic

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

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

// Marklogic configuration toml
type Marklogic struct {
	URL      string   `toml:"url"`
	Hosts    []string `toml:"hosts"`
	Username string   `toml:"username"`
	Password string   `toml:"password"`
	Sources  []string

	tls.ClientConfig

	client *http.Client
}

type MlPointInt struct {
	Value int `json:"value"`
}

type MlPointFloat struct {
	Value float64 `json:"value"`
}

type MlPointBool struct {
	Value bool `json:"value"`
}

// MarkLogic v2 management api endpoints for hosts status
const statsPath = "/manage/v2/hosts/"
const viewFormat = "view=status&format=json"

type MlHost struct {
	HostStatus struct {
		ID               string `json:"id"`
		Name             string `json:"name"`
		StatusProperties struct {
			Online         MlPointBool `json:"online"`
			LoadProperties struct {
				TotalLoad MlPointFloat `json:"total-load"`
			} `json:"load-properties"`
			RateProperties struct {
				TotalRate MlPointFloat `json:"total-rate"`
			} `json:"rate-properties"`
			StatusDetail struct {
				Cpus                   MlPointInt `json:"cpus"`
				Cores                  MlPointInt `json:"cores"`
				TotalCPUStatUser       float64    `json:"total-cpu-stat-user"`
				TotalCPUStatSystem     float64    `json:"total-cpu-stat-system"`
				TotalCPUStatIdle       float64    `json:"total-cpu-stat-idle"`
				TotalCPUStatIowait     float64    `json:"total-cpu-stat-iowait"`
				MemoryProcessSize      MlPointInt `json:"memory-process-size"`
				MemoryProcessRss       MlPointInt `json:"memory-process-rss"`
				MemorySystemTotal      MlPointInt `json:"memory-system-total"`
				MemorySystemFree       MlPointInt `json:"memory-system-free"`
				MemoryProcessSwapSize  MlPointInt `json:"memory-process-swap-size"`
				MemorySize             MlPointInt `json:"memory-size"`
				HostSize               MlPointInt `json:"host-size"`
				LogDeviceSpace         MlPointInt `json:"log-device-space"`
				DataDirSpace           MlPointInt `json:"data-dir-space"`
				QueryReadBytes         MlPointInt `json:"query-read-bytes"`
				QueryReadLoad          MlPointInt `json:"query-read-load"`
				MergeReadLoad          MlPointInt `json:"merge-read-load"`
				MergeWriteLoad         MlPointInt `json:"merge-write-load"`
				HTTPServerReceiveBytes MlPointInt `json:"http-server-receive-bytes"`
				HTTPServerSendBytes    MlPointInt `json:"http-server-send-bytes"`
			} `json:"status-detail"`
		} `json:"status-properties"`
	} `json:"host-status"`
}

// Description of plugin returned
func (c *Marklogic) Description() string {
	return "Retrieves information on a specific host in a MarkLogic Cluster"
}

var sampleConfig = `
  ## Base URL of the MarkLogic HTTP Server.
  url = "http://localhost:8002"

  ## List of specific hostnames to retrieve information. At least (1) required.
  # hosts = ["hostname1", "hostname2"]

  ## Using HTTP Basic Authentication. Management API requires 'manage-user' role privileges
  # username = "myuser"
  # password = "mypassword"

  ## Optional TLS Config
  # tls_ca = "/etc/telegraf/ca.pem"
  # tls_cert = "/etc/telegraf/cert.pem"
  # tls_key = "/etc/telegraf/key.pem"
  ## Use TLS but skip chain & host verification
  # insecure_skip_verify = false
`

// Init parse all source URLs and place on the Marklogic struct
func (c *Marklogic) Init() error {

	if len(c.URL) == 0 {
		c.URL = "http://localhost:8002/"
	}

	for _, u := range c.Hosts {
		base, err := url.Parse(c.URL)
		if err != nil {
			return err
		}

		base.Path = path.Join(base.Path, statsPath, u)
		addr := base.ResolveReference(base)

		addr.RawQuery = viewFormat
		u := addr.String()
		c.Sources = append(c.Sources, u)
	}
	return nil
}

// SampleConfig to gather stats from localhost, default port.
func (c *Marklogic) SampleConfig() string {
	return sampleConfig
}

// Gather metrics from HTTP Server.
func (c *Marklogic) Gather(accumulator telegraf.Accumulator) error {
	var wg sync.WaitGroup

	if c.client == nil {
		client, err := c.createHTTPClient()

		if err != nil {
			return err
		}
		c.client = client
	}

	// Range over all source URL's appended to the struct
	for _, serv := range c.Sources {
		//fmt.Printf("Encoded URL is %q\n", serv)
		wg.Add(1)
		go func(serv string) {
			defer wg.Done()
			if err := c.fetchAndInsertData(accumulator, serv); err != nil {
				accumulator.AddError(fmt.Errorf("[host=%s]: %s", serv, err))
			}
		}(serv)
	}

	wg.Wait()

	return nil
}

func (c *Marklogic) fetchAndInsertData(acc telegraf.Accumulator, url string) error {
	ml := &MlHost{}
	if err := c.gatherJSONData(url, ml); err != nil {
		return err
	}

	// Build a map of tags
	tags := map[string]string{
		"source": ml.HostStatus.Name,
		"id":     ml.HostStatus.ID,
	}

	// Build a map of field values
	fields := map[string]interface{}{
		"online":                    ml.HostStatus.StatusProperties.Online.Value,
		"total_load":                ml.HostStatus.StatusProperties.LoadProperties.TotalLoad.Value,
		"total_rate":                ml.HostStatus.StatusProperties.RateProperties.TotalRate.Value,
		"ncpus":                     ml.HostStatus.StatusProperties.StatusDetail.Cpus.Value,
		"ncores":                    ml.HostStatus.StatusProperties.StatusDetail.Cores.Value,
		"total_cpu_stat_user":       ml.HostStatus.StatusProperties.StatusDetail.TotalCPUStatUser,
		"total_cpu_stat_system":     ml.HostStatus.StatusProperties.StatusDetail.TotalCPUStatSystem,
		"total_cpu_stat_idle":       ml.HostStatus.StatusProperties.StatusDetail.TotalCPUStatIdle,
		"total_cpu_stat_iowait":     ml.HostStatus.StatusProperties.StatusDetail.TotalCPUStatIowait,
		"memory_process_size":       ml.HostStatus.StatusProperties.StatusDetail.MemoryProcessSize.Value,
		"memory_process_rss":        ml.HostStatus.StatusProperties.StatusDetail.MemoryProcessRss.Value,
		"memory_system_total":       ml.HostStatus.StatusProperties.StatusDetail.MemorySystemTotal.Value,
		"memory_system_free":        ml.HostStatus.StatusProperties.StatusDetail.MemorySystemFree.Value,
		"memory_process_swap_size":  ml.HostStatus.StatusProperties.StatusDetail.MemoryProcessSwapSize.Value,
		"memory_size":               ml.HostStatus.StatusProperties.StatusDetail.MemorySize.Value,
		"host_size":                 ml.HostStatus.StatusProperties.StatusDetail.HostSize.Value,
		"log_device_space":          ml.HostStatus.StatusProperties.StatusDetail.LogDeviceSpace.Value,
		"data_dir_space":            ml.HostStatus.StatusProperties.StatusDetail.DataDirSpace.Value,
		"query_read_bytes":          ml.HostStatus.StatusProperties.StatusDetail.QueryReadBytes.Value,
		"query_read_load":           ml.HostStatus.StatusProperties.StatusDetail.QueryReadLoad.Value,
		"merge_read_load":           ml.HostStatus.StatusProperties.StatusDetail.MergeReadLoad.Value,
		"merge_write_load":          ml.HostStatus.StatusProperties.StatusDetail.MergeWriteLoad.Value,
		"http_server_receive_bytes": ml.HostStatus.StatusProperties.StatusDetail.HTTPServerReceiveBytes.Value,
		"http_server_send_bytes":    ml.HostStatus.StatusProperties.StatusDetail.HTTPServerSendBytes.Value,
	}

	// Accumulate the tags and values
	acc.AddFields("marklogic", fields, tags)

	return nil
}

func (c *Marklogic) createHTTPClient() (*http.Client, error) {
	tlsCfg, err := c.ClientConfig.TLSConfig()
	if err != nil {
		return nil, err
	}

	client := &http.Client{
		Transport: &http.Transport{
			TLSClientConfig: tlsCfg,
		},
		Timeout: time.Duration(5 * time.Second),
	}

	return client, nil
}

func (c *Marklogic) gatherJSONData(url string, v interface{}) error {
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return err
	}

	if c.Username != "" || c.Password != "" {
		req.SetBasicAuth(c.Username, c.Password)
	}

	response, err := c.client.Do(req)
	if err != nil {
		return err
	}
	defer response.Body.Close()
	if response.StatusCode != http.StatusOK {
		return fmt.Errorf("marklogic: API responded with status-code %d, expected %d",
			response.StatusCode, http.StatusOK)
	}

	if err = json.NewDecoder(response.Body).Decode(v); err != nil {
		return err
	}

	return nil
}

func init() {
	inputs.Add("marklogic", func() telegraf.Input {
		return &Marklogic{}
	})
}
