package main

import (
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"sort"
	"strconv"
	"strings"
)

type rpcRequest struct {
	JSONRPC string          `json:"jsonrpc"`
	ID      string          `json:"id"`
	Method  string          `json:"method"`
	Params  json.RawMessage `json:"params"`
}

type rpcResponse struct {
	JSONRPC string    `json:"jsonrpc"`
	ID      string    `json:"id"`
	Result  any       `json:"result,omitempty"`
	Error   *rpcError `json:"error,omitempty"`
}

type rpcError struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

type createRequest struct {
	OutTradeNo  string         `json:"out_trade_no"`
	AmountCents int            `json:"amount_cents"`
	Amount      string         `json:"amount"`
	Subject     string         `json:"subject"`
	Method      string         `json:"method"`
	ClientIP    string         `json:"client_ip"`
	NotifyURL   string         `json:"notify_url"`
	ReturnURL   string         `json:"return_url"`
	Config      map[string]any `json:"config"`
}

type notifyRequest struct {
	Params map[string]string `json:"params"`
	Config map[string]any    `json:"config"`
}

func main() {
	raw, err := io.ReadAll(os.Stdin)
	if err != nil {
		writeError("", -32700, err.Error())
		return
	}
	var req rpcRequest
	if err := json.Unmarshal(raw, &req); err != nil {
		writeError("", -32700, err.Error())
		return
	}
	switch req.Method {
	case "payment.create":
		var params createRequest
		if err := json.Unmarshal(req.Params, &params); err != nil {
			writeError(req.ID, -32602, err.Error())
			return
		}
		result, err := createPayment(params)
		if err != nil {
			writeError(req.ID, -32000, err.Error())
			return
		}
		writeOK(req.ID, result)
	case "payment.notify":
		var params notifyRequest
		if err := json.Unmarshal(req.Params, &params); err != nil {
			writeError(req.ID, -32602, err.Error())
			return
		}
		result, err := handleNotify(params)
		if err != nil {
			writeError(req.ID, -32000, err.Error())
			return
		}
		writeOK(req.ID, result)
	default:
		writeError(req.ID, -32601, "method not found")
	}
}

func createPayment(req createRequest) (map[string]any, error) {
	apiURL := strings.TrimRight(configString(req.Config, "api_url"), "/")
	pid := configString(req.Config, "pid")
	key := configString(req.Config, "key")
	if apiURL == "" || pid == "" || key == "" {
		return nil, fmt.Errorf("missing epay api_url/pid/key")
	}
	method := strings.TrimSpace(req.Method)
	if method == "" {
		method = strings.TrimPrefix(configString(req.Config, "plugin_id"), "epay_")
	}
	if method == "" {
		method = "alipay"
	}
	money := req.Amount
	if money == "" && req.AmountCents > 0 {
		money = fmt.Sprintf("%.2f", float64(req.AmountCents)/100)
	}
	params := map[string]string{
		"pid":          pid,
		"type":         method,
		"out_trade_no": req.OutTradeNo,
		"notify_url":   req.NotifyURL,
		"return_url":   req.ReturnURL,
		"name":         defaultString(req.Subject, "Balance recharge"),
		"money":        money,
		"clientip":     req.ClientIP,
		"device":       "pc",
	}
	params["sign"] = epaySign(params, key)
	params["sign_type"] = "MD5"
	resp, err := http.PostForm(apiURL+"/xpay/epay/mapi.php", mapToValues(params))
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	raw, _ := io.ReadAll(resp.Body)
	var body map[string]any
	if err := json.Unmarshal(raw, &body); err != nil {
		return map[string]any{"ok": true, "raw": map[string]any{"text": string(raw)}}, nil
	}
	result := map[string]any{"ok": true, "raw": body}
	for _, key := range []string{"trade_no", "payurl", "qrcode", "urlscheme"} {
		if value := strings.TrimSpace(fmt.Sprint(body[key])); value != "" && value != "<nil>" {
			result[key] = value
		}
	}
	return result, nil
}

func handleNotify(req notifyRequest) (map[string]any, error) {
	key := configString(req.Config, "key")
	if key == "" {
		return nil, fmt.Errorf("missing epay key")
	}
	if !strings.EqualFold(req.Params["sign"], epaySign(req.Params, key)) {
		return nil, fmt.Errorf("invalid signature")
	}
	status := strings.ToUpper(strings.TrimSpace(firstNonEmpty(req.Params["trade_status"], req.Params["status"])))
	if status != "" && status != "TRADE_SUCCESS" && status != "SUCCESS" && status != "PAID" {
		return map[string]any{"ok": true, "ignored": true, "message": "trade status ignored"}, nil
	}
	amountCents := 0
	if money := strings.TrimSpace(req.Params["money"]); money != "" {
		if parsed, err := strconv.ParseFloat(money, 64); err == nil {
			amountCents = int(parsed*100 + 0.5)
		}
	}
	return map[string]any{
		"ok":           true,
		"paid":         true,
		"out_trade_no": req.Params["out_trade_no"],
		"trade_no":     req.Params["trade_no"],
		"amount_cents": amountCents,
		"raw":          req.Params,
	}, nil
}

func epaySign(params map[string]string, key string) string {
	keys := make([]string, 0, len(params))
	for k, v := range params {
		if v == "" || k == "sign" || k == "sign_type" {
			continue
		}
		keys = append(keys, k)
	}
	sort.Strings(keys)
	parts := make([]string, 0, len(keys))
	for _, k := range keys {
		parts = append(parts, k+"="+params[k])
	}
	sum := md5.Sum([]byte(strings.Join(parts, "&") + key))
	return hex.EncodeToString(sum[:])
}

func mapToValues(params map[string]string) url.Values {
	values := url.Values{}
	for key, value := range params {
		values.Set(key, value)
	}
	return values
}

func configString(config map[string]any, key string) string {
	if config == nil {
		return ""
	}
	return strings.TrimSpace(fmt.Sprint(config[key]))
}

func defaultString(value string, fallback string) string {
	if strings.TrimSpace(value) == "" {
		return fallback
	}
	return value
}

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

func writeOK(id string, result any) {
	_ = json.NewEncoder(os.Stdout).Encode(rpcResponse{JSONRPC: "2.0", ID: id, Result: result})
}

func writeError(id string, code int, message string) {
	_ = json.NewEncoder(os.Stdout).Encode(rpcResponse{JSONRPC: "2.0", ID: id, Error: &rpcError{Code: code, Message: message}})
}
