package mysql

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"strings"
	"time"

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

const (
	googlePaidSyncRetryBase = 15 * time.Second
	googlePaidSyncRetryMax  = 15 * time.Minute
)

// ConfirmGooglePayment 将 Google Play 已购买 token 落为平台支付订单、钱包交易和充值审计记录。
func (r *Repository) ConfirmGooglePayment(ctx context.Context, command ledger.GooglePaymentCommand, product ledger.RechargeProduct, purchase ledger.GooglePlayPurchase) (ledger.GooglePaymentReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.GooglePaymentReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)

	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return ledger.GooglePaymentReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()

	tokenHash := googlePurchaseTokenHash(command.PurchaseToken)
	if existing, exists, err := r.lookupGooglePaymentOrderByToken(ctx, tx, tokenHash); err != nil || exists {
		if err != nil || !exists {
			return ledger.GooglePaymentReceipt{}, err
		}
		if existing.UserID != command.UserID || existing.ProductID != product.ProductID {
			return ledger.GooglePaymentReceipt{}, xerr.New(xerr.Conflict, "google purchase token belongs to another payment order")
		}
		return r.receiptForGooglePaymentOrder(ctx, tx, existing.PaymentOrderID, true)
	}

	requestHash := googlePaymentRequestHash(command, product, purchase, tokenHash)
	if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeGooglePlayRecharge); err != nil || exists {
		if err != nil || !exists {
			return ledger.GooglePaymentReceipt{}, err
		}
		return r.receiptForGooglePaymentTransaction(ctx, tx, txRow.TransactionID, true)
	}

	nowMs := time.Now().UnixMilli()
	account, err := r.lockAccount(ctx, tx, command.UserID, ledger.AssetCoin, true, nowMs)
	if err != nil {
		return ledger.GooglePaymentReceipt{}, err
	}
	transactionID := transactionID(command.AppCode, command.CommandID)
	paymentOrderID := googlePaymentOrderID(command.AppCode, tokenHash)
	balanceAfter := account.AvailableAmount + product.CoinAmount
	// Google Play 返回和后台商品都保存微单位价格；充值事实表使用美元最小单位，必须在写账前收敛成美分。
	rechargeUSDMinor := googleAmountMicroToUSDMinor(product.AmountMicro)
	rechargeSequence, err := r.reserveUserRechargeSequence(ctx, tx, command.AppCode, command.UserID, transactionID, product.CoinAmount, rechargeUSDMinor, nowMs)
	if err != nil {
		return ledger.GooglePaymentReceipt{}, err
	}
	metadata := googlePaymentMetadata{
		AppCode:                command.AppCode,
		PaymentOrderID:         paymentOrderID,
		UserID:                 command.UserID,
		RegionID:               command.RegionID,
		ProductID:              product.ProductID,
		ProductCode:            product.ProductCode,
		ProductName:            product.ProductName,
		PackageName:            command.PackageName,
		Provider:               ledger.PaymentProviderGooglePlay,
		Channel:                ledger.RechargeChannelGoogle,
		ProviderOrderID:        purchase.OrderID,
		ClientOrderID:          command.OrderID,
		PurchaseTokenHash:      tokenHash,
		PurchaseState:          purchase.PurchaseState,
		ConsumptionState:       purchase.ConsumptionState,
		AcknowledgementState:   purchase.AcknowledgementState,
		PurchaseCompletionTime: purchase.PurchaseCompletionTime,
		CoinAmount:             product.CoinAmount,
		CurrencyCode:           product.CurrencyCode,
		AmountMicro:            product.AmountMicro,
		RechargeUSDMinor:       rechargeUSDMinor,
		BalanceAfter:           balanceAfter,
		RechargeSequence:       rechargeSequence,
		CreatedAtMS:            nowMs,
	}
	// 充值事实事件只消费 coinSellerTransferMetadata 的统一维度字段；Google 支付审计仍保留 googlePaymentMetadata，
	// 但 WalletRechargeRecorded 必须带 country_id/target_country_id，统计服务不能再从 region 或用户库反推国家。
	rechargeMetadata := coinSellerTransferMetadata{
		AppCode:                      command.AppCode,
		PaymentOrderID:               paymentOrderID,
		ProviderCode:                 ledger.PaymentProviderGooglePlay,
		TargetUserID:                 command.UserID,
		TargetCountryID:              command.CountryID,
		TargetRegionID:               command.RegionID,
		Amount:                       product.CoinAmount,
		TargetAssetType:              ledger.AssetCoin,
		TargetBalanceAfter:           balanceAfter,
		GoogleProductID:              product.ProductName,
		ProductCode:                  product.ProductCode,
		ProductName:                  product.ProductName,
		RechargeSequence:             rechargeSequence,
		RechargeUSDMinor:             rechargeUSDMinor,
		RechargeCurrencyCode:         product.CurrencyCode,
		RechargePolicyID:             0,
		RechargePolicyVersion:        product.PolicyVersion,
		RechargePolicyCoinAmount:     product.CoinAmount,
		RechargePolicyUSDMinorAmount: rechargeUSDMinor,
		RechargeType:                 ledger.RechargeChannelGoogle,
	}
	if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeGooglePlayRecharge, requestHash, externalGooglePaymentRef(purchase, tokenHash), metadata, nowMs); err != nil {
		return ledger.GooglePaymentReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, account, product.CoinAmount, 0, nowMs); err != nil {
		return ledger.GooglePaymentReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:  transactionID,
		UserID:         command.UserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: product.CoinAmount,
		FrozenDelta:    0,
		AvailableAfter: balanceAfter,
		FrozenAfter:    account.FrozenAmount,
		CreatedAtMS:    nowMs,
	}); err != nil {
		return ledger.GooglePaymentReceipt{}, err
	}
	if err := r.insertRechargeRecord(ctx, tx, transactionID, rechargeMetadata, nowMs); err != nil {
		return ledger.GooglePaymentReceipt{}, err
	}
	if err := r.insertGooglePaymentOrder(ctx, tx, paymentOrderID, command, product, purchase, tokenHash, transactionID, nowMs); err != nil {
		return ledger.GooglePaymentReceipt{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		balanceChangedEvent(transactionID, command.CommandID, command.UserID, ledger.AssetCoin, product.CoinAmount, 0, balanceAfter, account.FrozenAmount, account.Version+1, metadata, nowMs),
		googlePaymentCreditedEvent(transactionID, command.CommandID, metadata, nowMs),
		rechargeRecordedEvent(transactionID, command.CommandID, command.UserID, product.CoinAmount, rechargeMetadata, nowMs),
	}); err != nil {
		return ledger.GooglePaymentReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.GooglePaymentReceipt{}, err
	}

	return ledger.GooglePaymentReceipt{
		PaymentOrderID: paymentOrderID,
		TransactionID:  transactionID,
		Status:         ledger.PaymentStatusCredited,
		ProductID:      product.ProductID,
		ProductCode:    product.ProductCode,
		CoinAmount:     product.CoinAmount,
		Balance: ledger.AssetBalance{
			AppCode:         command.AppCode,
			UserID:          command.UserID,
			AssetType:       ledger.AssetCoin,
			AvailableAmount: balanceAfter,
			FrozenAmount:    account.FrozenAmount,
		},
		ConsumeState: ledger.PaymentConsumeStatePending,
	}, nil
}

