package metricstui

import (
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"sort"
	"strconv"
	"strings"
	"time"
)

func DecodeQueryRangeResponse(raw []byte) (*QueryRangeResponse, error) {
	var response queryRangeAPIResponse
	if err := json.Unmarshal(raw, &response); err != nil {
		return nil, fmt.Errorf("decode prometheus query_range response: %w", err)
	}

	if strings.TrimSpace(response.Status) == "" {
		return nil, errors.New("missing prometheus query_range status")
	}

	if response.Status != "success" {
		return nil, fmt.Errorf("prometheus query_range api error: %s", response.APIError())
	}

	if response.Data.ResultType != "matrix" {
		return nil, fmt.Errorf("unexpected prometheus query_range result type: %s", response.Data.ResultType)
	}

	result := &QueryRangeResponse{
		Series: make([]QueryRangeSeries, 0, len(response.Data.Result)),
	}

	for _, item := range response.Data.Result {
		series := QueryRangeSeries{
			Label:  formatSeriesLabel(item.Metric),
			Metric: cloneMetric(item.Metric),
		}

		for _, sample := range item.Values {
			if len(sample) != 2 {
				series.InvalidSamples++
				continue
			}

			timestamp, err := parsePrometheusNumber(sample[0])
			if err != nil {
				series.InvalidSamples++
				continue
			}

			value, err := parsePrometheusNumber(sample[1])
			if err != nil {
				series.InvalidSamples++
				continue
			}

			series.Samples = append(series.Samples, QueryRangeSample{
				Timestamp: time.Unix(0, prometheusSecondsToUnixNano(timestamp)),
				Value:     value,
			})
		}

		result.Series = append(result.Series, series)
	}

	sort.SliceStable(result.Series, func(i, j int) bool {
		if result.Series[i].Label != result.Series[j].Label {
			return result.Series[i].Label < result.Series[j].Label
		}
		return canonicalMetricKey(result.Series[i].Metric) < canonicalMetricKey(result.Series[j].Metric)
	})

	return result, nil
}

type queryRangeAPIResponse struct {
	Status    string            `json:"status"`
	ErrorType string            `json:"errorType"`
	Error     string            `json:"error"`
	Data      queryRangeAPIData `json:"data"`
}

func (r queryRangeAPIResponse) APIError() string {
	if r.ErrorType != "" && r.Error != "" {
		return fmt.Sprintf("%s: %s", r.ErrorType, r.Error)
	}
	if r.Error != "" {
		return r.Error
	}
	if r.ErrorType != "" {
		return r.ErrorType
	}
	return "unknown error"
}

type queryRangeAPIData struct {
	ResultType string                `json:"resultType"`
	Result     []queryRangeAPISeries `json:"result"`
}

type queryRangeAPISeries struct {
	Metric map[string]string   `json:"metric"`
	Values [][]json.RawMessage `json:"values"`
}

func parsePrometheusNumber(raw json.RawMessage) (float64, error) {
	var asString string
	if err := json.Unmarshal(raw, &asString); err == nil {
		return strconv.ParseFloat(asString, 64)
	}

	var asFloat float64
	if err := json.Unmarshal(raw, &asFloat); err == nil {
		return asFloat, nil
	}

	return 0, fmt.Errorf("invalid number: %s", string(raw))
}

func prometheusSecondsToUnixNano(seconds float64) int64 {
	return int64(math.Round(seconds * float64(time.Second)))
}

func formatSeriesLabel(metric map[string]string) string {
	if len(metric) == 0 {
		return "{}"
	}

	name := metric["__name__"]
	keys := make([]string, 0, len(metric))
	for key := range metric {
		if key == "__name__" {
			continue
		}
		keys = append(keys, key)
	}
	sort.Strings(keys)

	if len(keys) == 0 {
		if name != "" {
			return name
		}
		return "{}"
	}

	parts := make([]string, 0, len(keys))
	for _, key := range keys {
		parts = append(parts, fmt.Sprintf("%s=%q", key, metric[key]))
	}

	if name == "" {
		return fmt.Sprintf("{%s}", strings.Join(parts, ","))
	}

	return fmt.Sprintf("%s{%s}", name, strings.Join(parts, ","))
}

func canonicalMetricKey(metric map[string]string) string {
	if len(metric) == 0 {
		return ""
	}

	keys := make([]string, 0, len(metric))
	for key := range metric {
		keys = append(keys, key)
	}
	sort.Strings(keys)

	parts := make([]string, 0, len(keys))
	for _, key := range keys {
		parts = append(parts, key+"="+metric[key])
	}
	return strings.Join(parts, "\x00")
}

func cloneMetric(metric map[string]string) map[string]string {
	if len(metric) == 0 {
		return nil
	}

	cloned := make(map[string]string, len(metric))
	for key, value := range metric {
		cloned[key] = value
	}
	return cloned
}
