package leshua

import (
	"bytes"
	"context"
	"crypto/md5"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"encoding/xml"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"sort"
	"strings"
	"time"

	"hfb_sys/backend/internal/config"
)

var (
	ErrConfigIncomplete = errors.New("leshua payment config incomplete")
	ErrUnsupportedSign  = errors.New("unsupported leshua sign type")
)

type Client struct {
	cfg        config.LeshuaPaymentConfig
	httpClient *http.Client
}

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

type CreatePaymentResponse struct {
	RespCode        string
	ResultCode      string
	ErrorCode       string
	ErrorMessage    string
	MerchantID      string
	ThirdOrderID    string
	ProviderOrderID string
	PayWay          string
	TDCode          string
	JSPayURL        string
	JSPayInfo       string
	Raw             map[string]string
}

type QueryPaymentResponse struct {
	RespCode        string
	ResultCode      string
	ErrorCode       string
	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 // 原支付商户订单号
	LeshuaOrderID    string // 原支付乐刷订单号(优先使用)
	MerchantRefundID string // 商户退款单号(唯一)
	RefundAmountCent int64  // 退款金额(分)
	NotifyURL        string
	Attach           string
}

type CreateRefundResponse struct {
	RespCode         string
	ResultCode       string
	ErrorCode        string
	ErrorMessage     string
	MerchantID       string
	ThirdOrderID     string
	LeshuaOrderID    string
	MerchantRefundID string
	LeshuaRefundID   string
	RefundAmount     string
	TotalAmount      string
	OrderBalance     string
	Status           string
	Raw              map[string]string
}

type QueryRefundRequest struct {
	ThirdOrderID     string
	LeshuaOrderID    string
	MerchantRefundID string
	LeshuaRefundID   string
}

type QueryRefundResponse struct {
	RespCode         string
	ResultCode       string
	ErrorCode        string
	ErrorMessage     string
	MerchantID       string
	ThirdOrderID     string
	LeshuaOrderID    string
	MerchantRefundID string
	LeshuaRefundID   string
	Status           string
	RefundAmount     string
	TotalAmount      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
}

func NewClient(cfg config.LeshuaPaymentConfig) *Client {
	return &Client{
		cfg: cfg,
		httpClient: &http.Client{
			Timeout: 10 * time.Second,
		},
	}
}

func (c *Client) CreatePayment(ctx context.Context, req CreatePaymentRequest) (*CreatePaymentResponse, map[string]string, error) {
	if err := c.validate(); err != nil {
		return nil, nil, err
	}
	payWay := firstNonEmpty(req.PayWay, c.cfg.PayWay, "ZFBZF")
	jsPayFlag := firstNonEmpty(req.JSPayFlag, c.cfg.JSPayFlag, "2")
	params := map[string]string{
		"service":        "get_tdcode",
		"merchant_id":    c.cfg.MerchantID,
		"third_order_id": req.ThirdOrderID,
		"amount":         fmt.Sprintf("%d", req.AmountCent),
		"pay_way":        payWay,
		"jspay_flag":     jsPayFlag,
		"nonce_str":      Nonce(32),
		"body":           sanitizeText(req.Body, 128),
		"attach":         sanitizeText(req.Attach, 64),
	}
	if c.cfg.SignType != "" && !strings.EqualFold(c.cfg.SignType, "MD5") {
		params["sign_type"] = c.cfg.SignType
	}
	if req.NotifyURL != "" {
		params["notify_url"] = req.NotifyURL
	}
	if req.JumpURL != "" {
		params["jump_url"] = req.JumpURL
	}
	if req.ClientIP != "" {
		params["client_ip"] = req.ClientIP
	}
	params["sign"] = Sign(params, c.cfg.SignKey, SignOptions{})
	raw, err := c.post(ctx, params)
	if err != nil {
		return nil, params, err
	}
	resp := &CreatePaymentResponse{
		RespCode:        raw["resp_code"],
		ResultCode:      raw["result_code"],
		ErrorCode:       raw["error_code"],
		ErrorMessage:    firstNonEmpty(raw["error_msg"], raw["resp_msg"]),
		MerchantID:      raw["merchant_id"],
		ThirdOrderID:    raw["third_order_id"],
		ProviderOrderID: raw["leshua_order_id"],
		PayWay:          raw["pay_way"],
		TDCode:          raw["td_code"],
		JSPayURL:        raw["jspay_url"],
		JSPayInfo:       raw["jspay_info"],
		Raw:             raw,
	}
	return resp, params, 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",
		"merchant_id": c.cfg.MerchantID,
		"nonce_str":   Nonce(32),
	}
	if providerOrderID != "" {
		params["leshua_order_id"] = providerOrderID
	} else {
		params["third_order_id"] = thirdOrderID
	}
	if c.cfg.SignType != "" && !strings.EqualFold(c.cfg.SignType, "MD5") {
		params["sign_type"] = c.cfg.SignType
	}
	params["sign"] = Sign(params, c.cfg.SignKey, SignOptions{})
	raw, err := c.post(ctx, params)
	if err != nil {
		return nil, err
	}
	return &QueryPaymentResponse{
		RespCode:        raw["resp_code"],
		ResultCode:      raw["result_code"],
		ErrorCode:       raw["error_code"],
		ErrorMessage:    firstNonEmpty(raw["error_msg"], raw["resp_msg"]),
		MerchantID:      raw["merchant_id"],
		ThirdOrderID:    raw["third_order_id"],
		ProviderOrderID: raw["leshua_order_id"],
		Status:          raw["status"],
		Amount:          raw["amount"],
		PayWay:          raw["pay_way"],
		PayTime:         raw["pay_time"],
		Raw:             raw,
	}, nil
}

