package binance

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"hyapp/pkg/xerr"
	"hyapp/services/wallet-service/internal/config"
	"hyapp/services/wallet-service/internal/service/wallet/ports"
)

const (
	defaultDepositLookback = 90 * 24 * time.Hour
	depositHistoryLimit    = 1000
	defaultPayLookback     = 90 * 24 * time.Hour
	payOrderTimeRadius     = time.Hour
	payHistoryLimit        = 100
	binancePaySuccessCode  = "000000"
	c2cTradeTypeBuy        = "BUY"
	c2cOrderStatusComplete = "COMPLETED"
)

// Client 是 Binance 只读查单适配器；它只使用 signed read API，不保存、不生成任何链上私钥。
type Client struct {
	httpClient *http.Client
	apiBaseURL string
	recvWindow int64
	accounts   map[string]account
}

type account struct {
	apiKey       string
	apiSecretKey string
}

// New 从 wallet-service 配置创建 Binance client；具体 App 是否有账号在请求时按 appCode 决定。
func New(cfg config.BinanceConfig) *Client {
	timeout := cfg.HTTPTimeout
	if timeout <= 0 {
		timeout = 10 * time.Second
	}
	// 适配器只复制规范化租户注册表；新增 App 账号由环境配置扩展，不能在客户端再维护产品名映射。
	accounts := make(map[string]account, len(cfg.Accounts))
	for appCode, configured := range cfg.Accounts {
		appCode = strings.ToLower(strings.TrimSpace(appCode))
		if appCode != "" {
			accounts[appCode] = newAccount(configured)
		}
	}
	return &Client{
		httpClient: &http.Client{Timeout: timeout},
		apiBaseURL: strings.TrimRight(strings.TrimSpace(firstNonEmpty(cfg.APIBaseURL, "https://api.binance.com")), "/"),
		recvWindow: firstPositiveInt64(cfg.RecvWindow, 10000),
		accounts:   accounts,
	}
}

func newAccount(cfg config.BinanceAccountConfig) account {
	return account{
		apiKey:       strings.TrimSpace(cfg.APIKey),
		apiSecretKey: strings.TrimSpace(cfg.APISecretKey),
	}
}

// FindUSDTTransfer 按显式来源查 Binance 收款事实：C2C 只查 Pay，OFFCHAIN 只查 Deposit History；
// 历史 TRX/BSC 调用仍保留 Deposit 未命中后的 Pay 兼容路径。链上/站内入金必须校验网络、地址和状态，Pay C2C 必须证明是当前 API Key 账户正向收款。
func (c *Client) FindUSDTTransfer(ctx context.Context, req ports.BinanceTransferLookupRequest) (ports.BinanceTransferRecord, error) {
	if c == nil || c.apiBaseURL == "" {
		return ports.BinanceTransferRecord{}, xerr.New(xerr.Unavailable, "binance client is not configured")
	}
	account, ok := c.accountForApp(req.AppCode)
	if !ok {
		return ports.BinanceTransferRecord{}, xerr.New(xerr.Unavailable, "binance account is not configured")
	}
	req.ExternalID = strings.TrimSpace(req.ExternalID)
	req.Coin = strings.ToUpper(strings.TrimSpace(req.Coin))
	req.Network = strings.ToUpper(strings.TrimSpace(req.Network))
	req.ToAddress = strings.TrimSpace(req.ToAddress)
	if req.ExternalID == "" || req.Coin == "" || req.Network == "" || req.AmountMinor <= 0 {
		return ports.BinanceTransferRecord{}, xerr.New(xerr.InvalidArgument, "binance transfer verification request is incomplete")
	}
	serverTimeMS, err := c.serverTime(ctx)
	if err != nil {
		return ports.BinanceTransferRecord{}, err
	}
	if req.Network == "C2C" {
		// 后台的 C2C 对应 Binance Pay 账单中的 orderType=C2C，而不是法币 P2P Trade History。
		// 显式 C2C 只查 Pay，且允许收款币种为 USDT 或 USDC；不能再回退到链上或 P2P 历史。
		// 运营录单时间可能是补单操作时间而非 Binance 成交时间；显式订单号查验使用有界 90 天窗口，避免错误窄窗漏单。
		startTimeMS, endTimeMS := maxInt64(0, serverTimeMS-int64(defaultPayLookback/time.Millisecond)), serverTimeMS
		records, err := c.payHistory(ctx, account, startTimeMS, endTimeMS, serverTimeMS)
		if err != nil {
			return ports.BinanceTransferRecord{}, err
		}
		return matchExplicitC2CPayRecord(req, records)
	}
	// Binance 的 deposit history 查询窗口有限；后台凭证校验只做近期有界查询，避免一次手工校验变成无界扫描。
	records, err := c.depositHistory(ctx, account, req.Coin, serverTimeMS-int64(defaultDepositLookback/time.Millisecond), serverTimeMS)
	if err != nil {
		return ports.BinanceTransferRecord{}, err
	}
	if req.Network == "OFFCHAIN" {
		// 显式 Off-chain 只接受 Deposit History 中带该前缀的 TRX 站内转账；不能回退到 Pay，
		// 否则同一个纯数字编号可能跨事实源误绑定另一笔 C2C 收款。
		return matchOffchainDepositRecord(req, records)
	}
	matched, err := matchDepositRecord(req, records)
	if err == nil {
		return matched, nil
	}
	if !xerr.IsCode(err, xerr.NotFound) {
		// 同一编号已经命中链上事实但字段冲突时禁止切换事实源，避免用 Pay 记录绕过地址、网络或状态校验。
		return ports.BinanceTransferRecord{}, err
	}
	startTimeMS, endTimeMS := payHistoryRange(req.OrderTimeMS, serverTimeMS)
	payRecords, err := c.payHistory(ctx, account, startTimeMS, endTimeMS, serverTimeMS)
	if err != nil {
		return ports.BinanceTransferRecord{}, err
	}
	return matchPayRecord(req, payRecords)
}

