package monetix

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha512"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"sort"
	"strconv"
	"strings"
)

func signPayload(payload any, secret string) (string, error) {
	canonical, err := signaturePayloadString(payload)
	if err != nil {
		return "", err
	}

	mac := hmac.New(sha512.New, []byte(secret))
	if _, err := mac.Write([]byte(canonical)); err != nil {
		return "", err
	}
	return base64.StdEncoding.EncodeToString(mac.Sum(nil)), nil
}

// SignPayload exposes signature calculation for callback verification.
func SignPayload(payload any, secret string) (string, error) {
	return signPayload(payload, secret)
}

func signaturePayloadString(payload any) (string, error) {
	data, err := json.Marshal(payload)
	if err != nil {
		return "", err
	}

	var root any
	decoder := json.NewDecoder(bytes.NewReader(data))
	decoder.UseNumber()
	if err := decoder.Decode(&root); err != nil {
		return "", err
	}

	lines := make([]string, 0)
	collectSignatureLines(nil, root, &lines)
	sort.Strings(lines)

	return strings.Join(lines, ";"), nil
}

func collectSignatureLines(path []string, value any, lines *[]string) {
	switch v := value.(type) {
	case map[string]any:
		for key, child := range v {
			if strings.EqualFold(key, "signature") {
				continue
			}
			collectSignatureLines(append(path, key), child, lines)
		}
	case []any:
		if len(v) == 0 {
			return
		}
		for idx, child := range v {
			collectSignatureLines(append(path, strconv.Itoa(idx)), child, lines)
		}
	default:
		line := formatSignatureLine(path, v)
		if line != "" {
			*lines = append(*lines, line)
		}
	}
}

func formatSignatureLine(path []string, value any) string {
	if len(path) == 0 {
		return ""
	}
	val := signatureValueString(value)
	segments := append(append([]string{}, path...), val)
	return strings.Join(segments, ":")
}

func signatureValueString(value any) string {
	switch v := value.(type) {
	case nil:
		return "null"
	case string:
		return v
	case json.Number:
		return v.String()
	case bool:
		if v {
			return "1"
		}
		return "0"
	case float64:
		return strconv.FormatFloat(v, 'f', -1, 64)
	case float32:
		return strconv.FormatFloat(float64(v), 'f', -1, 32)
	case int:
		return strconv.Itoa(v)
	case int8, int16, int32, int64:
		return fmt.Sprint(v)
	case uint, uint8, uint16, uint32, uint64:
		return fmt.Sprint(v)
	default:
		return fmt.Sprint(v)
	}
}
