package datasource

import (
	"fmt"
	"net"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"go.uber.org/zap"

	apierr "github.com/pingcap-inc/clinic/pkg/api/error"
	"github.com/pingcap-inc/clinic/pkg/config"
	"github.com/pingcap-inc/clinic/pkg/log"
	"github.com/pingcap-inc/clinic/pkg/proxy"
)

const (
	vmSelectPrefix = "/select/%s/prometheus"
	queryRangePath = "/api/v1/query_range"

	headerOrgType   = "X-OrgType"
	headerOrgID     = "X-OrgID"
	headerClusterID = "X-ClusterID"
)

type ComputeEngineDataAPI struct {
	// metricsQueryRangeProxy forward get metrics request to vm select service or thanos service's query range api.
	metricsQueryRangeProxy *metricsQueryRangeProxy
	// metricsProxy forward request to vm select service (select api) or thanos service (query api).
	metricsProxy *metricsProxy
	// logProxy forward get log request to open-search.
	logProxy *logProxy

	metricHandler *proxy.TiDBClusterMetricHandler

	o11yProxyHandler *proxy.O11yProxyHandler
}

func customizedTransport() http.RoundTripper {
	defaultTransPtr, ok := http.DefaultTransport.(*http.Transport)
	if !ok {
		return http.DefaultTransport
	}
	return &http.Transport{
		Proxy: http.ProxyFromEnvironment,
		DialContext: (&net.Dialer{
			Timeout:   3 * time.Second,
			KeepAlive: 30 * time.Second,
		}).DialContext,
		MaxIdleConns: 100,
		// increase conn count for better concurrency
		MaxIdleConnsPerHost:   10,
		ResponseHeaderTimeout: 30 * time.Second,
		IdleConnTimeout:       defaultTransPtr.IdleConnTimeout,
		TLSHandshakeTimeout:   defaultTransPtr.TLSHandshakeTimeout,
		ExpectContinueTimeout: defaultTransPtr.ExpectContinueTimeout,
	}
}

// NewComputeEngineDataAPI is a reserved proxy builder
func NewComputeEngineDataAPI(metricConfig *config.MetricConfig, logConfig *config.DatasourceLogConfig,
	metricHandler *proxy.TiDBClusterMetricHandler, o11yProxyHandler *proxy.O11yProxyHandler,
) (*ComputeEngineDataAPI, error) {
	api := &ComputeEngineDataAPI{
		metricsQueryRangeProxy: &metricsQueryRangeProxy{},
		metricsProxy:           &metricsProxy{},
		logProxy:               &logProxy{},
		metricHandler:          metricHandler,
		o11yProxyHandler:       o11yProxyHandler,
	}
	// set metricsQueryRangeProxy
	{
		if metricConfig.SelectAddr.OpAddress.Host != "" {
			opProxy, err := buildMetricsQueryRangeProxy(&metricConfig.SelectAddr.OpAddress)
			if err != nil {
				return nil, err
			}
			api.metricsQueryRangeProxy.opProxy = opProxy
		}
		if metricConfig.SelectAddr.CloudAddress.Host != "" {
			cloudProxy, err := buildMetricsQueryRangeProxy(&metricConfig.SelectAddr.CloudAddress)
			if err != nil {
				return nil, err
			}
			api.metricsQueryRangeProxy.cloudProxy = cloudProxy
		}
	}
	// set metricsProxy
	{
		if metricConfig.SelectAddr.OpAddress.Host != "" {
			opProxy, err := buildMetricsProxy(&metricConfig.SelectAddr.OpAddress)
			if err != nil {
				return nil, err
			}
			api.metricsProxy.opProxy = opProxy
		}
		if metricConfig.SelectAddr.CloudAddress.Host != "" {
			cloudProxy, err := buildMetricsProxy(&metricConfig.SelectAddr.CloudAddress)
			if err != nil {
				return nil, err
			}
			api.metricsProxy.cloudProxy = cloudProxy
		}
	}
	// set logProxy
	{
		if logConfig.OpAddress.Endpoint != "" {
			rp, err := buildLogProxy(&logConfig.OpAddress)
			if err != nil {
				return nil, err
			}
			api.logProxy.opProxy = rp
		}

		if logConfig.CloudAddress.Endpoint != "" {
			rp, err := buildLogProxy(&logConfig.CloudAddress)
			if err != nil {
				return nil, err
			}
			api.logProxy.cloudProxy = rp
		}
	}
	return api, nil
}