func matchOffchainDepositRecord(req ports.BinanceTransferLookupRequest, records []depositRecord) (ports.BinanceTransferRecord, error) {
	for _, record := range records {
		if !depositTxIDMatches(record.TxID, req.ExternalID) {
			continue
		}
		if !strings.HasPrefix(strings.ToLower(strings.TrimSpace(record.TxID)), "off-chain transfer") {
			return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance deposit is not off-chain transfer")
		}
		// Binance 仍把站内入账记录标成实际充值网络 TRX；复用强校验时必须恢复真实网络，
		// 地址、成功状态和金额校验均与普通 Deposit History 保持一致。
		req.Network = "TRX"
		return matchDepositRecord(req, []depositRecord{record})
	}
	return ports.BinanceTransferRecord{}, xerr.New(xerr.NotFound, "binance off-chain deposit not found")
}

func matchExplicitC2CPayRecord(req ports.BinanceTransferLookupRequest, records []payRecord) (ports.BinanceTransferRecord, error) {
	for _, record := range records {
		if !payExternalIDMatches(record, req.ExternalID) {
			continue
		}
		if record.OrderType != "C2C" {
			return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance pay order type is not c2c")
		}
		if record.Currency != "USDT" && record.Currency != "USDC" {
			return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance pay c2c currency is not supported")
		}
		if strings.HasPrefix(strings.TrimSpace(record.Amount), "-") {
			return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance pay transfer is not income")
		}
		amountMinor, err := parseUSDTAmountMinor(record.Amount)
		if err != nil {
			return ports.BinanceTransferRecord{}, err
		}
		if amountMinor <= 0 || amountMinor != req.AmountMinor {
			return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance pay amount mismatch")
		}
		return ports.BinanceTransferRecord{Source: "pay_history", ID: record.OrderID, TxID: firstNonEmpty(record.TransactionID, record.OrderID), Coin: record.Currency, Status: 1, Amount: record.Amount, AmountMinor: amountMinor, InsertTimeMS: record.TransactionTimeMS, CompleteTimeMS: record.TransactionTimeMS, WalletType: record.WalletType, RawJSON: strings.TrimSpace(record.RawJSON)}, nil
	}
	return ports.BinanceTransferRecord{}, xerr.New(xerr.NotFound, "binance pay c2c transfer not found")
}

