package shuncheng

import (
	"bytes"
	"context"
	"crypto/md5"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"encoding/xml"
	"errors"
	"fmt"
	"html"
	"io"
	"math"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"
)

var (
	ErrConfigIncomplete = errors.New("shuncheng payment config incomplete")
	ErrUnsupportedSign  = errors.New("unsupported shuncheng sign type")
	ErrTokenUnavailable = errors.New("shuncheng token unavailable")
)

const (
	endpointToken        = "/api/app/backend-account/pay-token"
	endpointSimplePay    = "/api/app/payOreder/simplePay"
	endpointPaymentQuery = "/api/app/payOreder/PaymentResultQuery"
	endpointRefund       = "/api/app/refundOrder/orderRefund"
	endpointRefundQuery  = "/api/app/refundOrder/orderRefundQuery"
)

type Config struct {
	GatewayURL  string
	MerchantID  string
	SecretID    string
	SecretKey   string
	NotifyKey   string
	NotifyURL   string
	JumpURL     string
	PayWay      string
	JSPayFlag   string
	SignType    string
	ShopNo      string
	RefundType  string
	TokenMargin time.Duration
}

type Client struct {
	cfg        Config
	httpClient *http.Client
	tokenKey   string
}

type CreatePaymentRequest struct {
	ThirdOrderID string
	AmountCent   int64
	PayWay       string
	JSPayFlag    string
	NotifyURL    string
	JumpURL      string
	ClientIP     string
	Body         string
	Attach       string
}

type CreatePaymentResponse struct {
	OK              bool
	ErrorMessage    string
	MerchantID      string
	ThirdOrderID    string
	ProviderOrderID string
	PayWay          string
	Status          string
	PayTime         string
	TDCode          string
	JSPayURL        string
	JSPayInfo       string
	Raw             map[string]string
	RawRequest      map[string]string
}

type QueryPaymentResponse struct {
	OK              bool
	ErrorMessage    string
	MerchantID      string
	ThirdOrderID    string
	ProviderOrderID string
	Status          string
	Amount          string
	PayWay          string
	PayTime         string
	Raw             map[string]string
}

type CreateRefundRequest struct {
	ThirdOrderID     string
	ProviderOrderID  string
	MerchantRefundID string
	RefundAmountCent int64
	NotifyURL        string
	Attach           string
	RefundReason     string
	ClientIP         string
}

type CreateRefundResponse struct {
	OK               bool
	ErrorMessage     string
	MerchantID       string
	ThirdOrderID     string
	ProviderOrderID  string
	MerchantRefundID string
	ProviderRefundID string
	Status           string
	RefundAmount     string
	Raw              map[string]string
	RawRequest       map[string]string
}

type QueryRefundRequest struct {
	ThirdOrderID     string
	ProviderOrderID  string
	MerchantRefundID string
	ProviderRefundID string
}

type QueryRefundResponse struct {
	OK               bool
	ErrorMessage     string
	MerchantID       string
	ThirdOrderID     string
	ProviderOrderID  string
	MerchantRefundID string
	ProviderRefundID string
	Status           string
	RefundAmount     string
	RefundTime       string
	Raw              map[string]string
}

type VerifyNotifyResult struct {
	OK         bool
	MatchedKey string
	Got        string
	Expected   map[string]string
	BaseString map[string]string
	ParamKeys  []string
}

type tokenEntry struct {
	token     string
	expiresAt time.Time
}

var tokenCache = struct {
	sync.Mutex
	items map[string]tokenEntry
}{
	items: map[string]tokenEntry{},
}

func NewClient(cfg Config) *Client {
	return &Client{
		cfg: cfg,
		httpClient: &http.Client{
			Timeout: 15 * time.Second,
		},
		tokenKey: tokenCacheKey(cfg),
	}
}

