package mifapay

import (
	"bytes"
	"context"
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"
	"time"

	"hyapp/pkg/xerr"
	"hyapp/services/wallet-service/internal/config"
	"hyapp/services/wallet-service/internal/domain/ledger"
	walletservice "hyapp/services/wallet-service/internal/service/wallet"
)

type Client struct {
	cfg        config.MifaPayConfig
	httpClient *http.Client
	privateKey *rsa.PrivateKey
	publicKey  *rsa.PublicKey
}

type envelope struct {
	MerAccount string `json:"merAccount"`
	Data       string `json:"data"`
	Sign       string `json:"sign"`
}

func New(cfg config.MifaPayConfig) (*Client, error) {
	privateKey, err := parsePrivateKey(cfg.PrivateKey)
	if err != nil {
		return nil, err
	}
	publicKey, err := parsePublicKey(cfg.PlatformPublicKey)
	if err != nil {
		return nil, err
	}
	if cfg.HTTPTimeout <= 0 {
		cfg.HTTPTimeout = 10 * time.Second
	}
	return &Client{
		cfg:        cfg,
		httpClient: &http.Client{Timeout: cfg.HTTPTimeout},
		privateKey: privateKey,
		publicKey:  publicKey,
	}, nil
}

// CreateOrder 按 MiFaPay 统一 envelope 协议下单；业务层只消费 payUrl 和 mbOrderId。
func (c *Client) CreateOrder(ctx context.Context, req walletservice.MifaPayCreateOrderRequest) (walletservice.MifaPayCreateOrderResponse, error) {
	if c == nil || c.privateKey == nil {
		return walletservice.MifaPayCreateOrderResponse{}, xerr.New(xerr.Unavailable, "mifapay client is not configured")
	}
	data := map[string]any{
		"amount":       strconv.FormatInt(req.ProviderAmountMinor, 10),
		"orderId":      req.OrderID,
		"payWay":       req.PayWay,
		"language":     firstNonEmpty(req.Language, "en"),
		"terminalType": "WEB",
		"productInfo":  productInfo(req),
		"payType":      req.PayType,
		"merNo":        c.cfg.MerNo,
		"countryCode":  req.CountryCode,
		"notifyUrl":    req.NotifyURL,
		"currency":     req.CurrencyCode,
		"time":         time.Now().Unix(),
		"returnUrl":    req.ReturnURL,
		"memberId":     strconv.FormatInt(req.TargetUserID, 10),
		"userIp":       req.ClientIP,
	}
	body, err := c.signedEnvelope(data)
	if err != nil {
		return walletservice.MifaPayCreateOrderResponse{}, err
	}
	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.cfg.APIBaseURL+"/paygateway/mbpay/order/en_v2", bytes.NewReader(body))
	if err != nil {
		return walletservice.MifaPayCreateOrderResponse{}, err
	}
	httpReq.Header.Set("Content-Type", "application/json; charset=UTF-8")
	resp, err := c.httpClient.Do(httpReq)
	if err != nil {
		return walletservice.MifaPayCreateOrderResponse{}, err
	}
	defer resp.Body.Close()
	raw, err := io.ReadAll(io.LimitReader(resp.Body, 2<<20))
	if err != nil {
		return walletservice.MifaPayCreateOrderResponse{}, err
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return walletservice.MifaPayCreateOrderResponse{}, xerr.New(xerr.Unavailable, "mifapay order request failed")
	}
	var outer envelope
	var withCode struct {
		envelope
		Code string `json:"code"`
		Msg  string `json:"msg"`
	}
	if err := json.Unmarshal(raw, &withCode); err != nil {
		return walletservice.MifaPayCreateOrderResponse{}, err
	}
	outer = withCode.envelope
	if withCode.Code != "" && withCode.Code != "000000" {
		return walletservice.MifaPayCreateOrderResponse{}, xerr.New(xerr.Conflict, "mifapay order rejected: "+withCode.Msg)
	}
	if err := c.verify(outer.Data, outer.Sign); err != nil {
		return walletservice.MifaPayCreateOrderResponse{}, err
	}
	var inner struct {
		OrderID   string `json:"orderId"`
		PayURL    string `json:"payUrl"`
		MBOrderID string `json:"mbOrderId"`
	}
	if err := json.Unmarshal([]byte(outer.Data), &inner); err != nil {
		return walletservice.MifaPayCreateOrderResponse{}, err
	}
	if strings.TrimSpace(inner.PayURL) == "" || strings.TrimSpace(inner.MBOrderID) == "" {
		return walletservice.MifaPayCreateOrderResponse{}, xerr.New(xerr.Unavailable, "mifapay order response is incomplete")
	}
	return walletservice.MifaPayCreateOrderResponse{
		OrderID:         inner.OrderID,
		ProviderOrderID: inner.MBOrderID,
		PayURL:          inner.PayURL,
		RawJSON:         string(raw),
	}, nil
}

