package metrics

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

	"github.com/onsi/gomega/format"
	"github.com/pingcap/log"
	"github.com/prometheus/client_golang/api"
	v1 "github.com/prometheus/client_golang/api/prometheus/v1"
	"github.com/prometheus/common/model"
	"go.uber.org/zap"

	m2 "github.com/pingcap/endless/pkg/matcher"
)

func NewPrometheusClient(prometheusURL url.URL) (v1.API, error) {
	client, err := api.NewClient(api.Config{
		Address: prometheusURL.String(),
	})
	if err != nil {
		return nil, err
	}

	return v1.NewAPI(client), nil
}

type PromQLSubject struct {
	Ctx         context.Context
	Description string
	Query       string
	Api         v1.API

	toleration *m2.Toleration
}

func (p *PromQLSubject) AtLeastFailedFor(duration time.Duration) {
	p.toleration = &m2.Toleration{Duration: duration}
}

// PromQLExpr creates a PromQLSubject that you could assert on.
// currently, only PromQLEvaluatedToEmpty matcher is supported.
type PromQLExpr = PromQLSubject

type promQLEvaluatedToEmpty struct {
	evaluated bool

	instance         PromQLSubject
	firstFailureTime *time.Time
	results          []promQLResult
}

type promQLResult struct {
	isEmpty   bool
	tolerated bool
	value     model.Value
	time      time.Time
}

// PromQLEvaluatedToEmpty evaluates a PromQLExpr query against a cluster, and checks if it is:
//  - a Vector, and not empty
//  - a Scalar, and not empty
// if neither satisfied, the checker will fail. related checking logic is in promQLEvaluatedToEmpty::promQLEvaluatedToEmpty.
func PromQLEvaluatedToEmpty() *promQLEvaluatedToEmpty {
	return &promQLEvaluatedToEmpty{
		evaluated:        false,
		firstFailureTime: nil,
	}
}

func (matcher *promQLEvaluatedToEmpty) Match(actual interface{}) (bool, error) {
	ql, ok := actual.(PromQLSubject)
	if !ok {
		return false, fmt.Errorf("PromQLEvaluatedToEmpty must be passed a util.PromQLSubject. Got:\n%s", format.Object(actual, 1))
	}

	matcher.instance = ql

	if ql.Ctx != nil {
		select {
		case <-ql.Ctx.Done():
			return true, nil
		default:
		}
	}

	now := time.Now()
	isEmpty, value, err := matcher.promQLEvaluatedToEmpty(ql.Api, ql.Query, now)
	if err != nil {
		return false, err
	} else {
		matcher.evaluated = true
	}

	matched := isEmpty
	tolerated := matched
	if !isEmpty && matcher.firstFailureTime == nil {
		matcher.firstFailureTime = &now
	}

	if ql.toleration != nil {
		tolerated = ql.toleration.Tolerate(matched)
	}

	if !isEmpty {
		// Log the request and the response so that a user can replay it later.
		msg := "Query returned nonempty result"
		if tolerated {
			msg += ", but tolerated"
		}
		log.Debug(msg,
			zap.String("query", ql.Query),
			zap.Time("ts", now),
			zap.Any("value", value))
	}

	if !tolerated {
		// This is not matched. Log an entry to record the current time.
		logMsg := "PromQLEvaluatedToEmpty reports not-matched result"
		if ql.toleration != nil {
			logMsg += " after had been failed consecutively for " + ql.toleration.Duration.String()
		}
		log.Debug(logMsg,
			zap.Time("firstNonemptyTime", *matcher.firstFailureTime))
	}

	// this is not a code duplicate, Ctx needs to be checked twice.
	if matcher.instance.Ctx != nil {
		select {
		case <-matcher.instance.Ctx.Done():
			return true, nil
		default:
		}
	}

	matcher.results = append(matcher.results,
		promQLResult{
			isEmpty:   isEmpty,
			tolerated: tolerated,
			value:     value,
			time:      now,
		})

	return tolerated, err
}

// promQLEvaluatedToEmpty was copied from github.com/PingCAP-QE/metrics-checker with val returned
// promQLEvaluatedToEmpty checks if a query returns a nonempty Vector or Scalar, called by PromQLEvaluatedToEmpty.
func (matcher *promQLEvaluatedToEmpty) promQLEvaluatedToEmpty(client v1.API, query string, ts time.Time) (result bool, value model.Value, err error) {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	val, _, err := client.Query(ctx, query, ts)
	if err != nil {
		return false, nil, err
	}
	// Ref: https://github.com/prometheus/prometheus/blob/76750d2a96df54226e85ac272d7ad5a547630240/rules/manager.go#L186-L206
	switch v := val.(type) {
	case model.Vector:
		return v.Len() <= 0, val, nil // if empty Vector, returns True.
	case *model.Scalar:
		return v == nil, val, nil // if null Scalar, returns True.
	default:
		return false, val, errors.New("rule result is not a vector or scalar")
	}
}