// c2cHistory 查询当前 API Key 账户作为买方完成的 C2C 订单；BUY 表示账户收到 asset，SELL 则是付出 asset。
func (c *Client) c2cHistory(ctx context.Context, account account, startTimeMS int64, endTimeMS int64, serverTimeMS int64) ([]c2cRecord, error) {
	query := url.Values{}
	query.Set("tradeType", c2cTradeTypeBuy)
	query.Set("startTimestamp", strconv.FormatInt(startTimeMS, 10))
	query.Set("endTimestamp", strconv.FormatInt(endTimeMS, 10))
	query.Set("page", "1")
	query.Set("rows", strconv.Itoa(payHistoryLimit))
	body, err := c.signedGET(ctx, account, "/sapi/v1/c2c/orderMatch/listUserOrderHistory", query, serverTimeMS)
	if err != nil {
		return nil, err
	}
	var response c2cHistoryResponse
	if err := json.Unmarshal(body, &response); err != nil {
		return nil, err
	}
	if !strings.EqualFold(strings.TrimSpace(response.Code), "000000") || !response.Success {
		return nil, xerr.New(xerr.Unavailable, "binance c2c history returned non-success")
	}
	records := make([]c2cRecord, 0, len(response.Data))
	for _, raw := range response.Data {
		var record c2cRecord
		if err := json.Unmarshal(raw, &record); err != nil {
			return nil, err
		}
		record.OrderNumber = strings.TrimSpace(record.OrderNumber)
		record.TradeType = strings.ToUpper(strings.TrimSpace(record.TradeType))
		record.Asset = strings.ToUpper(strings.TrimSpace(record.Asset))
		record.Amount = strings.TrimSpace(record.Amount)
		record.OrderStatus = strings.ToUpper(strings.TrimSpace(record.OrderStatus))
		record.RawJSON = safeC2CRecordJSON(record)
		records = append(records, record)
	}
	return records, nil
}

func matchC2CRecord(req ports.BinanceTransferLookupRequest, records []c2cRecord) (ports.BinanceTransferRecord, error) {
	for _, record := range records {
		if !strings.EqualFold(record.OrderNumber, req.ExternalID) {
			continue
		}
		if record.TradeType != c2cTradeTypeBuy {
			return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance c2c trade is not income")
		}
		if !strings.EqualFold(record.Asset, req.Coin) {
			return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance c2c asset mismatch")
		}
		if record.OrderStatus != c2cOrderStatusComplete {
			return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance c2c order is not completed")
		}
		amountMinor, err := parseUSDTAmountMinor(record.Amount)
		if err != nil {
			return ports.BinanceTransferRecord{}, err
		}
		if amountMinor != req.AmountMinor {
			return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance c2c amount mismatch")
		}
		return ports.BinanceTransferRecord{Source: "c2c_history", ID: record.OrderNumber, TxID: record.OrderNumber, Coin: record.Asset, Status: 1, Amount: record.Amount, AmountMinor: amountMinor, InsertTimeMS: record.CreateTimeMS, CompleteTimeMS: record.CreateTimeMS, RawJSON: record.RawJSON}, nil
	}
	return ports.BinanceTransferRecord{}, xerr.New(xerr.NotFound, "binance c2c transfer not found")
}

func (c *Client) accountForApp(appCode string) (account, bool) {
	value, ok := c.accounts[strings.ToLower(strings.TrimSpace(appCode))]
	if !ok || strings.TrimSpace(value.apiKey) == "" || strings.TrimSpace(value.apiSecretKey) == "" {
		return account{}, false
	}
	return value, true
}

func (c *Client) serverTime(ctx context.Context) (int64, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.apiBaseURL+"/api/v3/time", nil)
	if err != nil {
		return 0, err
	}
	body, err := c.do(req)
	if err != nil {
		return 0, err
	}
	var parsed struct {
		ServerTime int64 `json:"serverTime"`
	}
	if err := json.Unmarshal(body, &parsed); err != nil {
		return 0, err
	}
	if parsed.ServerTime <= 0 {
		return 0, xerr.New(xerr.Unavailable, "binance server time is invalid")
	}
	return parsed.ServerTime, nil
}

