package prom

import (
	"context"
	"fmt"
	"math"
	"reflect"
	"regexp"
	"strings"
	"time"

	"github.com/prometheus/client_golang/api"
	v1 "github.com/prometheus/client_golang/api/prometheus/v1"
	"github.com/prometheus/common/model"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

const (
	grpcPromQL = `sum(increase(grpc_server_handled_total{namespace=~"nightly-ms|staging-ms|prod-ms", grpc_type="unary"}[5m])) by (grpc_service, grpc_method)`
	httpPromQL = `sum(increase(kong_custom_http_status{}[5m])) by (path) > 0`
)

var fetcher *Fetcher

type Fetcher struct {
	client api.Client
	cache  *Cache
	done   chan struct{}
}

type Samples []float64

func (s Samples) MaxQPS() string {
	var max float64 = 0
	for _, ss := range s {
		if ss > max {
			max = ss
		}
	}
	return fmt.Sprintf("%.2f", max/5/60)
}

func (s Samples) RequestsCount() string {
	var sum float64 = 0
	for _, ss := range s {
		sum += ss
	}
	return fmt.Sprintf("%.0f", math.Round(sum))
}

type Cache struct {
	grpc map[string]Samples
	http map[string]Samples
}

var env2ds = map[string]string{
	"dev":     "39190b22-bb48-4167-a485-eec9917ac85a",
	"staging": "ea5eef3e-2c03-462d-9594-8bdc2299168c",
	"prod":    "90c5d4fa-ba99-4c40-8b5f-041f6dd28501",
}

func Init(env string) error {
	ds, ok := env2ds[env]
	if !ok {
		return fmt.Errorf("unknown env: %s", env)
	}
	client, err := api.NewClient(api.Config{
		Address: fmt.Sprintf("https://www.ds.us-west-2.aws.observability.tidbcloud.com/internal/metrics/%s", ds),
	})
	if err != nil {
		return fmt.Errorf("failed to create client: %s", err)
	}
	fetcher = &Fetcher{client: client}
	err = fetcher.fetchAndFlush()
	if err != nil {
		return err
	}
	go fetcher.run()
	return nil
}

func (f *Fetcher) run() {
	ticker := time.NewTicker(10 * time.Minute)
	for {
		select {
		case <-f.done:
			tilog.PlainInfo("fetcher stoped")
			ticker.Stop()
			return
		case <-ticker.C:
			err := f.fetchAndFlush()
			if err != nil {
				tilog.PlainError("fetch and flush cache failed", zap.Error(err))
			}
		}
	}
}

func (f *Fetcher) fetchAndFlush() error {
	tilog.PlainInfo("starting cache flush")
	grpcCache, err := f.fetchGrpc()
	if err != nil {
		return err
	}
	httpCache, err := f.fetchHttp()
	if err != nil {
		return err
	}
	cache := Cache{
		grpc: grpcCache,
		http: httpCache,
	}
	f.cache = &cache
	return nil
}

func (f *Fetcher) promQueryRange(promQL string) (model.Matrix, error) {
	v1api := v1.NewAPI(f.client)
	r := v1.Range{
		Start: time.Now().Add(-24 * time.Hour),
		End:   time.Now(),
		Step:  5 * time.Minute,
	}
	result, warnings, err := v1api.QueryRange(context.Background(), promQL, r, v1.WithTimeout(5*time.Second))
	if err != nil {
		return nil, err
	}
	if len(warnings) > 0 {
		tilog.PlainWarn("query prometheus encounter warnings", zap.Strings("warnings", warnings))
	}
	switch result.Type() {
	case model.ValMatrix:
		matrix, ok := result.(model.Matrix)
		if !ok {
			return nil, fmt.Errorf("result reflect failed %s", reflect.TypeOf(matrix).String())
		}
		return matrix, nil
	default:
		return nil, fmt.Errorf("unknown result type: %s", result.Type())
	}
}

func (f *Fetcher) fetchGrpc() (map[string]Samples, error) {
	matrix, err := f.promQueryRange(grpcPromQL)
	if err != nil {
		return nil, err
	}
	grpcCache := make(map[string]Samples)
	for _, m := range matrix {
		var grpcMethod, grpcService string
		for k, v := range m.Metric {
			switch k {
			case "grpc_method":
				grpcMethod = string(v)
			case "grpc_service":
				vs := strings.Split(string(v), ".")
				if len(vs) == 2 {
					grpcService = vs[1]
				}
			}
		}
		if grpcMethod == "" || grpcService == "" {
			// tilog.PlainWarn("fetch grpc got invalid metrics", zap.String("grpc_method", grpcMethod), zap.String("grpc_service", grpcService))
			continue
		}
		values := make([]float64, 0, 64)
		for _, mv := range m.Values {
			values = append(values, float64(mv.Value))
		}
		grpcCache[fmt.Sprintf("%s.%s", grpcService, grpcMethod)] = values
	}
	return grpcCache, nil
}

func (f *Fetcher) fetchHttp() (map[string]Samples, error) {
	matrix, err := f.promQueryRange(httpPromQL)
	if err != nil {
		return nil, err
	}
	httpCache := make(map[string]Samples)
	for _, m := range matrix {
		var path string
		for k, v := range m.Metric {
			switch k {
			case "path":
				path = fmt.Sprintf("^%s", strings.TrimSuffix(string(v), "$"))
			}
		}
		if path == "" {
			continue
		}
		values := make([]float64, 0, 64)
		for _, mv := range m.Values {
			values = append(values, float64(mv.Value))
		}
		httpCache[path] = values
	}
	return httpCache, nil
}

func GetGrpcSamples(service, method string) (Samples, error) {
	sample, ok := fetcher.cache.grpc[fmt.Sprintf("%s.%s", service, method)]
	if !ok {
		sample = Samples{}
	}
	return sample, nil
}

func GetHttpSamples(httpPath string) (Samples, error) {
	// replace {xxx_id} -> 123
	exp := regexp.MustCompile("{[^/]+}")
	path := exp.ReplaceAllLiteralString(httpPath, "123")
	for re, sample := range fetcher.cache.http {
		matched, err := regexp.MatchString(re, path)
		if err != nil {
			return nil, err
		} else if matched {
			return sample, nil
		}
	}
	return Samples{}, nil
}