func (c *Client) CreatePayment(ctx context.Context, req CreatePaymentRequest) (*CreatePaymentResponse, error) {
	if err := c.validate(); err != nil {
		return nil, err
	}
	payWay := firstNonEmpty(req.PayWay, c.cfg.PayWay, "ZFBZF")
	params := map[string]string{
		"service":        "get_tdcode",
		"jspay_flag":     firstNonEmpty(req.JSPayFlag, c.cfg.JSPayFlag, "2"),
		"sign_type":      firstNonEmpty(c.cfg.SignType, "MD5"),
		"merchant_id":    c.cfg.MerchantID,
		"third_order_id": req.ThirdOrderID,
		"amount":         strconv.FormatInt(req.AmountCent, 10),
		"nonce_str":      Nonce(32),
		"req_serial_no":  Nonce(32),
		"body":           sanitizeText(req.Body, 256),
		"attach":         sanitizeText(req.Attach, 96),
	}
	if req.NotifyURL != "" {
		params["notify_url"] = req.NotifyURL
	} else if c.cfg.NotifyURL != "" {
		params["notify_url"] = c.cfg.NotifyURL
	}
	if req.JumpURL != "" {
		params["jump_url"] = req.JumpURL
	} else if c.cfg.JumpURL != "" {
		params["jump_url"] = c.cfg.JumpURL
	}
	if req.ClientIP != "" {
		params["client_ip"] = req.ClientIP
	}
	if c.cfg.ShopNo != "" {
		params["shop_no"] = c.cfg.ShopNo
	}
	params["sign"] = SignStrings(params, c.cfg.SecretKey)
	rawReq := copyStringMap(params)
	raw, err := c.postTrade(ctx, endpointSimplePay, params)
	if err != nil {
		return nil, err
	}
	return &CreatePaymentResponse{
		OK:              responseOK(raw),
		ErrorMessage:    responseMessage(raw),
		MerchantID:      firstNonEmpty(raw["merchant_id"], c.cfg.MerchantID),
		ThirdOrderID:    firstNonEmpty(raw["third_order_id"], req.ThirdOrderID),
		ProviderOrderID: raw["schc_order_id"],
		PayWay:          firstNonEmpty(raw["pay_way"], payWay),
		Status:          normalizePaymentStatus(raw["status"]),
		PayTime:         firstNonEmpty(raw["pay_time"], raw["channel_datetime"]),
		TDCode:          raw["jspay_url"],
		JSPayURL:        raw["jspay_url"],
		JSPayInfo:       raw["jspay_info"],
		Raw:             raw,
		RawRequest:      rawReq,
	}, nil
}

func (c *Client) QueryPayment(ctx context.Context, thirdOrderID, providerOrderID string) (*QueryPaymentResponse, error) {
	if err := c.validate(); err != nil {
		return nil, err
	}
	params := map[string]string{
		"service":       "query_status",
		"sign_type":     firstNonEmpty(c.cfg.SignType, "MD5"),
		"merchant_id":   c.cfg.MerchantID,
		"nonce_str":     Nonce(32),
		"req_serial_no": Nonce(32),
	}
	if providerOrderID != "" {
		params["schc_order_id"] = providerOrderID
	} else {
		params["third_order_id"] = thirdOrderID
	}
	params["sign"] = SignStrings(params, c.cfg.SecretKey)
	raw, err := c.postTrade(ctx, endpointPaymentQuery, params)
	if err != nil {
		return nil, err
	}
	return &QueryPaymentResponse{
		OK:              responseOK(raw),
		ErrorMessage:    responseMessage(raw),
		MerchantID:      firstNonEmpty(raw["merchant_id"], c.cfg.MerchantID),
		ThirdOrderID:    firstNonEmpty(raw["third_order_id"], thirdOrderID),
		ProviderOrderID: firstNonEmpty(raw["schc_order_id"], providerOrderID),
		Status:          normalizePaymentStatus(raw["status"]),
		Amount:          raw["amount"],
		PayWay:          raw["pay_way"],
		PayTime:         firstNonEmpty(raw["pay_time"], raw["channel_datetime"]),
		Raw:             raw,
	}, nil
}

