// Package alipay provides a small Alipay OpenAPI client for AgentTeam billing.
//
// The client supports two payment initiation modes:
//   - page pay: generate a signed alipay.trade.page.pay URL for redirecting
//     the browser to Alipay.
//   - precreate: call alipay.trade.precreate and return a QR code URL that can
//     be rendered in AgentTeam without navigating away.
package alipay

import (
	"context"
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"encoding/pem"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"sort"
	"strings"
	"time"
)

const (
	alipayCharset  = "utf-8"
	alipaySignType = "RSA2"
	alipayVersion  = "1.0"

	alipayMethodPagePay   = "alipay.trade.page.pay"
	alipayMethodPrecreate = "alipay.trade.precreate"
	alipayMethodQuery     = "alipay.trade.query"
	alipayMethodClose     = "alipay.trade.close"
)

var (
	// ErrNotConfigured is returned when required Alipay credentials or gateway
	// settings are missing.
	ErrNotConfigured = errors.New("alipay is not configured")
)

// Config contains the Alipay OpenAPI settings needed by the client.
type Config struct {
	AppID           string
	AppPrivateKey   string
	AlipayPublicKey string
	GatewayURL      string
}

// Client signs Alipay OpenAPI requests and verifies Alipay signatures.
type Client struct {
	config     Config
	httpClient *http.Client
	now        func() time.Time
}

// ConfigError reports missing Alipay configuration.
type ConfigError struct {
	missing []string
}

func (err ConfigError) Error() string {
	return "Alipay is not configured: " + strings.Join(err.missing, ", ")
}

func (err ConfigError) Unwrap() error {
	return ErrNotConfigured
}

// KeyError reports invalid Alipay key material.
type KeyError struct {
	message string
	cause   error
}

func (err KeyError) Error() string {
	return err.message
}

func (err KeyError) Unwrap() error {
	return err.cause
}

// PagePayInput describes an Alipay PC web payment request.
type PagePayInput struct {
	OutTradeNo     string
	Subject        string
	Amount         string
	Body           string
	TimeoutExpress string
	ReturnURL      string
	NotifyURL      string
}

// PrecreateInput describes an Alipay QR-code payment request.
type PrecreateInput struct {
	OutTradeNo string
	Subject    string
	Amount     string
	Body       string
	NotifyURL  string
}

// PrecreateResult contains the QR code URL returned by
// alipay.trade.precreate.
type PrecreateResult struct {
	OutTradeNo string
	QRCode     string
	Code       string
	Message    string
}

// QueryResult contains the status fields returned by alipay.trade.query.
type QueryResult struct {
	OutTradeNo       string
	TradeNo          string
	TradeStatus      string
	TotalAmount      string
	SendPayDate      string
	RawPayloadSHA256 string
	Code             string
	SubCode          string
	SubMessage       string
	Message          string
}

// CloseResult contains the status fields returned by alipay.trade.close.
type CloseResult struct {
	OutTradeNo string
	TradeNo    string
	Code       string
	Message    string
}

// NewClient creates an Alipay OpenAPI client.
func NewClient(config Config, httpClient *http.Client) *Client {
	if httpClient == nil {
		httpClient = http.DefaultClient
	}
	return &Client{
		config:     config,
		httpClient: httpClient,
		now:        time.Now,
	}
}

// CreatePagePayURL returns a signed Alipay PC web payment URL.
func (client *Client) CreatePagePayURL(input PagePayInput) (string, error) {
	if err := client.requireConfigured(true); err != nil {
		return "", err
	}

	bizPayload := map[string]string{
		"out_trade_no": input.OutTradeNo,
		"product_code": "FAST_INSTANT_TRADE_PAY",
		"total_amount": input.Amount,
		"subject":      input.Subject,
		"body":         input.Body,
	}
	if trimmed := strings.TrimSpace(input.TimeoutExpress); trimmed != "" {
		bizPayload["timeout_express"] = trimmed
	}
	bizContent, err := json.Marshal(bizPayload)
	if err != nil {
		return "", fmt.Errorf("build Alipay page-pay payload: %w", err)
	}

	params := client.commonParams(alipayMethodPagePay)
	params["biz_content"] = string(bizContent)
	if trimmed := strings.TrimSpace(input.ReturnURL); trimmed != "" {
		params["return_url"] = trimmed
	}
	if trimmed := strings.TrimSpace(input.NotifyURL); trimmed != "" {
		params["notify_url"] = trimmed
	}

	if err := client.signParams(params); err != nil {
		return "", err
	}

	gateway, err := url.Parse(strings.TrimSpace(client.config.GatewayURL))
	if err != nil {
		return "", fmt.Errorf("parse Alipay gateway URL: %w", err)
	}
	query := gateway.Query()
	for key, value := range params {
		query.Set(key, value)
	}
	gateway.RawQuery = query.Encode()
	return gateway.String(), nil
}

