package prometheus

import (
	"context"
	"errors"
	"fmt"
	"net/url"
	"regexp"
	"strings"
	"time"

	"github.com/opentracing/opentracing-go"

	"net/http"

	"github.com/grafana/grafana-plugin-sdk-go/data"
	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/models"
	"github.com/grafana/grafana/pkg/plugins"
	"github.com/grafana/grafana/pkg/tsdb/interval"
	"github.com/prometheus/client_golang/api"
	apiv1 "github.com/prometheus/client_golang/api/prometheus/v1"
	"github.com/prometheus/common/model"
)

type PrometheusExecutor struct {
	baseRoundTripperFactory func(dsInfo *models.DataSource) (http.RoundTripper, error)
	intervalCalculator      interval.Calculator
}

type prometheusTransport struct {
	Transport http.RoundTripper

	hasBasicAuth bool
	username     string
	password     string

	customQueryParameters string
}

func (transport *prometheusTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	if transport.hasBasicAuth {
		req.SetBasicAuth(transport.username, transport.password)
	}

	if transport.customQueryParameters != "" {
		params := url.Values{}
		for _, param := range strings.Split(transport.customQueryParameters, "&") {
			parts := strings.Split(param, "=")
			if len(parts) == 1 {
				// This is probably a mistake on the users part in defining the params but we don't want to crash.
				params.Add(parts[0], "")
			} else {
				params.Add(parts[0], parts[1])
			}
		}
		if req.URL.RawQuery != "" {
			req.URL.RawQuery = fmt.Sprintf("%s&%s", req.URL.RawQuery, params.Encode())
		} else {
			req.URL.RawQuery = params.Encode()
		}
	}

	return transport.Transport.RoundTrip(req)
}

//nolint: staticcheck // plugins.DataPlugin deprecated
func NewExecutor(dsInfo *models.DataSource) (plugins.DataPlugin, error) {
	return &PrometheusExecutor{
		intervalCalculator: interval.NewCalculator(interval.CalculatorOptions{MinInterval: time.Second * 1}),
		baseRoundTripperFactory: func(ds *models.DataSource) (http.RoundTripper, error) {
			return ds.GetHttpTransport()
		},
	}, nil
}