func (c *Client) CreateRefund(ctx context.Context, req CreateRefundRequest) (*CreateRefundResponse, error) {
	if err := c.validate(); err != nil {
		return nil, err
	}
	params := map[string]string{
		"service":            "unified_refund",
		"sign_type":          firstNonEmpty(c.cfg.SignType, "MD5"),
		"merchant_id":        c.cfg.MerchantID,
		"merchant_refund_id": req.MerchantRefundID,
		"refund_amount":      strconv.FormatInt(req.RefundAmountCent, 10),
		"nonce_str":          Nonce(32),
		"req_serial_no":      Nonce(32),
	}
	if req.ProviderOrderID != "" {
		params["schc_order_id"] = req.ProviderOrderID
	} else if req.ThirdOrderID != "" {
		params["third_order_id"] = req.ThirdOrderID
	}
	if req.NotifyURL != "" {
		params["notify_url"] = req.NotifyURL
	} else if c.cfg.NotifyURL != "" {
		params["notify_url"] = c.cfg.NotifyURL
	}
	if req.Attach != "" {
		params["attach"] = sanitizeText(req.Attach, 96)
	}
	if c.cfg.ShopNo != "" {
		params["shop_no"] = c.cfg.ShopNo
	}
	if c.cfg.RefundType != "" {
		params["refund_type"] = c.cfg.RefundType
	}
	params["sign"] = SignStrings(params, c.cfg.SecretKey)
	rawReq := copyStringMap(params)
	raw, err := c.postTrade(ctx, endpointRefund, params)
	if err != nil {
		return nil, err
	}
	return &CreateRefundResponse{
		OK:               responseOK(raw),
		ErrorMessage:     responseMessage(raw),
		MerchantID:       firstNonEmpty(raw["merchant_id"], c.cfg.MerchantID),
		ThirdOrderID:     firstNonEmpty(raw["third_order_id"], req.ThirdOrderID),
		ProviderOrderID:  firstNonEmpty(raw["schc_order_id"], req.ProviderOrderID),
		MerchantRefundID: firstNonEmpty(raw["merchant_refund_id"], req.MerchantRefundID),
		ProviderRefundID: raw["schc_refund_id"],
		Status:           normalizeRefundStatus(raw["status"]),
		RefundAmount:     raw["refund_amount"],
		Raw:              raw,
		RawRequest:       rawReq,
	}, nil
}

func (c *Client) QueryRefund(ctx context.Context, req QueryRefundRequest) (*QueryRefundResponse, error) {
	if err := c.validate(); err != nil {
		return nil, err
	}
	params := map[string]string{
		"service":       "unified_query_refund",
		"sign_type":     firstNonEmpty(c.cfg.SignType, "MD5"),
		"merchant_id":   c.cfg.MerchantID,
		"nonce_str":     Nonce(32),
		"req_serial_no": Nonce(32),
	}
	if req.ProviderOrderID != "" {
		params["schc_order_id"] = req.ProviderOrderID
	} else if req.ThirdOrderID != "" {
		params["third_order_id"] = req.ThirdOrderID
	}
	if req.ProviderRefundID != "" {
		params["schc_refund_id"] = req.ProviderRefundID
	} else if req.MerchantRefundID != "" {
		params["merchant_refund_id"] = req.MerchantRefundID
	}
	params["sign"] = SignStrings(params, c.cfg.SecretKey)
	raw, err := c.postTrade(ctx, endpointRefundQuery, params)
	if err != nil {
		return nil, err
	}
	return &QueryRefundResponse{
		OK:               responseOK(raw),
		ErrorMessage:     responseMessage(raw),
		MerchantID:       firstNonEmpty(raw["merchant_id"], c.cfg.MerchantID),
		ThirdOrderID:     firstNonEmpty(raw["third_order_id"], req.ThirdOrderID),
		ProviderOrderID:  firstNonEmpty(raw["schc_order_id"], req.ProviderOrderID),
		MerchantRefundID: firstNonEmpty(raw["merchant_refund_id"], req.MerchantRefundID),
		ProviderRefundID: firstNonEmpty(raw["schc_refund_id"], req.ProviderRefundID),
		Status:           normalizeRefundStatus(raw["status"]),
		RefundAmount:     raw["refund_amount"],
		RefundTime:       raw["refund_time"],
		Raw:              raw,
	}, nil
}