func (c *Client) depositHistory(ctx context.Context, account account, coin string, startTimeMS int64, endTimeMS int64) ([]depositRecord, error) {
	query := url.Values{}
	query.Set("coin", strings.ToUpper(strings.TrimSpace(coin)))
	query.Set("startTime", strconv.FormatInt(startTimeMS, 10))
	query.Set("endTime", strconv.FormatInt(endTimeMS, 10))
	query.Set("limit", strconv.Itoa(depositHistoryLimit))
	body, err := c.signedGET(ctx, account, "/sapi/v1/capital/deposit/hisrec", query, endTimeMS)
	if err != nil {
		return nil, err
	}
	var raws []json.RawMessage
	if err := json.Unmarshal(body, &raws); err != nil {
		return nil, err
	}
	records := make([]depositRecord, 0, len(raws))
	for _, raw := range raws {
		var wire depositRecordWire
		if err := json.Unmarshal(raw, &wire); err != nil {
			return nil, err
		}
		record := wire.toRecord()
		record.RawJSON = string(raw)
		records = append(records, record)
	}
	return records, nil
}

// payHistory 查询 Binance Pay 用户账单。接口单次最多返回 100 条，调用方提供订单时间时使用窄窗口降低高频账户漏单概率。
func (c *Client) payHistory(ctx context.Context, account account, startTimeMS int64, endTimeMS int64, serverTimeMS int64) ([]payRecord, error) {
	query := url.Values{}
	query.Set("startTime", strconv.FormatInt(startTimeMS, 10))
	query.Set("endTime", strconv.FormatInt(endTimeMS, 10))
	query.Set("limit", strconv.Itoa(payHistoryLimit))
	// 查询区间可以是历史日期，但 signed API 的 timestamp 必须始终使用当前 Binance server time，否则旧订单会被 -1021 拒绝。
	body, err := c.signedGET(ctx, account, "/sapi/v1/pay/transactions", query, serverTimeMS)
	if err != nil {
		return nil, err
	}
	var response payHistoryResponse
	if err := json.Unmarshal(body, &response); err != nil {
		return nil, err
	}
	if strings.TrimSpace(response.Code) != binancePaySuccessCode || !response.Success {
		return nil, xerr.New(xerr.Unavailable, "binance pay history returned non-success")
	}
	records := make([]payRecord, 0, len(response.Data))
	for _, raw := range response.Data {
		var wire payRecordWire
		if err := json.Unmarshal(raw, &wire); err != nil {
			return nil, err
		}
		record := wire.toRecord()
		// Pay 回包含付款人与收款人的账户资料；持久审计只保留完成资金校验所需字段，避免把无关 PII 写入充值凭证。
		record.RawJSON = safePayRecordJSON(record)
		records = append(records, record)
	}
	return records, nil
}

func payHistoryRange(orderTimeMS int64, serverTimeMS int64) (int64, int64) {
	lookbackMS := int64(defaultPayLookback / time.Millisecond)
	if orderTimeMS <= 0 {
		return maxInt64(0, serverTimeMS-lookbackMS), serverTimeMS
	}
	radiusMS := int64(payOrderTimeRadius / time.Millisecond)
	startTimeMS := maxInt64(0, orderTimeMS-radiusMS)
	endTimeMS := minInt64(serverTimeMS, orderTimeMS+radiusMS)
	if endTimeMS <= startTimeMS {
		// 未来订单时间不应发生；这里返回最小合法区间，让 Binance 给出空结果而不是发出反向时间范围。
		endTimeMS = startTimeMS + 1
	}
	return startTimeMS, endTimeMS
}

func (c *Client) signedGET(ctx context.Context, account account, path string, values url.Values, timestampMS int64) ([]byte, error) {
	values.Set("recvWindow", strconv.FormatInt(c.recvWindow, 10))
	values.Set("timestamp", strconv.FormatInt(timestampMS, 10))
	queryString := values.Encode()
	signature := signQueryString(queryString, account.apiSecretKey)
	endpoint := c.apiBaseURL + path + "?" + queryString + "&signature=" + signature
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("X-MBX-APIKEY", account.apiKey)
	return c.do(req)
}

func (c *Client) do(req *http.Request) ([]byte, error) {
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, xerr.New(xerr.Unavailable, "binance request failed")
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(io.LimitReader(resp.Body, 2<<20))
	if err != nil {
		return nil, err
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, xerr.New(xerr.Unavailable, "binance returned non-success")
	}
	return body, nil
}