var (
	plog         log.Logger
	legendFormat *regexp.Regexp = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`)
)

func init() {
	plog = log.New("tsdb.prometheus")
}

func (e *PrometheusExecutor) getClient(dsInfo *models.DataSource) (apiv1.API, error) {
	// Would make sense to cache this but executor is recreated on every alert request anyway.
	transport, err := e.baseRoundTripperFactory(dsInfo)
	if err != nil {
		return nil, err
	}

	promTransport := &prometheusTransport{
		Transport:             transport,
		hasBasicAuth:          dsInfo.BasicAuth,
		username:              dsInfo.BasicAuthUser,
		password:              dsInfo.DecryptedBasicAuthPassword(),
		customQueryParameters: dsInfo.JsonData.Get("customQueryParameters").MustString(""),
	}

	cfg := api.Config{
		Address:      dsInfo.Url,
		RoundTripper: promTransport,
	}

	client, err := api.NewClient(cfg)
	if err != nil {
		return nil, err
	}

	return apiv1.NewAPI(client), nil
}

//nolint: staticcheck // plugins.DataResponse deprecated
func (e *PrometheusExecutor) DataQuery(ctx context.Context, dsInfo *models.DataSource,
	tsdbQuery plugins.DataQuery) (plugins.DataResponse, error) {
	result := plugins.DataResponse{
		Results: map[string]plugins.DataQueryResult{},
	}

	client, err := e.getClient(dsInfo)
	if err != nil {
		return result, err
	}

	queries, err := e.parseQuery(dsInfo, tsdbQuery)
	if err != nil {
		return result, err
	}

	for _, query := range queries {
		timeRange := apiv1.Range{
			Start: query.Start,
			End:   query.End,
			Step:  query.Step,
		}

		plog.Debug("Sending query", "start", timeRange.Start, "end", timeRange.End, "step", timeRange.Step, "query", query.Expr)

		span, ctx := opentracing.StartSpanFromContext(ctx, "alerting.prometheus")
		span.SetTag("expr", query.Expr)
		span.SetTag("start_unixnano", query.Start.UnixNano())
		span.SetTag("stop_unixnano", query.End.UnixNano())
		defer span.Finish()

		value, _, err := client.QueryRange(ctx, query.Expr, timeRange)

		if err != nil {
			return result, err
		}

		queryResult, err := parseResponse(value, query)
		if err != nil {
			return result, err
		}
		result.Results[query.RefId] = queryResult
	}

	return result, nil
}

func formatLegend(metric model.Metric, query *PrometheusQuery) string {
	if query.LegendFormat == "" {
		return metric.String()
	}

	result := legendFormat.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte {
		labelName := strings.Replace(string(in), "{{", "", 1)
		labelName = strings.Replace(labelName, "}}", "", 1)
		labelName = strings.TrimSpace(labelName)
		if val, exists := metric[model.LabelName(labelName)]; exists {
			return []byte(val)
		}
		return []byte{}
	})

	return string(result)
}

func (e *PrometheusExecutor) parseQuery(dsInfo *models.DataSource, query plugins.DataQuery) (
	[]*PrometheusQuery, error) {
	qs := []*PrometheusQuery{}
	for _, queryModel := range query.Queries {
		expr, err := queryModel.Model.Get("expr").String()
		if err != nil {
			return nil, err
		}

		format := queryModel.Model.Get("legendFormat").MustString("")

		start, err := query.TimeRange.ParseFrom()
		if err != nil {
			return nil, err
		}

		end, err := query.TimeRange.ParseTo()
		if err != nil {
			return nil, err
		}

		dsInterval, err := interval.GetIntervalFrom(dsInfo, queryModel.Model, time.Second*15)
		if err != nil {
			return nil, err
		}

		intervalFactor := queryModel.Model.Get("intervalFactor").MustInt64(1)
		interval := e.intervalCalculator.Calculate(*query.TimeRange, dsInterval)
		step := time.Duration(int64(interval.Value) * intervalFactor)

		qs = append(qs, &PrometheusQuery{
			Expr:         expr,
			Step:         step,
			LegendFormat: format,
			Start:        start,
			End:          end,
			RefId:        queryModel.RefID,
		})
	}

	return qs, nil
}

//nolint: staticcheck // plugins.DataQueryResult deprecated
func parseResponse(value model.Value, query *PrometheusQuery) (plugins.DataQueryResult, error) {
	var queryRes plugins.DataQueryResult
	frames := data.Frames{}

	matrix, ok := value.(model.Matrix)
	if !ok {
		return queryRes, fmt.Errorf("unsupported result format: %q", value.Type().String())
	}

	for _, v := range matrix {
		name := formatLegend(v.Metric, query)
		tags := make(map[string]string, len(v.Metric))
		timeVector := make([]time.Time, 0, len(v.Values))
		values := make([]float64, 0, len(v.Values))

		for k, v := range v.Metric {
			tags[string(k)] = string(v)
		}

		for _, k := range v.Values {
			timeVector = append(timeVector, time.Unix(k.Timestamp.Unix(), 0).UTC())
			values = append(values, float64(k.Value))
		}
		frames = append(frames, data.NewFrame(name,
			data.NewField("time", nil, timeVector),
			data.NewField("value", tags, values).SetConfig(&data.FieldConfig{DisplayNameFromDS: name})))
	}
	queryRes.Dataframes = plugins.NewDecodedDataFrames(frames)

	return queryRes, nil
}

// IsAPIError returns whether err is or wraps a Prometheus error.
func IsAPIError(err error) bool {
	// Check if the right error type is in err's chain.
	var e *apiv1.Error
	return errors.As(err, &e)
}

func ConvertAPIError(err error) error {
	var e *apiv1.Error
	if errors.As(err, &e) {
		return fmt.Errorf("%s: %s", e.Msg, e.Detail)
	}
	return err
}