// CreatePrecreateOrder calls alipay.trade.precreate and returns the QR code
// URL that should be rendered for scan-to-pay.
func (client *Client) CreatePrecreateOrder(ctx context.Context, input PrecreateInput) (PrecreateResult, error) {
	if err := client.requireConfigured(true); err != nil {
		return PrecreateResult{}, err
	}

	bizContent, err := json.Marshal(map[string]string{
		"out_trade_no": input.OutTradeNo,
		"total_amount": input.Amount,
		"subject":      input.Subject,
		"body":         input.Body,
	})
	if err != nil {
		return PrecreateResult{}, fmt.Errorf("build Alipay precreate payload: %w", err)
	}

	params := client.commonParams(alipayMethodPrecreate)
	params["biz_content"] = string(bizContent)
	if trimmed := strings.TrimSpace(input.NotifyURL); trimmed != "" {
		params["notify_url"] = trimmed
	}
	if err := client.signParams(params); err != nil {
		return PrecreateResult{}, err
	}

	body, err := client.postSignedForm(ctx, params, "precreate")
	if err != nil {
		return PrecreateResult{}, err
	}
	return client.parsePrecreateResponse(body)
}

// QueryTrade calls alipay.trade.query for the given merchant order number.
func (client *Client) QueryTrade(ctx context.Context, outTradeNo string) (QueryResult, error) {
	if err := client.requireConfigured(true); err != nil {
		return QueryResult{}, err
	}

	bizContent, err := json.Marshal(map[string]string{"out_trade_no": outTradeNo})
	if err != nil {
		return QueryResult{}, fmt.Errorf("build Alipay query payload: %w", err)
	}

	params := client.commonParams(alipayMethodQuery)
	params["biz_content"] = string(bizContent)
	if err := client.signParams(params); err != nil {
		return QueryResult{}, err
	}

	body, err := client.postSignedForm(ctx, params, "query")
	if err != nil {
		return QueryResult{}, err
	}
	return client.parseQueryResponse(body)
}

// CloseTrade calls alipay.trade.close for the given merchant order number.
func (client *Client) CloseTrade(ctx context.Context, outTradeNo string) (CloseResult, error) {
	if err := client.requireConfigured(true); err != nil {
		return CloseResult{}, err
	}

	bizContent, err := json.Marshal(map[string]string{"out_trade_no": outTradeNo})
	if err != nil {
		return CloseResult{}, fmt.Errorf("build Alipay close payload: %w", err)
	}

	params := client.commonParams(alipayMethodClose)
	params["biz_content"] = string(bizContent)
	if err := client.signParams(params); err != nil {
		return CloseResult{}, err
	}

	body, err := client.postSignedForm(ctx, params, "close")
	if err != nil {
		return CloseResult{}, err
	}
	return client.parseCloseResponse(body)
}

func (client *Client) postSignedForm(ctx context.Context, params map[string]string, operation string) ([]byte, error) {
	form := url.Values{}
	for key, value := range params {
		form.Set(key, value)
	}
	request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimSpace(client.config.GatewayURL), strings.NewReader(form.Encode()))
	if err != nil {
		return nil, fmt.Errorf("build Alipay %s request: %w", operation, err)
	}
	request.Header.Set("Accept", "application/json")
	request.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=utf-8")

	response, err := client.httpClient.Do(request)
	if err != nil {
		return nil, fmt.Errorf("call Alipay %s API: %w", operation, err)
	}
	defer response.Body.Close()

	body, err := io.ReadAll(io.LimitReader(response.Body, 1<<20))
	if err != nil {
		return nil, fmt.Errorf("read Alipay %s response: %w", operation, err)
	}
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		return nil, fmt.Errorf("Alipay %s API returned HTTP %d: %s", operation, response.StatusCode, strings.TrimSpace(string(body)))
	}
	return body, nil
}