func (c *Client) VerifyNotify(params map[string]string) bool {
	return c.VerifyNotifyDetail(params).OK
}

func (c *Client) VerifyNotifyDetail(params map[string]string) VerifyNotifyResult {
	got := strings.TrimSpace(params["sign"])
	result := VerifyNotifyResult{
		Got:        got,
		Expected:   map[string]string{},
		BaseString: map[string]string{},
		ParamKeys:  notifyParamKeys(params),
	}
	if got == "" {
		return result
	}
	key := firstNonEmpty(c.cfg.NotifyKey, c.cfg.SecretKey)
	if key == "" {
		result.Expected["error"] = ErrConfigIncomplete.Error()
		return result
	}
	for _, alg := range []struct {
		name string
		sign func(map[string]string, string) string
	}{
		{name: "notify_key", sign: SignStrings},
		{name: "notify_key_upper_md5", sign: SignStringsUpperMD5},
	} {
		expected := alg.sign(params, key)
		result.Expected[alg.name] = expected
		result.BaseString[alg.name] = SignBaseString(stringMapToAny(params))
		if got == expected {
			result.OK = true
			result.MatchedKey = alg.name
			return result
		}
	}
	return result
}

func (c *Client) validate() error {
	if strings.TrimSpace(c.cfg.GatewayURL) == "" ||
		strings.TrimSpace(c.cfg.MerchantID) == "" ||
		strings.TrimSpace(c.cfg.SecretID) == "" ||
		strings.TrimSpace(c.cfg.SecretKey) == "" {
		return ErrConfigIncomplete
	}
	if c.cfg.SignType != "" && !strings.EqualFold(c.cfg.SignType, "MD5") {
		return ErrUnsupportedSign
	}
	return nil
}

func (c *Client) postTrade(ctx context.Context, endpoint string, params map[string]string) (map[string]string, error) {
	return c.postTradeWithRetry(ctx, endpoint, params, true)
}