func matchDepositRecord(req ports.BinanceTransferLookupRequest, records []depositRecord) (ports.BinanceTransferRecord, error) {
	var candidate *depositRecord
	for index := range records {
		if depositTxIDMatches(records[index].TxID, req.ExternalID) {
			candidate = &records[index]
			break
		}
	}
	if candidate == nil {
		return ports.BinanceTransferRecord{}, xerr.New(xerr.NotFound, "binance deposit not found")
	}
	if !strings.EqualFold(candidate.Coin, req.Coin) {
		return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance deposit coin mismatch")
	}
	if !strings.EqualFold(candidate.Network, req.Network) {
		return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance deposit network mismatch")
	}
	if candidate.Status != 1 {
		return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance deposit status is not successful")
	}
	// Deposit History 属于具体链上入账，必须有预先配置的收款地址；允许空地址仅服务于随后独立校验的 Pay C2C 事实源。
	if strings.TrimSpace(req.ToAddress) == "" || !strings.EqualFold(strings.TrimSpace(candidate.Address), strings.TrimSpace(req.ToAddress)) {
		return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance deposit address mismatch")
	}
	amountMinor, err := parseUSDTAmountMinor(candidate.Amount)
	if err != nil {
		return ports.BinanceTransferRecord{}, err
	}
	if amountMinor != req.AmountMinor {
		return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance deposit amount mismatch")
	}
	return ports.BinanceTransferRecord{
		Source:         "deposit_history",
		ID:             candidate.ID,
		TxID:           candidate.TxID,
		Coin:           candidate.Coin,
		Network:        candidate.Network,
		Status:         candidate.Status,
		Address:        candidate.Address,
		Amount:         candidate.Amount,
		AmountMinor:    amountMinor,
		TransferType:   candidate.TransferType,
		InsertTimeMS:   candidate.InsertTimeMS,
		CompleteTimeMS: candidate.CompleteTimeMS,
		ConfirmTimes:   candidate.ConfirmTimes,
		UnlockConfirm:  candidate.UnlockConfirm,
		WalletType:     candidate.WalletType,
		RawJSON:        strings.TrimSpace(candidate.RawJSON),
	}, nil
}

func matchPayRecord(req ports.BinanceTransferLookupRequest, records []payRecord) (ports.BinanceTransferRecord, error) {
	var candidate *payRecord
	for index := range records {
		if payExternalIDMatches(records[index], req.ExternalID) {
			candidate = &records[index]
			break
		}
	}
	if candidate == nil {
		return ports.BinanceTransferRecord{}, xerr.New(xerr.NotFound, "binance pay transfer not found")
	}
	if !strings.EqualFold(candidate.OrderType, "C2C") {
		return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance pay order type is not c2c")
	}
	if !strings.EqualFold(candidate.Currency, req.Coin) {
		return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance pay currency mismatch")
	}
	// Binance Pay 官方语义中正数是当前 API Key 账户收入、负数是支出；只允许正数，防止付款截图被当成平台收款。
	if strings.HasPrefix(strings.TrimSpace(candidate.Amount), "-") {
		return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance pay transfer is not income")
	}
	amountMinor, err := parseUSDTAmountMinor(candidate.Amount)
	if err != nil {
		return ports.BinanceTransferRecord{}, err
	}
	if amountMinor <= 0 {
		return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance pay transfer is not income")
	}
	if amountMinor != req.AmountMinor {
		return ports.BinanceTransferRecord{}, xerr.New(xerr.Conflict, "binance pay amount mismatch")
	}
	return ports.BinanceTransferRecord{
		Source:         "pay_history",
		ID:             candidate.OrderID,
		TxID:           firstNonEmpty(candidate.TransactionID, candidate.OrderID),
		Coin:           candidate.Currency,
		Status:         1,
		Amount:         candidate.Amount,
		AmountMinor:    amountMinor,
		InsertTimeMS:   candidate.TransactionTimeMS,
		CompleteTimeMS: candidate.TransactionTimeMS,
		WalletType:     candidate.WalletType,
		RawJSON:        strings.TrimSpace(candidate.RawJSON),
	}, nil
}