func (r *Repository) MarkGooglePaymentConsumed(ctx context.Context, appCode string, paymentOrderID string) error {
	if r == nil || r.db == nil {
		return xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, appCode)
	paymentOrderID = strings.TrimSpace(paymentOrderID)
	if paymentOrderID == "" {
		return xerr.New(xerr.InvalidArgument, "payment_order_id is required")
	}
	_, err := r.db.ExecContext(ctx, `
		UPDATE payment_orders
		SET consume_state = ?, updated_at_ms = ?
		WHERE app_code = ? AND payment_order_id = ? AND provider = ?`,
		ledger.PaymentConsumeStateConsumed,
		time.Now().UnixMilli(),
		appcode.FromContext(ctx),
		paymentOrderID,
		ledger.PaymentProviderGooglePlay,
	)
	return err
}

type googlePaymentOrderRow struct {
	PaymentOrderID string
	UserID         int64
	ProductID      int64
	TransactionID  string
}

func (r *Repository) lookupGooglePaymentOrderByToken(ctx context.Context, tx *sql.Tx, tokenHash string) (googlePaymentOrderRow, bool, error) {
	row := tx.QueryRowContext(ctx, `
		SELECT payment_order_id, user_id, product_id, wallet_transaction_id
		FROM payment_orders
		WHERE app_code = ? AND provider = ? AND purchase_token_hash = ?
		FOR UPDATE`,
		appcode.FromContext(ctx),
		ledger.PaymentProviderGooglePlay,
		tokenHash,
	)
	var payment googlePaymentOrderRow
	if err := row.Scan(&payment.PaymentOrderID, &payment.UserID, &payment.ProductID, &payment.TransactionID); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return googlePaymentOrderRow{}, false, nil
		}
		return googlePaymentOrderRow{}, false, err
	}
	return payment, true, nil
}

