package api

import (
	"encoding/json"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/grafana/grafana-plugin-sdk-go/data"
	"github.com/prometheus/prometheus/model/labels"
	"github.com/prometheus/prometheus/promql"
	"github.com/prometheus/prometheus/promql/parser"

	"github.com/grafana/grafana/pkg/api/response"
	"github.com/grafana/grafana/pkg/services/ngalert/eval"
	"github.com/grafana/grafana/pkg/util"
)

type instantQueryResponse struct {
	Status    string    `json:"status"`
	Data      queryData `json:"data,omitempty"`
	ErrorType string    `json:"errorType,omitempty"`
	Error     string    `json:"error,omitempty"`
}

type queryData struct {
	ResultType parser.ValueType `json:"resultType"`
	Result     json.RawMessage  `json:"result"`
	vector     vector           `json:"-"`
	scalar     scalar           `json:"-"`
}

type scalar promql.Scalar

func (s *scalar) UnmarshalJSON(b []byte) error {
	var xs []any
	if err := json.Unmarshal(b, &xs); err != nil {
		return err
	}
	// scalars are encoded like `[ts/1000, "value"]`
	if len(xs) != 2 {
		return fmt.Errorf("unexpected number of scalar encoded values: %d", len(xs))
	}
	ts, ok := xs[0].(float64)
	if !ok {
		return fmt.Errorf("first value in scalar uncoercible to timestamp: %v", xs[0])
	}
	s.T = int64(ts) * 1000
	v, ok := xs[1].(string)
	if !ok {
		return fmt.Errorf("second value in scalar not string encoded: %v", xs[1])
	}
	f, err := strconv.ParseFloat(v, 64)
	if err != nil {
		return err
	}
	s.V = f
	return nil
}

func (d *queryData) UnmarshalJSON(b []byte) error {
	type plain queryData
	if err := json.Unmarshal(b, (*plain)(d)); err != nil {
		return err
	}

	switch d.ResultType {
	case parser.ValueTypeScalar:
		return json.Unmarshal(d.Result, &d.scalar)
	case parser.ValueTypeVector:
		return json.Unmarshal(d.Result, &d.vector)
	default:
		return fmt.Errorf("unexpected response type: %s", d.ResultType)
	}
}

type sample struct {
	Metric labels.Labels `json:"metric"`
	Value  scalar        `json:"value"`
}
type vector []sample

func instantQueryResults(resp instantQueryResponse) (eval.Results, error) {
	if resp.Error != "" || resp.Status != "success" {
		return nil, errors.New(resp.Error)
	}

	switch resp.Data.ResultType {
	case parser.ValueTypeScalar:
		return eval.Results{{
			Instance:    map[string]string{},
			State:       eval.Alerting,
			EvaluatedAt: TimeFromMillis(resp.Data.scalar.T),
			EvaluationString: extractEvalStringFromProm(sample{
				Value: resp.Data.scalar,
			}),
		}}, nil
	case parser.ValueTypeVector:
		results := make(eval.Results, 0, len(resp.Data.vector))
		for _, s := range resp.Data.vector {
			results = append(results, eval.Result{
				Instance:         s.Metric.Map(),
				State:            eval.Alerting,
				EvaluatedAt:      TimeFromMillis(s.Value.T),
				EvaluationString: extractEvalStringFromProm(s),
			})
		}
		return results, nil
	default:
		return nil, fmt.Errorf("unexpected response type: %s", resp.Data.ResultType)
	}
}

func instantQueryResultsExtractor(r *response.NormalResponse) (any, error) {
	contentType := r.Header().Get("Content-Type")
	if !strings.Contains(contentType, "json") {
		return nil, fmt.Errorf("unexpected content type from upstream. expected JSON, got %v", contentType)
	}
	var resp instantQueryResponse
	err := json.Unmarshal(r.Body(), &resp)
	if err != nil {
		return nil, err
	}

	res, err := instantQueryResults(resp)
	if err != nil {
		return nil, err
	}
	frame := res.AsDataFrame()

	return util.DynMap{
		"instances": []*data.Frame{&frame},
	}, nil
}

// extractEvalStringFromProm is intended to mimic the functionality used in ngalert/eval
func extractEvalStringFromProm(s sample) string {
	var sb strings.Builder
	sb.WriteString("[ ")
	var ls string
	if len(s.Metric) > 0 {
		ls = s.Metric.String()
	}
	sb.WriteString(fmt.Sprintf("labels={%s} ", ls))
	sb.WriteString(fmt.Sprintf("value=%v ", fmt.Sprintf("%v", s.Value.V)))
	sb.WriteString("]")
	return sb.String()
}

// TimeFromMillis Copied from https://github.com/grafana/mimir/blob/main/pkg/util/time.go as it doesn't seem worth it to import Mimir.
// TimeFromMillis is a helper to turn milliseconds -> time.Time
func TimeFromMillis(ms int64) time.Time {
	return time.Unix(ms/1000, (ms%1000)*int64(time.Millisecond)).UTC()
}