func (c *Client) CreateRefund(ctx context.Context, req CreateRefundRequest) (*CreateRefundResponse, map[string]string, error) {
	if err := c.validate(); err != nil {
		return nil, nil, err
	}
	params := map[string]string{
		"service":           "unified_refund",
		"merchant_id":       c.cfg.MerchantID,
		"merchant_refund_id": req.MerchantRefundID,
		"refund_amount":     fmt.Sprintf("%d", req.RefundAmountCent),
		"nonce_str":         Nonce(32),
	}
	if req.LeshuaOrderID != "" {
		params["leshua_order_id"] = req.LeshuaOrderID
	} else if req.ThirdOrderID != "" {
		params["third_order_id"] = req.ThirdOrderID
	}
	if req.Attach != "" {
		params["attach"] = sanitizeText(req.Attach, 64)
	}
	if c.cfg.SignType != "" && !strings.EqualFold(c.cfg.SignType, "MD5") {
		params["sign_type"] = c.cfg.SignType
	}
	if req.NotifyURL != "" {
		params["notify_url"] = req.NotifyURL
	}
	params["sign"] = Sign(params, c.cfg.SignKey, SignOptions{})
	raw, err := c.post(ctx, params)
	if err != nil {
		return nil, params, err
	}
	resp := &CreateRefundResponse{
		RespCode:         raw["resp_code"],
		ResultCode:       raw["result_code"],
		ErrorCode:        raw["error_code"],
		ErrorMessage:     firstNonEmpty(raw["error_msg"], raw["resp_msg"]),
		MerchantID:       raw["merchant_id"],
		ThirdOrderID:     raw["third_order_id"],
		LeshuaOrderID:    raw["leshua_order_id"],
		MerchantRefundID: raw["merchant_refund_id"],
		LeshuaRefundID:   raw["leshua_refund_id"],
		RefundAmount:     raw["refund_amount"],
		TotalAmount:      raw["total_amount"],
		OrderBalance:     raw["order_balance"],
		Status:           raw["status"],
		Raw:              raw,
	}
	return resp, params, 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",
		"merchant_id": c.cfg.MerchantID,
		"nonce_str":   Nonce(32),
	}
	if req.LeshuaOrderID != "" {
		params["leshua_order_id"] = req.LeshuaOrderID
	} else if req.ThirdOrderID != "" {
		params["third_order_id"] = req.ThirdOrderID
	}
	if req.LeshuaRefundID != "" {
		params["leshua_refund_id"] = req.LeshuaRefundID
	} else if req.MerchantRefundID != "" {
		params["merchant_refund_id"] = req.MerchantRefundID
	}
	if c.cfg.SignType != "" && !strings.EqualFold(c.cfg.SignType, "MD5") {
		params["sign_type"] = c.cfg.SignType
	}
	params["sign"] = Sign(params, c.cfg.SignKey, SignOptions{})
	raw, err := c.post(ctx, params)
	if err != nil {
		return nil, err
	}
	return &QueryRefundResponse{
		RespCode:         raw["resp_code"],
		ResultCode:       raw["result_code"],
		ErrorCode:        raw["error_code"],
		ErrorMessage:     firstNonEmpty(raw["error_msg"], raw["resp_msg"]),
		MerchantID:       raw["merchant_id"],
		ThirdOrderID:     raw["third_order_id"],
		LeshuaOrderID:    raw["leshua_order_id"],
		MerchantRefundID: raw["merchant_refund_id"],
		LeshuaRefundID:   raw["leshua_refund_id"],
		Status:           raw["status"],
		RefundAmount:     raw["refund_amount"],
		TotalAmount:      raw["total_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.ToUpper(params["sign"])
	result := VerifyNotifyResult{
		Got:        got,
		Expected:   map[string]string{},
		BaseString: map[string]string{},
		ParamKeys:  notifyParamKeys(params),
	}
	if got == "" {
		return result
	}
	for _, item := range c.notifyKeyCandidates() {
		expected := Sign(params, item.key, notifySignOptions())
		baseString := SignBaseString(params, notifySignOptions())
		result.Expected[item.name] = expected
		result.BaseString[item.name] = baseString
		if got == expected {
			result.OK = true
			result.MatchedKey = item.name
			return result
		}
	}
	return result
}

func notifySignOptions() SignOptions {
	return SignOptions{
		IncludeEmpty: true,
		ExcludeKeys:  []string{"error_code", "leshua", "sign"},
	}
}

type notifyKeyCandidate struct {
	name string
	key  string
}

func (c *Client) notifyKeyCandidates() []notifyKeyCandidate {
	candidates := []notifyKeyCandidate{}
	if c.cfg.NotifyKey != "" {
		candidates = append(candidates, notifyKeyCandidate{name: "notify_key", key: c.cfg.NotifyKey})
	}
	return candidates
}

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

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

func (c *Client) post(ctx context.Context, params map[string]string) (map[string]string, error) {
	values := url.Values{}
	for key, value := range params {
		values.Set(key, value)
	}
	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.cfg.GatewayURL, strings.NewReader(values.Encode()))
	if err != nil {
		return nil, err
	}
	httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	httpResp, err := c.httpClient.Do(httpReq)
	if err != nil {
		return nil, err
	}
	defer httpResp.Body.Close()
	body, err := io.ReadAll(io.LimitReader(httpResp.Body, 1<<20))
	if err != nil {
		return nil, err
	}
	if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
		return nil, fmt.Errorf("leshua http status %d: %s", httpResp.StatusCode, string(body))
	}
	return ParsePayload(body)
}