// @Summary		Get the metrics data
// @Tags			Metric
// @Description	get the metrics data
// @Accept			json
// @Param			X-OrgType	header		string	true	"string enums"	Enums(cloud,op)
// @Param			X-OrgID		header		string	true	"the org id in clinic"
// @Param			X-ClusterID	header		string	true	"cluster id"
// @Param			query		query		string	true	"prometheus query"
// @Param			start		query		int		false	"the start timestamp of query data"
// @Param			end			query		int		false	"the end timestamp of query data"
// @Param			step		query		string	false	"prometheus step"
// @Success		200			{object}	string
// @Router			/data/metrics [get]
func (api *ComputeEngineDataAPI) ForwardMetricsQueryRangeRequest(c *gin.Context) {
	log.Debug(c.Request.Context(), "metrics request arrived", zap.String("path", c.Request.URL.Path))

	api.metricHandler.GinHandle(c,
		func(c *gin.Context) string {
			return c.Request.Header.Get(headerOrgType)
		}, func(c *gin.Context) uint {
			oidStr := c.Request.Header.Get(headerOrgID)
			oid, _ := strconv.ParseUint(oidStr, 10, 64)
			return uint(oid)
		}, func(c *gin.Context) string {
			return c.Request.Header.Get(headerClusterID)
		}, func(c *gin.Context) string {
			return ""
		}, func(s string) string {
			return queryRangePath
		},
	)
}

// @Summary		Get the dbaas control plane metrics data
// @Tags			Metric
// @Description	get the dbaas control plane metrics data
// @Accept			json
// @Param			control_plane_info	header		string	true	"the control plane info"
// @Param			query				query		string	true	"prometheus query"
// @Param			start				query		int		false	"the start timestamp of query data"
// @Param			end					query		int		false	"the end timestamp of query data"
// @Param			step				query		string	false	"prometheus step"
// @Success		200					{object}	string
// @Router			/data/control_plane/metrics [get]
func (api *ComputeEngineDataAPI) ForwardControlPlaneMetricsQueryRangeRequest(c *gin.Context) {
	log.Info(c.Request.Context(), "control plane metrics request arrived",
		zap.String("operation", "proxy_control_plane_metrics"),
		zap.String("path", c.Request.URL.Path))
	api.o11yProxyHandler.ProxyControlPlaneMetrics(c,
		func(s string) string {
			return queryRangePath
		},
	)
}

func (api *ComputeEngineDataAPI) ForwardMetricsRequest(c *gin.Context) {
	log.Debug(c.Request.Context(), "metrics request arrived", zap.String("path", c.Request.URL.Path))
	orgType := c.Request.Header.Get(headerOrgType)
	if orgType == "cloud" {
		if api.metricsProxy.cloudProxy == nil {
			_ = c.Error(apierr.ErrBadRequest.New("cloud address is not config"))
			return
		}
		api.metricsProxy.cloudProxy.ServeHTTP(c.Writer, c.Request)
		return
	}
	if api.metricsProxy.opProxy == nil {
		_ = c.Error(apierr.ErrBadRequest.New("op address is not config"))
		return
	}
	api.metricsProxy.opProxy.ServeHTTP(c.Writer, c.Request)
}

func (api *ComputeEngineDataAPI) ForwardLogRequest(c *gin.Context) {
	log.Debug(c.Request.Context(), "log request arrived", zap.String("path", c.Request.URL.Path))
	orgType := c.Request.Header.Get(headerOrgType)
	if orgType == "cloud" {
		if api.logProxy.cloudProxy == nil {
			_ = c.Error(apierr.ErrBadRequest.New("cloud address is not config"))
			return
		}
		api.logProxy.cloudProxy.ServeHTTP(c.Writer, c.Request)
		return
	}
	if api.logProxy.opProxy == nil {
		_ = c.Error(apierr.ErrBadRequest.New("op address is not config"))
		return
	}
	api.logProxy.opProxy.ServeHTTP(c.Writer, c.Request)
}

func buildMetricsQueryRangeProxy(cfg *config.VMClusterAddressConfig) (*httputil.ReverseProxy, error) {
	rp := &httputil.ReverseProxy{Transport: customizedTransport()}
	switch {
	case cfg.InVMCluster():
		// forward with vm-select style
		director := func(req *http.Request) {
			orgID := req.Header.Get(headerOrgID)
			req.URL.Scheme = "http"
			req.URL.Host = cfg.Authority()
			req.URL.Path = fmt.Sprintf("%s%s", fmt.Sprintf(vmSelectPrefix, orgID), queryRangePath)
		}
		rp.Director = director
	default:
		// forward with native prometheus style
		director := func(req *http.Request) {
			req.URL.Scheme = "http"
			req.URL.Host = cfg.Authority()
			req.URL.Path = queryRangePath
		}
		rp.Director = director
	}
	return rp, nil
}