// VerifyNotify verifies an Alipay asynchronous notification form signature.
func (client *Client) VerifyNotify(form url.Values) error {
	if err := client.requireConfigured(false); err != nil {
		return err
	}
	signature := strings.TrimSpace(form.Get("sign"))
	if signature == "" {
		return fmt.Errorf("Alipay notify signature is missing")
	}
	content := canonicalAlipayValues(form)
	if err := client.verifySignature([]byte(content), signature); err != nil {
		return err
	}
	if strings.TrimSpace(form.Get("app_id")) != strings.TrimSpace(client.config.AppID) {
		return fmt.Errorf("Alipay notify app_id does not match")
	}
	return nil
}

func (client *Client) commonParams(method string) map[string]string {
	now := time.Now
	if client.now != nil {
		now = client.now
	}
	return map[string]string{
		"app_id":    strings.TrimSpace(client.config.AppID),
		"method":    method,
		"charset":   alipayCharset,
		"sign_type": alipaySignType,
		"timestamp": now().Format("2006-01-02 15:04:05"),
		"version":   alipayVersion,
	}
}

func (client *Client) requireConfigured(requirePrivateKey bool) error {
	missing := make([]string, 0)
	if strings.TrimSpace(client.config.AppID) == "" {
		missing = append(missing, "AGENTTEAM_ALIPAY_APP_ID")
	}
	if strings.TrimSpace(client.config.GatewayURL) == "" {
		missing = append(missing, "AGENTTEAM_ALIPAY_GATEWAY_URL")
	}
	if requirePrivateKey && strings.TrimSpace(client.config.AppPrivateKey) == "" {
		missing = append(missing, "AGENTTEAM_ALIPAY_APP_PRIVATE_KEY")
	}
	if strings.TrimSpace(client.config.AlipayPublicKey) == "" {
		missing = append(missing, "AGENTTEAM_ALIPAY_PUBLIC_KEY")
	}
	if len(missing) > 0 {
		return ConfigError{missing: missing}
	}
	return nil
}

func (client *Client) signParams(params map[string]string) error {
	privateKey, err := parseRSAPrivateKey(client.config.AppPrivateKey)
	if err != nil {
		return KeyError{message: "Invalid AGENTTEAM_ALIPAY_APP_PRIVATE_KEY", cause: err}
	}
	content := canonicalAlipayParams(params)
	signature, err := signRSA2(privateKey, []byte(content))
	if err != nil {
		return fmt.Errorf("sign Alipay request: %w", err)
	}
	params["sign"] = signature
	return nil
}

func (client *Client) parsePrecreateResponse(body []byte) (PrecreateResult, error) {
	var envelope map[string]json.RawMessage
	if err := json.Unmarshal(body, &envelope); err != nil {
		return PrecreateResult{}, fmt.Errorf("decode Alipay precreate response: %w", err)
	}
	responseRaw, ok := envelope["alipay_trade_precreate_response"]
	if !ok {
		return PrecreateResult{}, fmt.Errorf("Alipay precreate response is missing alipay_trade_precreate_response")
	}

	var signature string
	if rawSignature, ok := envelope["sign"]; ok {
		_ = json.Unmarshal(rawSignature, &signature)
	}
	if strings.TrimSpace(signature) == "" {
		return PrecreateResult{}, fmt.Errorf("Alipay precreate response signature is missing")
	}
	if err := client.verifySignature(responseRaw, signature); err != nil {
		return PrecreateResult{}, err
	}

	var payload struct {
		Code       string `json:"code"`
		Message    string `json:"msg"`
		SubCode    string `json:"sub_code"`
		SubMessage string `json:"sub_msg"`
		OutTradeNo string `json:"out_trade_no"`
		QRCode     string `json:"qr_code"`
	}
	if err := json.Unmarshal(responseRaw, &payload); err != nil {
		return PrecreateResult{}, fmt.Errorf("decode Alipay precreate payload: %w", err)
	}
	return PrecreateResult{
		OutTradeNo: strings.TrimSpace(payload.OutTradeNo),
		QRCode:     strings.TrimSpace(payload.QRCode),
		Code:       strings.TrimSpace(payload.Code),
		Message:    alipayMessage(payload.Message, payload.SubMessage, payload.SubCode),
	}, nil
}

