package mysql

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
)

type transactionRow struct {
	TransactionID string
	MetadataJSON  string
}

func (r *Repository) lookupTransaction(ctx context.Context, tx *sql.Tx, commandID string, requestHash string, bizType string) (transactionRow, bool, error) {
	return r.lookupTransactionWithConflictCode(ctx, tx, commandID, requestHash, bizType, xerr.LedgerConflict)
}

func (r *Repository) lookupTransactionWithConflictCode(ctx context.Context, tx *sql.Tx, commandID string, requestHash string, bizType string, conflictCode xerr.Code) (transactionRow, bool, error) {
	row := tx.QueryRowContext(ctx,
		`SELECT transaction_id, request_hash, biz_type, COALESCE(CAST(metadata_json AS CHAR), '{}')
		 FROM wallet_transactions
		 WHERE app_code = ? AND command_id = ?
		 FOR UPDATE`,
		appcode.FromContext(ctx),
		commandID,
	)

	var txRow transactionRow
	var storedHash string
	var storedBizType string
	if err := row.Scan(&txRow.TransactionID, &storedHash, &storedBizType, &txRow.MetadataJSON); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return transactionRow{}, false, nil
		}
		return transactionRow{}, false, err
	}
	if storedHash != requestHash || storedBizType != bizType {

		return transactionRow{}, true, xerr.New(conflictCode, "wallet command idempotency conflict")
	}
	return txRow, true, nil
}

// lookupTransactionConsistent 不对不存在的唯一键做 gap lock，供能在 INSERT duplicate 后安全回读的命令使用。
// 这避免两个同 command 首发都持 gap lock、再以相反顺序等待业务行锁和唯一键插入造成死锁。
func (r *Repository) lookupTransactionConsistent(ctx context.Context, tx *sql.Tx, commandID string, requestHash string, bizType string, conflictCode xerr.Code) (transactionRow, bool, error) {
	row := tx.QueryRowContext(ctx,
		`SELECT transaction_id, request_hash, biz_type, COALESCE(CAST(metadata_json AS CHAR), '{}')
		 FROM wallet_transactions
		 WHERE app_code = ? AND command_id = ?`,
		appcode.FromContext(ctx),
		commandID,
	)
	var txRow transactionRow
	var storedHash string
	var storedBizType string
	if err := row.Scan(&txRow.TransactionID, &storedHash, &storedBizType, &txRow.MetadataJSON); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return transactionRow{}, false, nil
		}
		return transactionRow{}, false, err
	}
	if storedHash != requestHash || storedBizType != bizType {
		return transactionRow{}, true, xerr.New(conflictCode, "wallet command idempotency conflict")
	}
	return txRow, true, nil
}

func (r *Repository) insertTransaction(ctx context.Context, tx *sql.Tx, transactionID string, commandID string, bizType string, requestHash string, externalRef string, metadata any, nowMs int64) error {
	metadataJSON, err := json.Marshal(metadata)
	if err != nil {
		return err
	}
	_, err = tx.ExecContext(ctx,
		`INSERT INTO wallet_transactions (
		   app_code, transaction_id, command_id, biz_type, status, request_hash, external_ref, metadata_json, created_at_ms, updated_at_ms
		 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		appcode.FromContext(ctx),
		transactionID,
		commandID,
		bizType,
		transactionStatusSucceeded,
		requestHash,
		externalRef,
		string(metadataJSON),
		nowMs,
		nowMs,
	)
	return err
}

// updateTransactionMetadata replaces the provisional transaction snapshot after every tracked account
// mutation has succeeded. Gift transactions must be inserted before ledger entries for referential and
// idempotency ordering, but balance_version is authoritative only after applyTrackedAccountDelta has
// advanced the locked account state. Persisting the final JSON in the same transaction makes a later
// command_id replay return the exact original balance and version instead of reading mutable live state.
func (r *Repository) updateTransactionMetadata(ctx context.Context, tx *sql.Tx, transactionID string, metadata any, nowMS int64) error {
	metadataJSON, err := json.Marshal(metadata)
	if err != nil {
		return err
	}
	result, err := tx.ExecContext(ctx,
		`UPDATE wallet_transactions
		 SET metadata_json = ?, updated_at_ms = ?
		 WHERE app_code = ? AND transaction_id = ?`,
		string(metadataJSON),
		nowMS,
		appcode.FromContext(ctx),
		transactionID,
	)
	if err != nil {
		return err
	}
	affected, err := result.RowsAffected()
	if err != nil {
		return err
	}
	// go-sql-driver/mysql 默认返回 changed rows，不返回 matched rows。当最终 metadata
	// 与插入时的预测值完全一致时，这条 UPDATE 合法命中主键但 affected=0；交易行已在
	// 同一事务中由 insertTransaction 成功插入，因此只能把多行更新视为不变量破坏。
	if affected > 1 {
		return xerr.New(xerr.Internal, "wallet transaction metadata update lost its transaction")
	}
	return nil
}