func (r *Repository) insertGooglePaymentOrder(ctx context.Context, tx *sql.Tx, paymentOrderID string, command ledger.GooglePaymentCommand, product ledger.RechargeProduct, purchase ledger.GooglePlayPurchase, tokenHash string, transactionID string, nowMs int64) error {
	payloadJSON := purchase.RawJSON
	if strings.TrimSpace(payloadJSON) == "" {
		payloadJSON = mustJSON(purchase)
	}
	_, err := tx.ExecContext(ctx, `
		INSERT INTO payment_orders (
		   app_code, payment_order_id, provider, channel, status, command_id, user_id,
		   product_id, product_code, package_name, provider_order_id, purchase_token_hash,
		   purchase_state, consume_state, wallet_transaction_id, coin_amount, currency_code,
		   amount_micro, provider_payload, created_at_ms, updated_at_ms
		 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		command.AppCode,
		paymentOrderID,
		ledger.PaymentProviderGooglePlay,
		ledger.RechargeChannelGoogle,
		ledger.PaymentStatusCredited,
		command.CommandID,
		command.UserID,
		product.ProductID,
		product.ProductCode,
		command.PackageName,
		purchase.OrderID,
		tokenHash,
		purchase.PurchaseState,
		ledger.PaymentConsumeStatePending,
		transactionID,
		product.CoinAmount,
		product.CurrencyCode,
		product.AmountMicro,
		payloadJSON,
		nowMs,
		nowMs,
	)
	if err != nil && isMySQLDuplicateError(err) {
		return xerr.New(xerr.Conflict, "google payment order already exists")
	}
	return err
}

func (r *Repository) receiptForGooglePaymentTransaction(ctx context.Context, tx *sql.Tx, transactionID string, idempotentReplay bool) (ledger.GooglePaymentReceipt, error) {
	var paymentOrderID string
	if err := tx.QueryRowContext(ctx, `
		SELECT payment_order_id
		FROM payment_orders
		WHERE app_code = ? AND wallet_transaction_id = ?
		LIMIT 1`,
		appcode.FromContext(ctx),
		transactionID,
	).Scan(&paymentOrderID); err != nil {
		return ledger.GooglePaymentReceipt{}, err
	}
	return r.receiptForGooglePaymentOrder(ctx, tx, paymentOrderID, idempotentReplay)
}

func (r *Repository) receiptForGooglePaymentOrder(ctx context.Context, tx *sql.Tx, paymentOrderID string, idempotentReplay bool) (ledger.GooglePaymentReceipt, error) {
	row := tx.QueryRowContext(ctx, `
		SELECT payment_order_id, wallet_transaction_id, status, user_id, product_id, product_code,
		       coin_amount, consume_state
		FROM payment_orders
		WHERE app_code = ? AND payment_order_id = ?`,
		appcode.FromContext(ctx),
		paymentOrderID,
	)
	var receipt ledger.GooglePaymentReceipt
	var userID int64
	if err := row.Scan(
		&receipt.PaymentOrderID,
		&receipt.TransactionID,
		&receipt.Status,
		&userID,
		&receipt.ProductID,
		&receipt.ProductCode,
		&receipt.CoinAmount,
		&receipt.ConsumeState,
	); err != nil {
		return ledger.GooglePaymentReceipt{}, err
	}
	balance, err := r.balanceAfterTransaction(ctx, tx, receipt.TransactionID, userID, ledger.AssetCoin)
	if err != nil {
		return ledger.GooglePaymentReceipt{}, err
	}
	receipt.Balance = balance
	receipt.IdempotentReplay = idempotentReplay
	return receipt, nil
}

// ListGooglePaymentOrderRefs 按钱包交易 ID 批量反查 Google 支付订单定位信息；只返回命中的交易。
func (r *Repository) ListGooglePaymentOrderRefs(ctx context.Context, appCode string, transactionIDs []string) (map[string]ledger.GooglePaymentOrderRef, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, appCode)
	cleaned := make([]string, 0, len(transactionIDs))
	for _, transactionID := range transactionIDs {
		if transactionID = strings.TrimSpace(transactionID); transactionID != "" {
			cleaned = append(cleaned, transactionID)
		}
	}
	if len(cleaned) == 0 {
		return map[string]ledger.GooglePaymentOrderRef{}, nil
	}
	placeholders := strings.TrimSuffix(strings.Repeat("?,", len(cleaned)), ",")
	args := make([]any, 0, len(cleaned)+2)
	args = append(args, appcode.FromContext(ctx), ledger.PaymentProviderGooglePlay)
	for _, transactionID := range cleaned {
		args = append(args, transactionID)
	}
	rows, err := r.db.QueryContext(ctx, `
		SELECT app_code, payment_order_id, wallet_transaction_id, package_name, provider_order_id,
		       paid_synced_at_ms, paid_sync_attempt_count
		FROM payment_orders
		WHERE app_code = ? AND provider = ? AND wallet_transaction_id IN (`+placeholders+`)`,
		args...,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	refs := make(map[string]ledger.GooglePaymentOrderRef, len(cleaned))
	for rows.Next() {
		var ref ledger.GooglePaymentOrderRef
		if err := rows.Scan(&ref.AppCode, &ref.PaymentOrderID, &ref.TransactionID, &ref.PackageName, &ref.ProviderOrderID, &ref.PaidSyncedAtMS, &ref.SyncAttemptCount); err != nil {
			return nil, err
		}
		refs[ref.TransactionID] = ref
	}
	return refs, rows.Err()
}

// ListPendingGooglePaymentOrderRefs claims no rows: paid-detail synchronization is idempotent and the
// retry columns prevent failed orders from monopolizing every 15-second scan. Duplicate workers may
// read the same row during a rolling deploy, but both write the same provider fact.
func (r *Repository) ListPendingGooglePaymentOrderRefs(ctx context.Context, limit int, nowMS int64) ([]ledger.GooglePaymentOrderRef, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	if limit <= 0 {
		return []ledger.GooglePaymentOrderRef{}, nil
	}
	if limit > 1000 {
		limit = 1000
	}
	rows, err := r.db.QueryContext(ctx, `
		SELECT app_code, payment_order_id, wallet_transaction_id, package_name, provider_order_id,
		       paid_synced_at_ms, paid_sync_attempt_count
		FROM payment_orders
		WHERE provider = ? AND paid_synced_at_ms = 0 AND paid_sync_next_retry_at_ms <= ?
		ORDER BY created_at_ms DESC, payment_order_id DESC
		LIMIT ?`,
		ledger.PaymentProviderGooglePlay, nowMS, limit,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	refs := make([]ledger.GooglePaymentOrderRef, 0, limit)
	for rows.Next() {
		var ref ledger.GooglePaymentOrderRef
		if err := rows.Scan(&ref.AppCode, &ref.PaymentOrderID, &ref.TransactionID, &ref.PackageName, &ref.ProviderOrderID, &ref.PaidSyncedAtMS, &ref.SyncAttemptCount); err != nil {
			return nil, err
		}
		refs = append(refs, ref)
	}
	return refs, rows.Err()
}

// UpdateGooglePaymentPaidDetails 把 Orders API 返回的实付明细写回 payment_orders；同步时间必须由调用方给出。
func (r *Repository) UpdateGooglePaymentPaidDetails(ctx context.Context, appCode string, paymentOrderID string, details ledger.GooglePaymentPaidDetails) error {
	if r == nil || r.db == nil {
		return xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, appCode)
	paymentOrderID = strings.TrimSpace(paymentOrderID)
	if paymentOrderID == "" {
		return xerr.New(xerr.InvalidArgument, "payment_order_id is required")
	}
	_, err := r.db.ExecContext(ctx, `
		UPDATE payment_orders
		SET paid_currency_code = ?, paid_amount_micro = ?, paid_tax_micro = ?, paid_net_micro = ?, paid_synced_at_ms = ?,
		    paid_sync_attempt_count = 0, paid_sync_last_error = '', paid_sync_next_retry_at_ms = 0, updated_at_ms = ?
		WHERE app_code = ? AND payment_order_id = ? AND provider = ?`,
		strings.ToUpper(strings.TrimSpace(details.CurrencyCode)),
		details.PaidAmountMicro,
		details.TaxAmountMicro,
		details.NetAmountMicro,
		details.SyncedAtMS,
		time.Now().UnixMilli(),
		appcode.FromContext(ctx),
		paymentOrderID,
		ledger.PaymentProviderGooglePlay,
	)
	return err
}

// MarkGooglePaymentPaidSyncFailure stores sanitized diagnostics and exponential retry time.
// It never overwrites a concurrently completed synchronization because paid_synced_at_ms=0 is required.
func (r *Repository) MarkGooglePaymentPaidSyncFailure(ctx context.Context, ref ledger.GooglePaymentOrderRef, message string, nowMS int64) error {
	if r == nil || r.db == nil {
		return xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	message = strings.TrimSpace(message)
	if len(message) > 512 {
		message = message[:512]
	}
	backoff := googlePaidSyncRetryBase
	for attempt := 0; attempt < ref.SyncAttemptCount && backoff < googlePaidSyncRetryMax; attempt++ {
		backoff *= 2
		if backoff > googlePaidSyncRetryMax {
			backoff = googlePaidSyncRetryMax
		}
	}
	_, err := r.db.ExecContext(ctx, `
		UPDATE payment_orders
		SET paid_sync_attempt_count = paid_sync_attempt_count + 1,
		    paid_sync_last_error = ?, paid_sync_next_retry_at_ms = ?, updated_at_ms = ?
		WHERE app_code = ? AND payment_order_id = ? AND provider = ? AND paid_synced_at_ms = 0`,
		message, nowMS+backoff.Milliseconds(), nowMS,
		strings.ToLower(strings.TrimSpace(ref.AppCode)), strings.TrimSpace(ref.PaymentOrderID), ledger.PaymentProviderGooglePlay,
	)
	return err
}

type googlePaymentMetadata struct {
	AppCode                string `json:"app_code"`
	PaymentOrderID         string `json:"payment_order_id"`
	UserID                 int64  `json:"user_id"`
	RegionID               int64  `json:"region_id"`
	ProductID              int64  `json:"product_id"`
	ProductCode            string `json:"product_code"`
	ProductName            string `json:"product_name"`
	PackageName            string `json:"package_name"`
	Provider               string `json:"provider"`
	Channel                string `json:"channel"`
	ProviderOrderID        string `json:"provider_order_id"`
	ClientOrderID          string `json:"client_order_id"`
	PurchaseTokenHash      string `json:"purchase_token_hash"`
	PurchaseState          string `json:"purchase_state"`
	ConsumptionState       string `json:"consumption_state"`
	AcknowledgementState   string `json:"acknowledgement_state"`
	PurchaseCompletionTime string `json:"purchase_completion_time"`
	CoinAmount             int64  `json:"coin_amount"`
	CurrencyCode           string `json:"currency_code"`
	AmountMicro            int64  `json:"amount_micro"`
	RechargeUSDMinor       int64  `json:"recharge_usd_minor"`
	BalanceAfter           int64  `json:"balance_after"`
	RechargeSequence       int64  `json:"recharge_sequence"`
	CreatedAtMS            int64  `json:"created_at_ms"`
}

func googleAmountMicroToUSDMinor(amountMicro int64) int64 {
	if amountMicro <= 0 {
		return 0
	}
	return amountMicro / 10_000
}

func googlePaymentCreditedEvent(transactionID string, commandID string, metadata googlePaymentMetadata, nowMs int64) walletOutboxEvent {
	return walletOutboxEvent{
		EventID:        eventID(transactionID, "WalletGooglePaymentCredited", metadata.UserID, ledger.AssetCoin),
		EventType:      "WalletGooglePaymentCredited",
		TransactionID:  transactionID,
		CommandID:      commandID,
		UserID:         metadata.UserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: metadata.CoinAmount,
		FrozenDelta:    0,
		Payload:        metadata,
		CreatedAtMS:    nowMs,
	}
}

func googlePaymentRequestHash(command ledger.GooglePaymentCommand, product ledger.RechargeProduct, purchase ledger.GooglePlayPurchase, tokenHash string) string {
	return stableHash(fmt.Sprintf("google_play|%s|%d|%d|%d|%s|%s|%s|%s|%s|%d|%d",
		appcode.Normalize(command.AppCode),
		command.UserID,
		command.RegionID,
		product.ProductID,
		product.ProductCode,
		command.PackageName,
		tokenHash,
		purchase.OrderID,
		purchase.PurchaseState,
		product.CoinAmount,
		product.AmountMicro,
	))
}

func googlePurchaseTokenHash(token string) string {
	return stableHash(strings.TrimSpace(token))
}

func googlePaymentOrderID(appCode string, tokenHash string) string {
	return "gpay_" + stableHash(appcode.Normalize(appCode) + "|" + tokenHash)[:48]
}

func externalGooglePaymentRef(purchase ledger.GooglePlayPurchase, tokenHash string) string {
	if strings.TrimSpace(purchase.OrderID) != "" {
		return purchase.OrderID
	}
	return "token:" + tokenHash[:32]
}
