// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. 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 estransport

import (
	"errors"
	"fmt"
	"strconv"
	"strings"
	"sync"
	"time"
)

// Measurable defines the interface for transports supporting metrics.
//
type Measurable interface {
	Metrics() (Metrics, error)
}

// connectionable defines the interface for transports returning a list of connections.
//
type connectionable interface {
	connections() []*Connection
}

// Metrics represents the transport metrics.
//
type Metrics struct {
	Requests  int         `json:"requests"`
	Failures  int         `json:"failures"`
	Responses map[int]int `json:"responses"`

	Connections []fmt.Stringer `json:"connections"`
}

// ConnectionMetric represents metric information for a connection.
//
type ConnectionMetric struct {
	URL       string     `json:"url"`
	Failures  int        `json:"failures,omitempty"`
	IsDead    bool       `json:"dead,omitempty"`
	DeadSince *time.Time `json:"dead_since,omitempty"`

	Meta struct {
		ID    string   `json:"id"`
		Name  string   `json:"name"`
		Roles []string `json:"roles"`
	} `json:"meta"`
}

// metrics represents the inner state of metrics.
//
type metrics struct {
	sync.RWMutex

	requests  int
	failures  int
	responses map[int]int

	connections []*Connection
}

// Metrics returns the transport metrics.
//
func (c *Client) Metrics() (Metrics, error) {
	if c.metrics == nil {
		return Metrics{}, errors.New("transport metrics not enabled")
	}
	c.metrics.RLock()
	defer c.metrics.RUnlock()

	if lockable, ok := c.pool.(sync.Locker); ok {
		lockable.Lock()
		defer lockable.Unlock()
	}

	m := Metrics{
		Requests:  c.metrics.requests,
		Failures:  c.metrics.failures,
		Responses: make(map[int]int, len(c.metrics.responses)),
	}

	for code, num := range c.metrics.responses {
		m.Responses[code] = num
	}

	if pool, ok := c.pool.(connectionable); ok {
		for _, c := range pool.connections() {
			c.Lock()

			cm := ConnectionMetric{
				URL:      c.URL.String(),
				IsDead:   c.IsDead,
				Failures: c.Failures,
			}

			if !c.DeadSince.IsZero() {
				cm.DeadSince = &c.DeadSince
			}

			if c.ID != "" {
				cm.Meta.ID = c.ID
			}

			if c.Name != "" {
				cm.Meta.Name = c.Name
			}

			if len(c.Roles) > 0 {
				cm.Meta.Roles = c.Roles
			}

			m.Connections = append(m.Connections, cm)
			c.Unlock()
		}
	}

	return m, nil
}

// String returns the metrics as a string.
//
func (m Metrics) String() string {
	var (
		i int
		b strings.Builder
	)
	b.WriteString("{")

	b.WriteString("Requests:")
	b.WriteString(strconv.Itoa(m.Requests))

	b.WriteString(" Failures:")
	b.WriteString(strconv.Itoa(m.Failures))

	if len(m.Responses) > 0 {
		b.WriteString(" Responses: ")
		b.WriteString("[")

		for code, num := range m.Responses {
			b.WriteString(strconv.Itoa(code))
			b.WriteString(":")
			b.WriteString(strconv.Itoa(num))
			if i+1 < len(m.Responses) {
				b.WriteString(", ")
			}
			i++
		}
		b.WriteString("]")
	}

	b.WriteString(" Connections: [")
	for i, c := range m.Connections {
		b.WriteString(c.String())
		if i+1 < len(m.Connections) {
			b.WriteString(", ")
		}
		i++
	}
	b.WriteString("]")

	b.WriteString("}")
	return b.String()
}

// String returns the connection information as a string.
//
func (cm ConnectionMetric) String() string {
	var b strings.Builder
	b.WriteString("{")
	b.WriteString(cm.URL)
	if cm.IsDead {
		fmt.Fprintf(&b, " dead=%v", cm.IsDead)
	}
	if cm.Failures > 0 {
		fmt.Fprintf(&b, " failures=%d", cm.Failures)
	}
	if cm.DeadSince != nil {
		fmt.Fprintf(&b, " dead_since=%s", cm.DeadSince.Local().Format(time.Stamp))
	}
	b.WriteString("}")
	return b.String()
}