// FailureMessage checkout comments of DiagnoseInfo for an example of failure message.
func (matcher *promQLEvaluatedToEmpty) FailureMessage(_ interface{}) (message string) {
	return matcher.getFailureMessage(false)
}

// NegatedFailureMessage checkout comments of DiagnoseInfo for an example of failure message.
func (matcher *promQLEvaluatedToEmpty) NegatedFailureMessage(_ interface{}) (message string) {
	return matcher.getFailureMessage(true)
}

func (matcher *promQLEvaluatedToEmpty) getFailureMessage(isNegative bool) (message string) {
	if !matcher.evaluated {
		return "PromQLSubject has not evaluated yet."
	} else {
		var errmsg string
		if isNegative {
			errmsg = format.IndentString("<nonnull Scalar or nonempty Vector>"+matcher.failedForMessage(), 1)
		} else {
			errmsg = format.IndentString("<nil Scalar or empty Vector>"+matcher.failedForMessage(), 1)
		}
		instance := matcher.instance
		return fmt.Sprintf("Expected\n%s\nto match the expected result of PromQLExpr [%s: %s]\n%s\n\n%s",
			format.Object(matcher.results[len(matcher.results)-1].value, 1),
			instance.Description,
			instance.Query,
			errmsg,
			matcher.DiagnoseInfo())
	}
}

// DiagnoseInfo the log of PromQLEvaluatedToEmpty now runs at
// Debug level rather than Info level, and we add a separate
// Diagnose Info section to make the test significantly more
// concise and easier to read:
//
// Expected
//    <*model.Scalar | 0x1f33e30>: {Value: 42.42, Timestamp: 0}
// to match the expected result of PromQLExpr [test query: XXX]
//    <nil Scalar or empty Vector>, but failed for at least 500ms
//
// ======== Diagnose Info ========
//  query: XXX (test query)
//  firstNonemptyTime: 2022-01-17 21:41:30.704347 +0800 CST m=+5.331367122
//  PromQL query result log:
//    0. [2022/01/17 21:41:30.704 +08:00] NON-EMPTY (tolerated): <scalar: 42.42 @[0]>
//    1. [2022/01/17 21:41:30.904 +08:00] EMPTY     (tolerated): <>
//    2. [2022/01/17 21:41:30.904 +08:00] NON-EMPTY (tolerated): <scalar: 42.42 @[0]>
//    3. [2022/01/17 21:41:31.304 +08:00] NON-EMPTY (tolerated): <scalar: 42.42 @[0]>
//    4. [2022/01/17 21:41:31.504 +08:00] NON-EMPTY: <scalar: 42.42 @[0]>
func (matcher *promQLEvaluatedToEmpty) DiagnoseInfo() string {
	instance := matcher.instance
	return fmt.Sprintf("======== Diagnose Info ========\n  query: %s (%s)\n  firstNonemptyTime: %s\n  PromQL query result log: \n%s",
		instance.Query,
		instance.Description,
		matcher.firstFailureTime,
		format.IndentString(matcher.promQLResultLog(), 1))
}

func (matcher *promQLEvaluatedToEmpty) promQLResultLog() string {
	str := ""
	for i, value := range matcher.results {
		result := "NON-EMPTY"
		if value.isEmpty {
			result = "EMPTY"
		}
		tolerate := ""
		if matcher.instance.toleration != nil && value.tolerated {
			tolerate = " (tolerated)"
		}
		str += fmt.Sprintf("%d. [%s] %-9s%s: <%s>\n", i, m2.FormatTime(value.time), result, tolerate, value.value)
	}
	return str
}

func (matcher *promQLEvaluatedToEmpty) failedForMessage() string {
	failedFor := ""
	if matcher.instance.toleration != nil && matcher.instance.toleration.Duration != 0 {
		failedFor = fmt.Sprintf(", but failed for at least %s", matcher.instance.toleration.Duration.String())
	}
	return failedFor
}

func (matcher *promQLEvaluatedToEmpty) MatchMayChangeInTheFuture(_ interface{}) bool {
	// Maybe we can implement a separate matcher to check if ctx is Done.
	if matcher.instance.Ctx != nil {
		select {
		case <-matcher.instance.Ctx.Done():
			return false
		default:
		}
	}
	return true
}