func (client *Client) parseQueryResponse(body []byte) (QueryResult, error) {
	var envelope map[string]json.RawMessage
	if err := json.Unmarshal(body, &envelope); err != nil {
		return QueryResult{}, fmt.Errorf("decode Alipay query response: %w", err)
	}
	responseRaw, ok := envelope["alipay_trade_query_response"]
	if !ok {
		return QueryResult{}, fmt.Errorf("Alipay query response is missing alipay_trade_query_response")
	}

	var signature string
	if rawSignature, ok := envelope["sign"]; ok {
		_ = json.Unmarshal(rawSignature, &signature)
	}
	if strings.TrimSpace(signature) == "" {
		return QueryResult{}, fmt.Errorf("Alipay query response signature is missing")
	}
	if err := client.verifySignature(responseRaw, signature); err != nil {
		return QueryResult{}, err
	}

	var payload struct {
		Code        string `json:"code"`
		Message     string `json:"msg"`
		SubCode     string `json:"sub_code"`
		SubMessage  string `json:"sub_msg"`
		OutTradeNo  string `json:"out_trade_no"`
		TradeNo     string `json:"trade_no"`
		TradeStatus string `json:"trade_status"`
		TotalAmount string `json:"total_amount"`
		SendPayDate string `json:"send_pay_date"`
	}
	if err := json.Unmarshal(responseRaw, &payload); err != nil {
		return QueryResult{}, fmt.Errorf("decode Alipay query payload: %w", err)
	}
	bodyHash := sha256.Sum256(body)
	return QueryResult{
		OutTradeNo:       firstPresent(payload.OutTradeNo, ""),
		TradeNo:          strings.TrimSpace(payload.TradeNo),
		TradeStatus:      strings.TrimSpace(payload.TradeStatus),
		TotalAmount:      strings.TrimSpace(payload.TotalAmount),
		SendPayDate:      strings.TrimSpace(payload.SendPayDate),
		RawPayloadSHA256: hex.EncodeToString(bodyHash[:]),
		Code:             strings.TrimSpace(payload.Code),
		SubCode:          strings.TrimSpace(payload.SubCode),
		SubMessage:       strings.TrimSpace(payload.SubMessage),
		Message:          alipayMessage(payload.Message, payload.SubMessage, payload.SubCode),
	}, nil
}

func (client *Client) parseCloseResponse(body []byte) (CloseResult, error) {
	var envelope map[string]json.RawMessage
	if err := json.Unmarshal(body, &envelope); err != nil {
		return CloseResult{}, fmt.Errorf("decode Alipay close response: %w", err)
	}
	responseRaw, ok := envelope["alipay_trade_close_response"]
	if !ok {
		return CloseResult{}, fmt.Errorf("Alipay close response is missing alipay_trade_close_response")
	}

	var signature string
	if rawSignature, ok := envelope["sign"]; ok {
		_ = json.Unmarshal(rawSignature, &signature)
	}
	if strings.TrimSpace(signature) == "" {
		return CloseResult{}, fmt.Errorf("Alipay close response signature is missing")
	}
	if err := client.verifySignature(responseRaw, signature); err != nil {
		return CloseResult{}, err
	}

	var payload struct {
		Code       string `json:"code"`
		Message    string `json:"msg"`
		SubCode    string `json:"sub_code"`
		SubMessage string `json:"sub_msg"`
		OutTradeNo string `json:"out_trade_no"`
		TradeNo    string `json:"trade_no"`
	}
	if err := json.Unmarshal(responseRaw, &payload); err != nil {
		return CloseResult{}, fmt.Errorf("decode Alipay close payload: %w", err)
	}
	return CloseResult{
		OutTradeNo: strings.TrimSpace(payload.OutTradeNo),
		TradeNo:    strings.TrimSpace(payload.TradeNo),
		Code:       strings.TrimSpace(payload.Code),
		Message:    alipayMessage(payload.Message, payload.SubMessage, payload.SubCode),
	}, nil
}

