package cluster

import (
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"strconv"
	"time"

	"github.com/pingcap/errors"
	"k8s.io/client-go/kubernetes"
	"sigs.k8s.io/controller-runtime/pkg/log"
)

// SuccessStatus in the response.
const SuccessStatus = "success"

// QueryResponse of the Query.
type QueryResponse struct {
	Status string `json:"status"`
	Data   Data   `json:"data"`
}

// Data part from the response of prometheus.
type Data struct {
	ResultType string       `json:"resultType"`
	Result     []ResultItem `json:"result"`
	Error      string       `json:"error"`
}

// ResultItem in the response of prometheus.
type ResultItem struct {
	Metric map[string]string `json:"metric"`
	Value  []interface{}     `json:"value"`
}

// Querier know how to query metrics from prometheus.
type Querier interface {
	Query(ctx context.Context, path string, values map[string]string) (body []byte, err error)
}

type kubeProxyQueryer struct {
	cli  kubernetes.Interface
	ns   string
	name string
	port string
}

// nolint:deadcode // may be used in the future
// current we assume controller is deployed at the same k8s with tidb.
func newKubeProxyQueyer(cli kubernetes.Interface, ns, name, port string) *kubeProxyQueryer {
	return &kubeProxyQueryer{
		cli:  cli,
		ns:   ns,
		name: name,
		port: port,
	}
}

var _ Querier = &kubeProxyQueryer{}

// Query implements Querier interface.
func (q *kubeProxyQueryer) Query(ctx context.Context, path string, values map[string]string) (body []byte, err error) {
	resp := q.cli.CoreV1().Services(q.ns).ProxyGet("http", q.name, q.port, path, nil)
	data, err := resp.DoRaw(ctx)
	if err != nil {
		return nil, err
	}

	log := log.FromContext(ctx)
	log.V(3).Info("query prometheus while proxy", "url", "response", string(data))

	return data, nil
}

type httpClientQueyer struct {
	client *http.Client
	url    string
}

// url: base URL of form http://ipaddr:port with no trailing slash
func newHTTPClientQueyer(client *http.Client, url string) *httpClientQueyer {
	return &httpClientQueyer{
		url:    url,
		client: client,
	}
}

var _ Querier = &httpClientQueyer{}

// Query implements Querier interface.
func (q *httpClientQueyer) Query(ctx context.Context, path string, values map[string]string) (body []byte, err error) {
	v := url.Values{}
	for key, value := range values {
		v.Set(key, value)
	}

	urlStr := fmt.Sprintf("%s%s?%s", q.url, path, v.Encode())
	req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
	if err != nil {
		return nil, errors.AddStack(err)
	}

	resp, err := q.client.Do(req)
	if err != nil {
		return nil, errors.AddStack(err)
	}

	log := log.FromContext(ctx)
	defer func() {
		err := resp.Body.Close()
		if err != nil {
			log.Error(err, "failed to close body")
		}
	}()
	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, errors.Annotate(err, "failed to read response body")
	}

	log.V(3).Info("query prometheus", "url", urlStr, "response", string(data))

	return data, nil
}

// Active detect if the cluster is active in the specified time range.
// example query:
// http://127.0.0.1:9090/api/v1/query?query=changes(tidb_executor_statement_total[40m])%3E0&time=&_=1628499112154]
// ref https://prometheus.io/docs/prometheus/latest/querying/api/
func Active(ctx context.Context, querier Querier, start time.Time, end time.Time) (bool, error) {
	base10 := 10
	duration := strconv.FormatInt(int64(end.Sub(start).Seconds()), base10) + "s"

	v := make(map[string]string)
	v["query"] = fmt.Sprintf("changes(tidb_executor_statement_total[%s])>0", duration)
	v["time"] = end.Format(time.RFC3339)
	// a dummy value just avoid cache
	v["_"] = strconv.FormatInt(time.Now().Unix(), base10)

	body, err := querier.Query(ctx, "/api/v1/query", v)
	if err != nil {
		return false, err
	}

	queryResponse := &QueryResponse{}
	err = json.Unmarshal(body, queryResponse)
	if err != nil {
		return false, errors.Annotatef(err, "failed to unmarshal json %s", string(body))
	}

	if queryResponse.Status != SuccessStatus {
		return false, errors.Errorf("query failed: %s", queryResponse.Data.Error)
	}

	if len(queryResponse.Data.Result) > 0 {
		return true, nil
	}

	return false, nil
}