type SignOptions struct {
	IncludeEmpty bool
	ExcludeKeys  []string
}

func Sign(params map[string]string, key string, opts SignOptions) string {
	baseString := SignBaseString(params, opts)
	stringSignTemp := "key=" + key
	if baseString != "" {
		stringSignTemp = baseString + "&key=" + key
	}
	sum := md5.Sum([]byte(stringSignTemp))
	return strings.ToUpper(hex.EncodeToString(sum[:]))
}

func SignBaseString(params map[string]string, opts SignOptions) string {
	excluded := map[string]bool{}
	for _, item := range opts.ExcludeKeys {
		excluded[item] = true
	}
	if len(opts.ExcludeKeys) == 0 {
		excluded["sign"] = true
	}
	keys := make([]string, 0, len(params))
	for name, value := range params {
		if excluded[name] {
			continue
		}
		if !opts.IncludeEmpty && value == "" {
			continue
		}
		keys = append(keys, name)
	}
	sort.Strings(keys)
	parts := make([]string, 0, len(keys)+1)
	for _, name := range keys {
		parts = append(parts, name+"="+params[name])
	}
	return strings.Join(parts, "&")
}

func ParsePayload(body []byte) (map[string]string, error) {
	trimmed := bytes.TrimSpace(body)
	if len(trimmed) == 0 {
		return map[string]string{}, nil
	}
	if trimmed[0] == '<' {
		return parseXMLPayload(trimmed)
	}
	if trimmed[0] == '{' {
		var raw map[string]any
		if err := json.Unmarshal(trimmed, &raw); err != nil {
			return nil, err
		}
		out := map[string]string{}
		for key, value := range raw {
			out[key] = fmt.Sprint(value)
		}
		return out, nil
	}
	values, err := url.ParseQuery(string(trimmed))
	if err != nil {
		return nil, 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 leshua payload: %s", string(trimmed))
	}
	return out, nil
}

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 Nonce(length int) string {
	const alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
	if length <= 0 {
		length = 32
	}
	buf := make([]byte, length)
	random := make([]byte, length)
	if _, err := rand.Read(random); err != nil {
		for i := range buf {
			buf[i] = alphabet[int(time.Now().UnixNano())%len(alphabet)]
		}
		return string(buf)
	}
	for i, item := range random {
		buf[i] = alphabet[int(item)%len(alphabet)]
	}
	return string(buf)
}

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

func sanitizeText(value string, max int) string {
	value = strings.ReplaceAll(value, "\n", " ")
	value = strings.ReplaceAll(value, "\r", " ")
	value = strings.TrimSpace(value)
	if max > 0 && len([]rune(value)) > max {
		return string([]rune(value)[:max])
	}
	return value
}