func (c *Client) postTradeWithRetry(ctx context.Context, endpoint string, params map[string]string, retryOnUnauthorized bool) (map[string]string, error) {
	token, err := c.token(ctx)
	if err != nil {
		return nil, err
	}
	body, err := json.Marshal(params)
	if err != nil {
		return nil, err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpointURL(endpoint), bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json;charset=UTF-8")
	req.Header.Set("Accept", "application/xml,application/json")
	req.Header.Set("Authorization", "Bearer "+token)
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	respBody, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
	if err != nil {
		return nil, err
	}
	if resp.StatusCode == http.StatusUnauthorized && retryOnUnauthorized {
		c.clearToken()
		return c.postTradeWithRetry(ctx, endpoint, params, false)
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("shuncheng http status %d: %s", resp.StatusCode, string(respBody))
	}
	raw, err := ParsePayload(respBody)
	if err != nil {
		return nil, fmt.Errorf("shuncheng parse response failed endpoint=%s status=%d body=%s: %w", endpoint, resp.StatusCode, payloadSnippet(respBody), err)
	}
	return raw, nil
}

func (c *Client) token(ctx context.Context) (string, error) {
	margin := c.cfg.TokenMargin
	if margin <= 0 {
		margin = 5 * time.Minute
	}
	now := time.Now()
	tokenCache.Lock()
	if item, ok := tokenCache.items[c.tokenKey]; ok && item.token != "" && now.Add(margin).Before(item.expiresAt) {
		tokenCache.Unlock()
		return item.token, nil
	}
	token, err := c.fetchToken(ctx)
	if err != nil {
		tokenCache.Unlock()
		return "", err
	}
	tokenCache.items[c.tokenKey] = tokenEntry{
		token:     token,
		expiresAt: time.Now().Add(time.Hour),
	}
	tokenCache.Unlock()
	return token, nil
}

func (c *Client) fetchToken(ctx context.Context) (string, error) {
	body, err := json.Marshal(map[string]string{
		"secretId":  c.cfg.SecretID,
		"secretKey": c.cfg.SecretKey,
	})
	if err != nil {
		return "", err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpointURL(endpointToken), bytes.NewReader(body))
	if err != nil {
		return "", err
	}
	req.Header.Set("Content-Type", "application/json;charset=UTF-8")
	req.Header.Set("Accept", "application/json")
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if err != nil {
		return "", err
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return "", fmt.Errorf("shuncheng token http status %d: %s", resp.StatusCode, string(respBody))
	}
	var payload struct {
		RespCode    string `json:"respCode"`
		RespMsg     string `json:"respMsg"`
		ReqSerialNo string `json:"reqSerialNo"`
		Version     string `json:"version"`
		Data        string `json:"data"`
	}
	if err := json.Unmarshal(respBody, &payload); err != nil {
		return "", err
	}
	if payload.RespCode != "200" || strings.TrimSpace(payload.Data) == "" {
		return "", fmt.Errorf("%w: %s %s", ErrTokenUnavailable, payload.RespCode, payload.RespMsg)
	}
	return payload.Data, nil
}

func (c *Client) clearToken() {
	tokenCache.Lock()
	delete(tokenCache.items, c.tokenKey)
	tokenCache.Unlock()
}

func (c *Client) endpointURL(endpoint string) string {
	return strings.TrimRight(strings.TrimSpace(c.cfg.GatewayURL), "/") + endpoint
}

func ParsePayload(body []byte) (map[string]string, error) {
	trimmed := trimPayload(body)
	if len(trimmed) == 0 {
		return map[string]string{}, nil
	}
	if trimmed[0] == '"' {
		var xmlText string
		if err := json.Unmarshal(trimmed, &xmlText); err == nil && strings.TrimSpace(xmlText) != "" {
			return ParsePayload([]byte(xmlText))
		}
	}
	if xmlBodies := extractXMLPayloads(trimmed); len(xmlBodies) > 0 {
		var lastErr error
		for _, xmlBody := range xmlBodies {
			raw, err := parseXMLPayload(xmlBody)
			if err != nil {
				lastErr = err
				continue
			}
			if !looksLikeShunchengPayload(raw) {
				lastErr = fmt.Errorf("unsupported shuncheng xml payload %s", payloadSnippet(trimmed))
				continue
			}
			normalizeAliases(raw)
			return raw, nil
		}
		if lastErr != nil {
			return nil, fmt.Errorf("unsupported shuncheng xml payload %s: %w", payloadSnippet(trimmed), lastErr)
		}
		return nil, fmt.Errorf("unsupported shuncheng xml payload %s", payloadSnippet(trimmed))
	}
	switch trimmed[0] {
	case '<':
		raw, err := parseXMLPayload(trimmed)
		if err != nil {
			return nil, err
		}
		if !looksLikeShunchengPayload(raw) {
			return nil, fmt.Errorf("unsupported shuncheng xml payload %s", payloadSnippet(trimmed))
		}
		normalizeAliases(raw)
		return raw, nil
	case '{', '[':
		var payload any
		if err := json.Unmarshal(trimmed, &payload); err != nil {
			return nil, err
		}
		raw := flattenJSON(payload)
		if !looksLikeShunchengPayload(raw) {
			return nil, fmt.Errorf("unsupported shuncheng json payload %s", payloadSnippet(trimmed))
		}
		normalizeAliases(raw)
		return raw, nil
	default:
		values, err := url.ParseQuery(string(trimmed))
		if err != nil {
			return nil, fmt.Errorf("unsupported shuncheng payload %s: %w", payloadSnippet(trimmed), err)
		}
		out := map[string]string{}
		for key, item := range values {
			if len(item) > 0 {
				out[key] = item[0]
			}
		}
		if len(out) == 0 {
			return nil, fmt.Errorf("unsupported shuncheng payload %s", payloadSnippet(trimmed))
		}
		if !looksLikeShunchengPayload(out) {
			return nil, fmt.Errorf("unsupported shuncheng form payload %s", payloadSnippet(trimmed))
		}
		normalizeAliases(out)
		return out, nil
	}
}

func looksLikeShunchengPayload(raw map[string]string) bool {
	for _, key := range []string{
		"resp_code",
		"respCode",
		"result_code",
		"third_order_id",
		"schc_order_id",
		"schc_refund_id",
		"merchant_refund_id",
		"status",
		"error_code",
		"error_msg",
	} {
		if strings.TrimSpace(raw[key]) != "" {
			return true
		}
	}
	return false
}

func extractXMLPayloads(body []byte) [][]byte {
	candidates := [][]byte{
		body,
		[]byte(html.UnescapeString(string(body))),
		[]byte(unescapeXMLTagEntities(string(body))),
	}
	out := make([][]byte, 0, len(candidates))
	for _, candidate := range candidates {
		payload := extractRawXMLPayload(candidate)
		if len(payload) == 0 || hasPayload(out, payload) {
			continue
		}
		out = append(out, payload)
	}
	return out
}

func extractRawXMLPayload(candidate []byte) []byte {
	start := bytes.Index(candidate, []byte("<schc"))
	if start < 0 {
		return nil
	}
	end := bytes.Index(candidate[start:], []byte("</schc>"))
	if end < 0 {
		return nil
	}
	end += start + len("</schc>")
	return bytes.TrimSpace(candidate[start:end])
}

func hasPayload(items [][]byte, candidate []byte) bool {
	for _, item := range items {
		if bytes.Equal(item, candidate) {
			return true
		}
	}
	return false
}

func unescapeXMLTagEntities(value string) string {
	replacer := strings.NewReplacer(
		"&lt;", "<",
		"&LT;", "<",
		"&#60;", "<",
		"&#060;", "<",
		"&#x3c;", "<",
		"&#x3C;", "<",
		"&#X3c;", "<",
		"&#X3C;", "<",
		"&gt;", ">",
		"&GT;", ">",
		"&#62;", ">",
		"&#062;", ">",
		"&#x3e;", ">",
		"&#x3E;", ">",
		"&#X3e;", ">",
		"&#X3E;", ">",
	)
	return replacer.Replace(value)
}

func trimPayload(body []byte) []byte {
	trimmed := bytes.TrimSpace(body)
	trimmed = bytes.TrimPrefix(trimmed, []byte{0xEF, 0xBB, 0xBF})
	return bytes.TrimSpace(trimmed)
}

func payloadSnippet(body []byte) string {
	trimmed := trimPayload(body)
	text := strings.Join(strings.Fields(string(trimmed)), " ")
	if text == "" {
		return "<empty>"
	}
	runes := []rune(text)
	if len(runes) > 300 {
		text = string(runes[:300]) + "..."
	}
	return strconv.Quote(text)
}

func parseXMLPayload(body []byte) (map[string]string, error) {
	decoder := xml.NewDecoder(bytes.NewReader(body))
	out := map[string]string{}
	var current string
	depth := 0
	for {
		token, err := decoder.Token()
		if err == io.EOF {
			break
		}
		if err != nil {
			return nil, err
		}
		switch item := token.(type) {
		case xml.StartElement:
			depth++
			if depth > 1 {
				current = item.Name.Local
				if _, exists := out[current]; !exists {
					out[current] = ""
				}
			}
		case xml.CharData:
			value := strings.TrimSpace(string(item))
			if current != "" && value != "" {
				out[current] = value
			}
		case xml.EndElement:
			if current == item.Name.Local {
				current = ""
			}
			if depth > 0 {
				depth--
			}
		}
	}
	return out, nil
}

func SignStrings(params map[string]string, secretKey string) string {
	return Sign(stringMapToAny(params), secretKey)
}

func Sign(params map[string]any, secretKey string) string {
	baseString := SignBaseString(params)
	signString := "key=" + secretKey
	if baseString != "" {
		signString = baseString + "&key=" + secretKey
	}
	sum := md5.Sum([]byte(signString))
	hexValue := strings.ToLower(hex.EncodeToString(sum[:]))
	return base64.StdEncoding.EncodeToString([]byte(hexValue))
}

func SignStringsUpperMD5(params map[string]string, secretKey string) string {
	baseString := SignBaseString(stringMapToAny(params))
	signString := "key=" + secretKey
	if baseString != "" {
		signString = baseString + "&key=" + secretKey
	}
	sum := md5.Sum([]byte(signString))
	return strings.ToUpper(hex.EncodeToString(sum[:]))
}

func SignBaseString(params map[string]any) string {
	filtered := map[string]string{}
	for key, value := range params {
		if key == "sign" || value == nil {
			continue
		}
		processed, ok := signValue(value)
		if !ok || processed == "" {
			continue
		}
		filtered[key] = processed
	}
	keys := make([]string, 0, len(filtered))
	for key := range filtered {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	parts := make([]string, 0, len(keys))
	for _, key := range keys {
		parts = append(parts, key+"="+filtered[key])
	}
	return strings.Join(parts, "&")
}

func signValue(value any) (string, bool) {
	switch item := value.(type) {
	case nil:
		return "", false
	case string:
		return item, true
	case map[string]any:
		return sortedJSONObject(item), true
	case map[string]string:
		converted := make(map[string]any, len(item))
		for key, child := range item {
			converted[key] = child
		}
		return sortedJSONObject(converted), true
	case []any:
		raw, _ := json.Marshal(item)
		return string(raw), true
	case []string:
		raw, _ := json.Marshal(item)
		return string(raw), true
	case bool:
		return strconv.FormatBool(item), true
	case int:
		return strconv.Itoa(item), true
	case int64:
		return strconv.FormatInt(item, 10), true
	case float64:
		if math.Trunc(item) == item {
			return strconv.FormatInt(int64(item), 10), true
		}
		return strconv.FormatFloat(item, 'f', -1, 64), true
	default:
		return fmt.Sprint(item), true
	}
}

func sortedJSONObject(value map[string]any) string {
	filtered := map[string]any{}
	keys := make([]string, 0, len(value))
	for key, item := range value {
		if item == nil {
			continue
		}
		filtered[key] = item
		keys = append(keys, key)
	}
	sort.Strings(keys)
	buf := bytes.NewBufferString("{")
	for idx, key := range keys {
		if idx > 0 {
			buf.WriteByte(',')
		}
		keyRaw, _ := json.Marshal(key)
		buf.Write(keyRaw)
		buf.WriteByte(':')
		valueRaw, _ := json.Marshal(filtered[key])
		buf.Write(valueRaw)
	}
	buf.WriteByte('}')
	return buf.String()
}

func normalizeAliases(raw map[string]string) {
	raw["third_order_id"] = firstNonEmpty(raw["third_order_id"], raw["out_order_no"])
	raw["provider_order_id"] = firstNonEmpty(raw["provider_order_id"], raw["schc_order_id"])
	raw["amount"] = firstNonEmpty(raw["amount"], raw["total_amount"])
	raw["pay_time"] = firstNonEmpty(raw["pay_time"], raw["channel_datetime"], raw["refund_time"])
	if raw["merchant_refund_id"] != "" || raw["schc_refund_id"] != "" || raw["refund_amount"] != "" {
		raw["provider_refund_id"] = firstNonEmpty(raw["provider_refund_id"], raw["schc_refund_id"])
	}
}

func normalizePaymentStatus(status string) string {
	switch strings.ToUpper(strings.TrimSpace(status)) {
	case "2", "SUCCESS", "PAID", "PAY_SUCCESS", "TRADE_SUCCESS":
		return "paid"
	case "6", "CLOSED", "CLOSE", "CANCEL", "CANCELLED", "CANCELED":
		return "closed"
	case "8", "FAIL", "FAILED", "PAY_FAIL", "TRADE_FAIL":
		return "failed"
	default:
		return "paying"
	}
}

func normalizeRefundStatus(status string) string {
	switch strings.ToUpper(strings.TrimSpace(status)) {
	case "11", "SUCCESS", "REFUNDED", "REFUND_SUCCESS", "TRADE_SUCCESS":
		return "refunded"
	case "12", "FAIL", "FAILED", "REFUND_FAIL", "TRADE_FAIL":
		return "failed"
	default:
		return "refunding"
	}
}

func responseOK(raw map[string]string) bool {
	respCode := strings.ToUpper(firstNonEmpty(raw["resp_code"], raw["respCode"]))
	resultCode := strings.ToUpper(raw["result_code"])
	if respCode == "" && resultCode == "" {
		return false
	}
	respOK := respCode == "" || respCode == "200" || respCode == "0" || respCode == "SUCCESS"
	resultOK := resultCode == "" || resultCode == "0" || resultCode == "SUCCESS"
	return respOK && resultOK
}

func responseMessage(raw map[string]string) string {
	return firstNonEmpty(raw["error_msg"], raw["resp_msg"], raw["respMsg"], raw["message"], raw["msg"])
}

func flattenJSON(value any) map[string]string {
	out := map[string]string{}
	var walk func(prefix string, v any)
	walk = func(prefix string, v any) {
		switch item := v.(type) {
		case map[string]any:
			for key, child := range item {
				if key == "data" || key == "resp_data" || key == "req_data" {
					walk("", child)
					continue
				}
				walk(key, child)
			}
		case []any:
			raw, _ := json.Marshal(item)
			if prefix != "" {
				out[prefix] = string(raw)
			}
		case nil:
			if prefix != "" {
				out[prefix] = ""
			}
		case string:
			if prefix != "" {
				out[prefix] = item
			}
		case float64:
			if prefix != "" {
				if math.Trunc(item) == item {
					out[prefix] = strconv.FormatInt(int64(item), 10)
				} else {
					out[prefix] = strconv.FormatFloat(item, 'f', -1, 64)
				}
			}
		case bool:
			if prefix != "" {
				out[prefix] = strconv.FormatBool(item)
			}
		default:
			if prefix != "" {
				out[prefix] = fmt.Sprint(item)
			}
		}
	}
	walk("", value)
	return out
}

func stringMapToAny(params map[string]string) map[string]any {
	out := make(map[string]any, len(params))
	for key, value := range params {
		out[key] = value
	}
	return out
}

func copyStringMap(params map[string]string) map[string]string {
	out := make(map[string]string, len(params))
	for key, value := range params {
		out[key] = value
	}
	return out
}

func notifyParamKeys(params map[string]string) []string {
	keys := make([]string, 0, len(params))
	for key, value := range params {
		if key == "sign" || value == "" {
			continue
		}
		keys = append(keys, key)
	}
	sort.Strings(keys)
	return keys
}

func tokenCacheKey(cfg Config) string {
	hash := sha256.Sum256([]byte(cfg.SecretKey))
	return strings.TrimRight(strings.TrimSpace(cfg.GatewayURL), "/") + "\x00" + cfg.SecretID + "\x00" + hex.EncodeToString(hash[:])
}

func Nonce(length int) string {
	const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
	if length <= 0 {
		length = 32
	}
	buf := make([]byte, length)
	if _, err := rand.Read(buf); err != nil {
		return strconv.FormatInt(time.Now().UnixNano(), 36)
	}
	for idx := range buf {
		buf[idx] = chars[int(buf[idx])%len(chars)]
	}
	return string(buf)
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return value
		}
	}
	return ""
}

func sanitizeText(value string, maxRunes int) string {
	value = strings.ReplaceAll(value, "\n", " ")
	value = strings.ReplaceAll(value, "\r", " ")
	value = strings.TrimSpace(value)
	if value == "" || maxRunes <= 0 {
		return value
	}
	runes := []rune(value)
	if len(runes) <= maxRunes {
		return value
	}
	return string(runes[:maxRunes])
}
