package runlimit

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

// NormalizedNumberString resolves one JSON number to the finite float64 domain
// used by Workflow Plan expressions and Run persistence, then removes exponent
// notation, insignificant zeroes, and negative zero. The returned string is a
// valid JSON number and matches the bytes emitted by the durable Run Store.
func NormalizedNumberString(source string) (string, error) {
	number, err := strconv.ParseFloat(source, 64)
	if err != nil || math.IsNaN(number) || math.IsInf(number, 0) {
		return "", fmt.Errorf("JSON number %q is outside the finite runtime number domain", source)
	}
	if err := validateJSONNumberString(source); err != nil {
		return "", err
	}

	source = strconv.FormatFloat(number, 'g', -1, 64)
	negative := strings.HasPrefix(source, "-")
	unsigned := strings.TrimPrefix(source, "-")
	mantissa, exponentText := unsigned, ""
	if index := strings.IndexAny(unsigned, "eE"); index >= 0 {
		mantissa, exponentText = unsigned[:index], unsigned[index+1:]
	}
	exponent := 0
	if exponentText != "" {
		parsed, err := strconv.ParseInt(exponentText, 10, 32)
		if err != nil {
			return "", fmt.Errorf("JSON number exponent is out of range in %q", source)
		}
		exponent = int(parsed)
	}
	integer, fraction := mantissa, ""
	if index := strings.IndexByte(mantissa, '.'); index >= 0 {
		integer, fraction = mantissa[:index], mantissa[index+1:]
	}
	digits := integer + fraction
	decimalPosition := len(integer) + exponent
	for len(digits) > 0 && digits[0] == '0' {
		digits = digits[1:]
		decimalPosition--
	}
	if digits == "" {
		return "0", nil
	}
	for len(digits) > 1 && digits[len(digits)-1] == '0' {
		digits = digits[:len(digits)-1]
	}
	// Finite float64 formatting bounds decimalPosition to a few hundred, but
	// retain a defensive guard before allocating the expanded decimal form.
	if decimalPosition > 1024 || decimalPosition < -1024 {
		return "", fmt.Errorf("JSON number exponent is too large in %q", source)
	}

	var result string
	switch {
	case decimalPosition <= 0:
		result = "0." + strings.Repeat("0", -decimalPosition) + digits
	case decimalPosition >= len(digits):
		result = digits + strings.Repeat("0", decimalPosition-len(digits))
	default:
		result = digits[:decimalPosition] + "." + digits[decimalPosition:]
	}
	if negative {
		result = "-" + result
	}
	return result, nil
}

func validateJSONNumberString(source string) error {
	decoder := json.NewDecoder(strings.NewReader(source))
	decoder.UseNumber()
	var value any
	if err := decoder.Decode(&value); err != nil {
		return fmt.Errorf("invalid JSON number %q: %w", source, err)
	}
	if _, ok := value.(json.Number); !ok {
		return fmt.Errorf("invalid JSON number %q", source)
	}
	var trailing any
	if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
		if err == nil {
			return fmt.Errorf("invalid JSON number %q: trailing data", source)
		}
		return fmt.Errorf("invalid JSON number %q: %w", source, err)
	}
	return nil
}