func payExternalIDMatches(record payRecord, externalID string) bool {
	externalID = strings.TrimSpace(externalID)
	if externalID == "" {
		return false
	}
	return strings.EqualFold(strings.TrimSpace(record.OrderID), externalID) ||
		strings.EqualFold(strings.TrimSpace(record.TransactionID), externalID)
}

func depositTxIDMatches(txID string, externalID string) bool {
	normalizedExternalID := normalizeOffchainExternalID(externalID)
	if normalizedExternalID == "" {
		return false
	}
	normalizedTxID := strings.ToLower(strings.TrimSpace(txID))
	return strings.Contains(normalizedTxID, normalizedExternalID) || normalizeOffchainExternalID(txID) == normalizedExternalID
}

func normalizeOffchainExternalID(value string) string {
	value = strings.ToLower(strings.TrimSpace(value))
	value = strings.TrimPrefix(value, "off-chain transfer")
	value = strings.TrimSpace(strings.Trim(value, ":#"))
	return value
}

func parseUSDTAmountMinor(value string) (int64, error) {
	value = strings.TrimSpace(value)
	if value == "" || strings.HasPrefix(value, "-") {
		return 0, xerr.New(xerr.InvalidArgument, "binance deposit amount is invalid")
	}
	parts := strings.Split(value, ".")
	if len(parts) > 2 {
		return 0, xerr.New(xerr.InvalidArgument, "binance deposit amount is invalid")
	}
	whole, err := strconv.ParseInt(firstNonEmpty(parts[0], "0"), 10, 64)
	if err != nil {
		return 0, xerr.New(xerr.InvalidArgument, "binance deposit amount is invalid")
	}
	frac := ""
	if len(parts) == 2 {
		frac = parts[1]
	}
	if len(frac) > 2 && strings.Trim(frac[2:], "0") != "" {
		return 0, xerr.New(xerr.Conflict, "binance deposit amount has sub-cent precision")
	}
	if len(frac) > 2 {
		frac = frac[:2]
	}
	for len(frac) < 2 {
		frac += "0"
	}
	minorFrac, err := strconv.ParseInt(firstNonEmpty(frac, "0"), 10, 64)
	if err != nil {
		return 0, xerr.New(xerr.InvalidArgument, "binance deposit amount is invalid")
	}
	return whole*100 + minorFrac, nil
}

func signQueryString(queryString string, secret string) string {
	mac := hmac.New(sha256.New, []byte(secret))
	_, _ = mac.Write([]byte(queryString))
	return hex.EncodeToString(mac.Sum(nil))
}

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

func firstPositiveInt64(values ...int64) int64 {
	for _, value := range values {
		if value > 0 {
			return value
		}
	}
	return 0
}

type depositRecord struct {
	ID             string
	Amount         string
	Coin           string
	Network        string
	Status         int64
	Address        string
	TxID           string
	TransferType   int64
	InsertTimeMS   int64
	CompleteTimeMS int64
	ConfirmTimes   string
	UnlockConfirm  int64
	WalletType     int64
	RawJSON        string
}

type depositRecordWire struct {
	ID             json.RawMessage `json:"id"`
	Amount         string          `json:"amount"`
	Coin           string          `json:"coin"`
	Network        string          `json:"network"`
	Status         int64           `json:"status"`
	Address        string          `json:"address"`
	TxID           string          `json:"txId"`
	TransferType   int64           `json:"transferType"`
	InsertTimeMS   int64           `json:"insertTime"`
	CompleteTimeMS int64           `json:"completeTime"`
	ConfirmTimes   string          `json:"confirmTimes"`
	UnlockConfirm  int64           `json:"unlockConfirm"`
	WalletType     int64           `json:"walletType"`
}

type payHistoryResponse struct {
	Code    string            `json:"code"`
	Message string            `json:"message"`
	Success bool              `json:"success"`
	Data    []json.RawMessage `json:"data"`
}

type c2cHistoryResponse struct {
	Code    string            `json:"code"`
	Success bool              `json:"success"`
	Data    []json.RawMessage `json:"data"`
}

