package query

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

	"github.com/pingcap/errors"
	"github.com/tidbcloud/aws-shared-provider/pkg/metrics"
	"k8s.io/client-go/kubernetes"
	"sigs.k8s.io/controller-runtime/pkg/log"
)

const (
	// DefaultQueryTimeout is the default timeout for queryer
	DefaultQueryTimeout = 10 * time.Second
)

var (
	// DefaultQueryerClient is the default queryer client
	DefaultQueryerClient = &http.Client{Timeout: DefaultQueryTimeout}
)

// Querier conducts queries
type Querier interface {
	Query(ctx context.Context, path string, values map[string]string) ([]byte, error)
}

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

var _ Querier = &kubeProxyQueryer{}

// current we assume controller is deployed at the same k8s with tidb.
func NewKubeProxyQueryer(cli kubernetes.Interface, ns, name, port string) *kubeProxyQueryer {
	return &kubeProxyQueryer{
		cli:  cli,
		ns:   ns,
		name: name,
		port: port,
	}
}

func (q *kubeProxyQueryer) Query(ctx context.Context, path string, values map[string]string) ([]byte, 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 httpClientQueryer struct {
	client *http.Client
	url    string
}

var _ Querier = &httpClientQueryer{}

// url: base URL of form http://ipaddr:port with no trailing slash
func NewHTTPClientQueryer(client *http.Client, url string) *httpClientQueryer {
	if client == nil {
		client = DefaultQueryerClient
	}
	return &httpClientQueryer{
		url:    url,
		client: client,
	}
}

func (q *httpClientQueryer) Query(ctx context.Context, path string, values map[string]string) ([]byte, error) {
	v := url.Values{}
	for key, value := range values {
		v.Set(key, value)
	}

	start := time.Now()
	respCode, read := "unknown", "no read"
	defer func() {
		metrics.UtilsQueryHistogram.WithLabelValues(respCode, read).Observe(time.Since(start).Seconds())
	}()

	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)
	}

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

	read = "success"
	log.V(3).Info("query", "url", urlStr, "response", string(data))

	return data, nil
}

//nolint:lll //nolint
func (q *httpClientQueryer) QueryWithCode(ctx context.Context, path string, values map[string]string) (int, []byte, error) {
	v := url.Values{}
	for key, value := range values {
		v.Set(key, value)
	}

	start := time.Now()
	respCode, read := "unknown", "no read"
	defer func() {
		metrics.UtilsQueryHistogram.WithLabelValues(respCode, read).Observe(time.Since(start).Seconds())
	}()

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

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

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

	read = "success"
	log.V(3).Info("query", "url", urlStr, "response", string(data))

	return resp.StatusCode, data, nil
}