func alipayMessage(message string, subMessage string, subCode string) string {
	result := strings.TrimSpace(message)
	if trimmedSubMessage := strings.TrimSpace(subMessage); trimmedSubMessage != "" {
		if result != "" {
			result += ": "
		}
		result += trimmedSubMessage
	}
	if code := strings.TrimSpace(subCode); code != "" {
		if result != "" {
			result += " "
		}
		result += "(" + code + ")"
	}
	return result
}

func (client *Client) verifySignature(content []byte, signature string) error {
	publicKey, err := parseRSAPublicKey(client.config.AlipayPublicKey)
	if err != nil {
		return KeyError{message: "Invalid AGENTTEAM_ALIPAY_PUBLIC_KEY", cause: err}
	}
	decodedSignature, err := base64.StdEncoding.DecodeString(strings.TrimSpace(signature))
	if err != nil {
		return fmt.Errorf("decode Alipay signature: %w", err)
	}
	hash := sha256.Sum256(content)
	if err := rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hash[:], decodedSignature); err != nil {
		return fmt.Errorf("verify Alipay signature: %w", err)
	}
	return nil
}

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

	parts := make([]string, 0, len(keys))
	for _, key := range keys {
		parts = append(parts, key+"="+params[key])
	}
	return strings.Join(parts, "&")
}

func canonicalAlipayValues(values url.Values) string {
	keys := make([]string, 0, len(values))
	for key, list := range values {
		if key == "sign" || key == "sign_type" || len(list) == 0 || strings.TrimSpace(list[0]) == "" {
			continue
		}
		keys = append(keys, key)
	}
	sort.Strings(keys)

	parts := make([]string, 0, len(keys))
	for _, key := range keys {
		parts = append(parts, key+"="+values.Get(key))
	}
	return strings.Join(parts, "&")
}

func signRSA2(privateKey *rsa.PrivateKey, content []byte) (string, error) {
	hash := sha256.Sum256(content)
	signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hash[:])
	if err != nil {
		return "", err
	}
	return base64.StdEncoding.EncodeToString(signature), nil
}

func parseRSAPrivateKey(raw string) (*rsa.PrivateKey, error) {
	der, err := decodeKeyMaterial(raw)
	if err != nil {
		return nil, err
	}
	if parsed, err := x509.ParsePKCS8PrivateKey(der); err == nil {
		privateKey, ok := parsed.(*rsa.PrivateKey)
		if !ok {
			return nil, fmt.Errorf("private key is not RSA")
		}
		return privateKey, nil
	}
	return x509.ParsePKCS1PrivateKey(der)
}

func parseRSAPublicKey(raw string) (*rsa.PublicKey, error) {
	der, err := decodeKeyMaterial(raw)
	if err != nil {
		return nil, err
	}
	if parsed, err := x509.ParsePKIXPublicKey(der); err == nil {
		publicKey, ok := parsed.(*rsa.PublicKey)
		if !ok {
			return nil, fmt.Errorf("public key is not RSA")
		}
		return publicKey, nil
	}
	return x509.ParsePKCS1PublicKey(der)
}

func decodeKeyMaterial(raw string) ([]byte, error) {
	normalized := strings.TrimSpace(strings.ReplaceAll(raw, `\n`, "\n"))
	if normalized == "" {
		return nil, fmt.Errorf("key is empty")
	}
	if block, _ := pem.Decode([]byte(normalized)); block != nil {
		return block.Bytes, nil
	}
	compact := strings.Join(strings.Fields(normalized), "")
	return base64.StdEncoding.DecodeString(compact)
}

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