func buildMetricsProxy(cfg *config.VMClusterAddressConfig) (*reverseProxyWrapper, error) {
	rp := &httputil.ReverseProxy{Transport: customizedTransport()}
	switch {
	case cfg.InVMCluster():
		// forward with vm-select style
		director := func(req *http.Request) {
			orgID := req.Header.Get(headerOrgID)
			req.URL.Scheme = "http"
			req.URL.Host = cfg.Authority()
			req.URL.Path = fmt.Sprintf("%s%s", fmt.Sprintf(vmSelectPrefix, orgID), req.URL.Path)
			req.Host = cfg.Authority()
			log.Debug(req.Context(), "forward metrics request", zap.String("host", req.URL.Host), zap.String("path", req.URL.Path))
		}
		rp.Director = director
	default:
		// forward with native prometheus style
		director := func(req *http.Request) {
			req.URL.Scheme = "http"
			req.URL.Host = cfg.Authority()
			req.Host = cfg.Authority()
			log.Debug(req.Context(), "forward metrics request", zap.String("host", req.URL.Host), zap.String("path", req.URL.Path))
		}
		rp.Director = director
	}
	return &reverseProxyWrapper{ReverseProxy: rp}, nil
}

func buildLogProxy(cfg *config.LogConfig) (*reverseProxyWrapper, error) {
	u, err := url.Parse(cfg.Endpoint)
	if err != nil {
		return nil, err
	}
	rp := &httputil.ReverseProxy{
		Transport: customizedTransport(),
		Director: func(req *http.Request) {
			targetPath := strings.TrimPrefix(req.URL.Path, "/data/log")
			req.URL.Scheme = u.Scheme
			req.URL.Host = u.Host
			req.URL.Path = targetPath
			// add basic auth
			if cfg.User != "" && cfg.Password != "" {
				req.SetBasicAuth(cfg.User, cfg.Password)
			}
			// deal with compatibility header
			// Treat 'application/vnd.elasticsearch+json' as application/json for compatibility
			if strings.HasPrefix(req.Header.Get("Content-Type"), "application/vnd.elasticsearch+json") {
				req.Header.Set("Content-Type", "application/json")
			}
		},
		ModifyResponse: func(response *http.Response) error {
			// https://github.com/elastic/elasticsearch-py/blob/main/elasticsearch/_sync/client/_base.py#L328
			response.Header.Set("x-elastic-product", "Elasticsearch")
			return nil
		},
	}
	return &reverseProxyWrapper{ReverseProxy: rp}, nil
}

// reverseProxyWrapper wrap native ReverseProxy for customization
type reverseProxyWrapper struct {
	*httputil.ReverseProxy
}

func (wrapper *reverseProxyWrapper) WithPathPrefixTrim(pathPrefix string) *reverseProxyWrapper {
	originDirector := wrapper.ReverseProxy.Director
	wrapper.ReverseProxy.Director = func(req *http.Request) {
		req.URL.Path = strings.TrimPrefix(req.URL.Path, pathPrefix)
		req.URL.RawPath = strings.TrimPrefix(req.URL.RawPath, pathPrefix)
		originDirector(req)
	}
	return wrapper
}

type metricsQueryRangeProxy struct {
	opProxy    *httputil.ReverseProxy
	cloudProxy *httputil.ReverseProxy
}

type metricsProxy struct {
	opProxy    *reverseProxyWrapper
	cloudProxy *reverseProxyWrapper
}

func (proxy *metricsProxy) WithPathPrefixTrim(basePath string) *metricsProxy {
	if proxy.opProxy != nil {
		proxy.opProxy.WithPathPrefixTrim(basePath)
	}
	if proxy.cloudProxy != nil {
		proxy.cloudProxy.WithPathPrefixTrim(basePath)
	}
	return proxy
}

type logProxy struct {
	opProxy    *reverseProxyWrapper
	cloudProxy *reverseProxyWrapper
}

func (proxy *logProxy) WithPathPrefixTrim(basePath string) *logProxy {
	if proxy.opProxy != nil {
		proxy.opProxy.WithPathPrefixTrim(basePath)
	}
	if proxy.cloudProxy != nil {
		proxy.cloudProxy.WithPathPrefixTrim(basePath)
	}
	return proxy
}