// ParseNotification 验签 MiFaPay 回调并返回内层业务字段；验签失败不允许业务层入账。
func (c *Client) ParseNotification(notification ledger.MifaPayNotification) (walletservice.MifaPayNotifyData, error) {
	if c == nil || c.publicKey == nil {
		return walletservice.MifaPayNotifyData{}, xerr.New(xerr.Unavailable, "mifapay client is not configured")
	}
	if strings.TrimSpace(notification.MerAccount) != c.cfg.MerAccount {
		return walletservice.MifaPayNotifyData{}, xerr.New(xerr.PermissionDenied, "mifapay mer_account is invalid")
	}
	if err := c.verify(notification.Data, notification.Sign); err != nil {
		return walletservice.MifaPayNotifyData{}, err
	}
	var inner struct {
		Amount      string `json:"amount"`
		PayType     string `json:"payType"`
		OrderID     string `json:"orderId"`
		BankOrderNo string `json:"bankOrderNo"`
		OrderStatus string `json:"orderStatus"`
		Currency    string `json:"currency"`
		MBOrderID   string `json:"mbOrderId"`
		MemberID    string `json:"memberId"`
	}
	if err := json.Unmarshal([]byte(notification.Data), &inner); err != nil {
		return walletservice.MifaPayNotifyData{}, err
	}
	if strings.TrimSpace(inner.OrderID) == "" {
		return walletservice.MifaPayNotifyData{}, xerr.New(xerr.InvalidArgument, "mifapay callback order_id is required")
	}
	return walletservice.MifaPayNotifyData{
		OrderID:         inner.OrderID,
		ProviderOrderID: inner.MBOrderID,
		OrderStatus:     inner.OrderStatus,
		Amount:          inner.Amount,
		Currency:        inner.Currency,
		PayType:         inner.PayType,
		RawJSON:         notification.Data,
	}, nil
}

func (c *Client) signedEnvelope(data any) ([]byte, error) {
	dataJSON, err := json.Marshal(data)
	if err != nil {
		return nil, err
	}
	sign, err := c.sign(string(dataJSON))
	if err != nil {
		return nil, err
	}
	return json.Marshal(envelope{MerAccount: c.cfg.MerAccount, Data: string(dataJSON), Sign: sign})
}

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

func (c *Client) verify(data string, sign string) error {
	if c.publicKey == nil {
		return nil
	}
	signature, err := base64.StdEncoding.DecodeString(strings.TrimSpace(sign))
	if err != nil {
		return err
	}
	hash := sha256.Sum256([]byte(data))
	if err := rsa.VerifyPKCS1v15(c.publicKey, crypto.SHA256, hash[:], signature); err != nil {
		return xerr.New(xerr.PermissionDenied, "mifapay signature is invalid")
	}
	return nil
}

func parsePrivateKey(value string) (*rsa.PrivateKey, error) {
	block := pemBlock(value, "PRIVATE KEY")
	key, err := x509.ParsePKCS8PrivateKey(block)
	if err != nil {
		return nil, err
	}
	privateKey, ok := key.(*rsa.PrivateKey)
	if !ok {
		return nil, fmt.Errorf("mifapay private key is not rsa")
	}
	return privateKey, nil
}

func parsePublicKey(value string) (*rsa.PublicKey, error) {
	block := pemBlock(value, "PUBLIC KEY")
	key, err := x509.ParsePKIXPublicKey(block)
	if err != nil {
		return nil, err
	}
	publicKey, ok := key.(*rsa.PublicKey)
	if !ok {
		return nil, fmt.Errorf("mifapay public key is not rsa")
	}
	return publicKey, nil
}

func pemBlock(value string, blockType string) []byte {
	value = strings.TrimSpace(value)
	if decoded, _ := pem.Decode([]byte(value)); decoded != nil {
		return decoded.Bytes
	}
	clean := strings.NewReplacer("\n", "", "\r", "", " ", "").Replace(value)
	body, err := base64.StdEncoding.DecodeString(clean)
	if err == nil {
		return body
	}
	return []byte(value)
}

func productInfo(req walletservice.MifaPayCreateOrderRequest) string {
	body, _ := json.Marshal([]map[string]string{{
		"quantity":    "1",
		"price":       strconv.FormatInt(req.AmountMinor, 10),
		"sku":         req.OrderID,
		"productName": req.ProductName,
	}})
	return string(body)
}

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