type c2cRecord struct {
	OrderNumber  string `json:"orderNumber"`
	TradeType    string `json:"tradeType"`
	Asset        string `json:"asset"`
	Amount       string `json:"amount"`
	OrderStatus  string `json:"orderStatus"`
	CreateTimeMS int64  `json:"createTime"`
	RawJSON      string `json:"-"`
}

func safeC2CRecordJSON(record c2cRecord) string {
	body, err := json.Marshal(struct {
		OrderNumber  string `json:"orderNumber"`
		TradeType    string `json:"tradeType"`
		Asset        string `json:"asset"`
		Amount       string `json:"amount"`
		OrderStatus  string `json:"orderStatus"`
		CreateTimeMS int64  `json:"createTime"`
	}{record.OrderNumber, record.TradeType, record.Asset, record.Amount, record.OrderStatus, record.CreateTimeMS})
	if err != nil {
		return ""
	}
	return string(body)
}

type payRecord struct {
	OrderType         string
	OrderID           string
	TransactionID     string
	TransactionTimeMS int64
	Amount            string
	Currency          string
	WalletType        int64
	RawJSON           string
}

type payRecordWire struct {
	OrderType         string          `json:"orderType"`
	OrderID           json.RawMessage `json:"orderId"`
	TransactionID     string          `json:"transactionId"`
	TransactionTimeMS int64           `json:"transactionTime"`
	Amount            string          `json:"amount"`
	Currency          string          `json:"currency"`
	WalletType        int64           `json:"walletType"`
}

func (wire payRecordWire) toRecord() payRecord {
	return payRecord{
		OrderType:         strings.ToUpper(strings.TrimSpace(wire.OrderType)),
		OrderID:           rawScalarString(wire.OrderID),
		TransactionID:     strings.TrimSpace(wire.TransactionID),
		TransactionTimeMS: wire.TransactionTimeMS,
		Amount:            strings.TrimSpace(wire.Amount),
		Currency:          strings.ToUpper(strings.TrimSpace(wire.Currency)),
		WalletType:        wire.WalletType,
	}
}

func safePayRecordJSON(record payRecord) string {
	payload := struct {
		OrderType         string `json:"orderType"`
		OrderID           string `json:"orderId,omitempty"`
		TransactionID     string `json:"transactionId"`
		TransactionTimeMS int64  `json:"transactionTime"`
		Amount            string `json:"amount"`
		Currency          string `json:"currency"`
		WalletType        int64  `json:"walletType"`
	}{
		OrderType:         record.OrderType,
		OrderID:           record.OrderID,
		TransactionID:     record.TransactionID,
		TransactionTimeMS: record.TransactionTimeMS,
		Amount:            record.Amount,
		Currency:          record.Currency,
		WalletType:        record.WalletType,
	}
	body, err := json.Marshal(payload)
	if err != nil {
		return ""
	}
	return string(body)
}

func (wire depositRecordWire) toRecord() depositRecord {
	return depositRecord{
		ID:             rawScalarString(wire.ID),
		Amount:         strings.TrimSpace(wire.Amount),
		Coin:           strings.ToUpper(strings.TrimSpace(wire.Coin)),
		Network:        strings.ToUpper(strings.TrimSpace(wire.Network)),
		Status:         wire.Status,
		Address:        strings.TrimSpace(wire.Address),
		TxID:           strings.TrimSpace(wire.TxID),
		TransferType:   wire.TransferType,
		InsertTimeMS:   wire.InsertTimeMS,
		CompleteTimeMS: wire.CompleteTimeMS,
		ConfirmTimes:   strings.TrimSpace(wire.ConfirmTimes),
		UnlockConfirm:  wire.UnlockConfirm,
		WalletType:     wire.WalletType,
	}
}

func rawScalarString(raw json.RawMessage) string {
	if len(raw) == 0 || string(raw) == "null" {
		return ""
	}
	var text string
	if err := json.Unmarshal(raw, &text); err == nil {
		return strings.TrimSpace(text)
	}
	return strings.TrimSpace(string(raw))
}

func minInt64(left int64, right int64) int64 {
	if left < right {
		return left
	}
	return right
}

func maxInt64(left int64, right int64) int64 {
	if left > right {
		return left
	}
	return right
}
