import { CoinStruct, SuiClient } from '@mysten/sui/client'
import { Transaction, TransactionArgument, TransactionResult } from '@mysten/sui/transactions'

import { asAddress, asBool, asBytes, asBytes32, asObject, asU128, asU16, asU256, asU32, asU64, asU8 } from '../utils'

const BYTES32_MODULE_NAME = 'bytes32'
const BUFFER_READER_MODULE_NAME = 'buffer_reader'
const BUFFER_WRITER_MODULE_NAME = 'buffer_writer'
const PACKAGE_MODULE_NAME = 'package'

export const UtilsErrorCode = {
    // Utils related errors (with Utils_ prefix)
    Utils_EInvalidLength: 1,
} as const

export class Utils {
    public packageId: string
    public readonly client: SuiClient

    constructor(packageId: string, client: SuiClient) {
        this.packageId = packageId
        this.client = client
    }

    // === bytes32 Functions ===

    /**
     * Create a bytes32 from a byte array
     * @param tx - The transaction to add the move call to
     * @param peer - The byte array to convert to bytes32 or transaction argument
     * @returns Transaction result containing the bytes32 value
     */
    fromBytesMoveCall(tx: Transaction, peer: Uint8Array | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('from_bytes'),
            arguments: [asBytes(tx, peer)],
        })
    }

    /**
     * Create a bytes32 from a byte array with left padding
     * @param tx - The transaction to add the move call to
     * @param bytes - The byte array to convert with left padding or transaction argument
     * @returns Transaction result containing the left-padded bytes32 value
     */
    fromBytesLeftPaddedMoveCall(tx: Transaction, bytes: Uint8Array | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('from_bytes_left_padded'),
            arguments: [asBytes(tx, bytes)],
        })
    }

    /**
     * Create a bytes32 from a byte array with right padding
     * @param tx - The transaction to add the move call to
     * @param bytes - The byte array to convert with right padding or transaction argument
     * @returns Transaction result containing the right-padded bytes32 value
     */
    fromBytesRightPaddedMoveCall(tx: Transaction, bytes: Uint8Array | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('from_bytes_right_padded'),
            arguments: [asBytes(tx, bytes)],
        })
    }

    /**
     * Create a bytes32 from an address
     * @param tx - The transaction to add the move call to
     * @param address - The address to convert to bytes32 or transaction argument
     * @returns Transaction result containing the bytes32 representation of the address
     */
    fromAddressMoveCall(tx: Transaction, address: string | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('from_address'),
            arguments: [asAddress(tx, address)],
        })
    }

    /**
     * Create a bytes32 from an object ID
     * @param tx - The transaction to add the move call to
     * @param id - The object ID to convert to bytes32 or transaction argument
     * @returns Transaction result containing the bytes32 representation of the ID
     */
    fromIdMoveCall(tx: Transaction, id: string | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('from_id'),
            arguments: [asObject(tx, id)],
        })
    }

    /**
     * Create a zero bytes32 value (all zeros)
     * @param tx - The transaction to add the move call to
     * @returns Transaction result containing a zero bytes32
     */
    zeroBytes32MoveCall(tx: Transaction): TransactionResult {
        return tx.moveCall({
            target: this.#target('zero_bytes32'),
        })
    }

    /**
     * Create a bytes32 with all bits set to 1 (0xff...)
     * @param tx - The transaction to add the move call to
     * @returns Transaction result containing a bytes32 with all bits set
     */
    ffBytes32MoveCall(tx: Transaction): TransactionResult {
        return tx.moveCall({
            target: this.#target('ff_bytes32'),
        })
    }

    /**
     * Check if a bytes32 value is zero (all zeros)
     * @param tx - The transaction to add the move call to
     * @param bytes32 - The bytes32 value to check or transaction argument
     * @returns Transaction result containing a boolean indicating if the value is zero
     */
    isZeroMoveCall(tx: Transaction, bytes32: TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('is_zero'),
            arguments: [bytes32],
        })
    }

    /**
     * Check if a bytes32 value has all bits set to 1
     * @param tx - The transaction to add the move call to
     * @param bytes32 - The bytes32 value to check or transaction argument
     * @returns Transaction result containing a boolean indicating if all bits are set
     */
    isFfMoveCall(tx: Transaction, bytes32: TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('is_ff'),
            arguments: [bytes32],
        })
    }

    /**
     * Convert a bytes32 to a byte array
     * @param tx - The transaction to add the move call to
     * @param bytes32 - The bytes32 value to convert or transaction argument
     * @returns Transaction result containing the byte array representation
     */
    toBytesMoveCall(tx: Transaction, bytes32: TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('to_bytes'),
            arguments: [bytes32],
        })
    }

    /**
     * Convert a bytes32 to an address
     * @param tx - The transaction to add the move call to
     * @param bytes32 - The bytes32 value to convert or transaction argument
     * @returns Transaction result containing the address representation
     */
    toAddressMoveCall(tx: Transaction, bytes32: TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('to_address'),
            arguments: [bytes32],
        })
    }

    /**
     * Convert a bytes32 to an object ID
     * @param tx - The transaction to add the move call to
     * @param bytes32 - The bytes32 value to convert or transaction argument
     * @returns Transaction result containing the object ID representation
     */
    toIdMoveCall(tx: Transaction, bytes32: TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('to_id'),
            arguments: [bytes32],
        })
    }

    // === Reader Buffer Functions ===

    /**
     * Create a new buffer reader from a byte array
     * @param tx - The transaction to add the move call to
     * @param buffer - The byte array to create the reader from or transaction argument
     * @returns Transaction result containing the buffer reader
     */
    newReaderMoveCall(tx: Transaction, buffer: Uint8Array | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('create', BUFFER_READER_MODULE_NAME),
            arguments: [asBytes(tx, buffer)],
        })
    }

    /**
     * Get the current position of the buffer reader
     * @param tx - The transaction to add the move call to
     * @param reader - The buffer reader instance or transaction argument
     * @returns Transaction result containing the current position
     */
    positionMoveCall(tx: Transaction, reader: TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('position', BUFFER_READER_MODULE_NAME),
            arguments: [reader],
        })
    }

    /**
     * Get the remaining length of data in the buffer reader
     * @param tx - The transaction to add the move call to
     * @param reader - The buffer reader instance or transaction argument
     * @returns Transaction result containing the remaining length
     */
    remainingLengthMoveCall(tx: Transaction, reader: TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('remaining_length', BUFFER_READER_MODULE_NAME),
            arguments: [reader],
        })
    }

    /**
     * Skip a specified number of bytes in the buffer reader
     * @param tx - The transaction to add the move call to
     * @param reader - The buffer reader instance or transaction argument
     * @param len - The number of bytes to skip or transaction argument
     * @returns Transaction result containing the updated reader
     */
    skipMoveCall(tx: Transaction, reader: TransactionArgument, len: number | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('skip', BUFFER_READER_MODULE_NAME),
            arguments: [reader, asU64(tx, len)],
        })
    }

    /**
     * Set the position of the buffer reader
     * @param tx - The transaction to add the move call to
     * @param reader - The buffer reader instance or transaction argument
     * @param position - The position to set or transaction argument
     * @returns Transaction result containing the updated reader
     */
    setPositionMoveCall(
        tx: Transaction,
        reader: TransactionArgument,
        position: number | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('set_position', BUFFER_READER_MODULE_NAME),
            arguments: [reader, asU64(tx, position)],
        })
    }

    /**
     * Rewind the buffer reader by a specified number of bytes
     * @param tx - The transaction to add the move call to
     * @param reader - The buffer reader instance or transaction argument
     * @param len - The number of bytes to rewind or transaction argument
     * @returns Transaction result containing the updated reader
     */
    rewindMoveCall(tx: Transaction, reader: TransactionArgument, len: number | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('rewind', BUFFER_READER_MODULE_NAME),
            arguments: [reader, asU64(tx, len)],
        })
    }

    /**
     * Read a boolean value from the buffer reader
     * @param tx - The transaction to add the move call to
     * @param reader - The buffer reader instance
     * @returns Transaction result containing the boolean value
     */
    readBoolMoveCall(tx: Transaction, reader: TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('read_bool', BUFFER_READER_MODULE_NAME),
            arguments: [reader],
        })
    }

    /**
     * Read a u8 value from the buffer reader
     * @param tx - The transaction to add the move call to
     * @param reader - The buffer reader instance
     * @returns Transaction result containing the u8 value
     */
    readU8MoveCall(tx: Transaction, reader: TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('read_u8', BUFFER_READER_MODULE_NAME),
            arguments: [reader],
        })
    }

    /**
     * Read a u16 value from the buffer reader
     * @param tx - The transaction to add the move call to
     * @param reader - The buffer reader instance
     * @returns Transaction result containing the u16 value
     */
    readU16MoveCall(tx: Transaction, reader: TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('read_u16', BUFFER_READER_MODULE_NAME),
            arguments: [reader],
        })
    }

    /**
     * Read a u32 value from the buffer reader
     * @param tx - The transaction to add the move call to
     * @param reader - The buffer reader instance
     * @returns Transaction result containing the u32 value
     */
    readU32MoveCall(tx: Transaction, reader: TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('read_u32', BUFFER_READER_MODULE_NAME),
            arguments: [reader],
        })
    }

    /**
     * Read a u64 value from the buffer reader
     * @param tx - The transaction to add the move call to
     * @param reader - The buffer reader instance
     * @returns Transaction result containing the u64 value
     */
    readU64MoveCall(tx: Transaction, reader: TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('read_u64', BUFFER_READER_MODULE_NAME),
            arguments: [reader],
        })
    }

    /**
     * Read a u128 value from the buffer reader
     * @param tx - The transaction to add the move call to
     * @param reader - The buffer reader instance
     * @returns Transaction result containing the u128 value
     */
    readU128MoveCall(tx: Transaction, reader: TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('read_u128', BUFFER_READER_MODULE_NAME),
            arguments: [reader],
        })
    }

    /**
     * Read a u256 value from the buffer reader
     * @param tx - The transaction to add the move call to
     * @param reader - The buffer reader instance
     * @returns Transaction result containing the u256 value
     */
    readU256MoveCall(tx: Transaction, reader: TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('read_u256', BUFFER_READER_MODULE_NAME),
            arguments: [reader],
        })
    }

    /**
     * Read a bytes32 value from the buffer reader
     * @param tx - The transaction to add the move call to
     * @param reader - The buffer reader instance
     * @returns Transaction result containing the bytes32 value
     */
    readBytes32MoveCall(tx: Transaction, reader: TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('read_bytes32', BUFFER_READER_MODULE_NAME),
            arguments: [reader],
        })
    }

    /**
     * Read an address value from the buffer reader
     * @param tx - The transaction to add the move call to
     * @param reader - The buffer reader instance
     * @returns Transaction result containing the address value
     */
    readAddressMoveCall(tx: Transaction, reader: TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('read_address', BUFFER_READER_MODULE_NAME),
            arguments: [reader],
        })
    }

    /**
     * Read a fixed-length byte array from the buffer reader
     * @param tx - The transaction to add the move call to
     * @param reader - The buffer reader instance or transaction argument
     * @param len - The length of bytes to read or transaction argument
     * @returns Transaction result containing the byte array
     */
    readFixedLenBytesMoveCall(
        tx: Transaction,
        reader: TransactionArgument,
        len: number | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('read_fixed_len_bytes', BUFFER_READER_MODULE_NAME),
            arguments: [reader, asU64(tx, len)],
        })
    }

    /**
     * Read all remaining bytes from the buffer reader until the end
     * @param tx - The transaction to add the move call to
     * @param reader - The buffer reader instance or transaction argument
     * @returns Transaction result containing the remaining bytes
     */
    readBytesUntilEndMoveCall(tx: Transaction, reader: TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('read_bytes_until_end', BUFFER_READER_MODULE_NAME),
            arguments: [reader],
        })
    }

    /**
     * Get the total buffer length of the reader
     * @param tx - The transaction to add the move call to
     * @param reader - The buffer reader instance or transaction argument
     * @returns Transaction result containing the total buffer length
     */
    readerBufferLengthMoveCall(tx: Transaction, reader: TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('length', BUFFER_READER_MODULE_NAME),
            arguments: [reader],
        })
    }

    /**
     * Convert the buffer reader to a byte array
     * @param tx - The transaction to add the move call to
     * @param reader - The buffer reader instance or transaction argument
     * @returns Transaction result containing the byte array representation
     */
    readerToBytesMoveCall(tx: Transaction, reader: TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('to_bytes', BUFFER_READER_MODULE_NAME),
            arguments: [reader],
        })
    }

    // Writer functions

    /**
     * Create a new empty buffer writer
     * @param tx - The transaction to add the move call to
     * @returns Transaction result containing the buffer writer
     */
    newWriterMoveCall(tx: Transaction): TransactionResult {
        return tx.moveCall({
            target: this.#target('new', BUFFER_WRITER_MODULE_NAME),
        })
    }

    /**
     * Create a buffer writer with initial data
     * @param tx - The transaction to add the move call to
     * @param buffer - The initial byte array data for the writer or transaction argument
     * @returns Transaction result containing the buffer writer
     */
    createWriterMoveCall(tx: Transaction, buffer: Uint8Array | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('create', BUFFER_WRITER_MODULE_NAME),
            arguments: [asBytes(tx, buffer)],
        })
    }

    /**
     * Get the total buffer length of the writer
     * @param tx - The transaction to add the move call to
     * @param writer - The buffer writer instance
     * @returns Transaction result containing the total buffer length
     */
    writerBufferLengthMoveCall(tx: Transaction, writer: TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('length', BUFFER_WRITER_MODULE_NAME),
            arguments: [writer],
        })
    }

    /**
     * Convert the buffer writer to a byte array
     * @param tx - The transaction to add the move call to
     * @param writer - The buffer writer instance
     * @returns Transaction result containing the byte array representation
     */
    writerToBytesMoveCall(tx: Transaction, writer: TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('to_bytes', BUFFER_WRITER_MODULE_NAME),
            arguments: [writer],
        })
    }

    /**
     * Write a boolean value to the buffer writer
     * @param tx - The transaction to add the move call to
     * @param writer - The buffer writer instance or transaction argument
     * @param value - The boolean value to write or transaction argument
     * @returns Transaction result containing the updated writer
     */
    writeBoolMoveCall(
        tx: Transaction,
        writer: TransactionArgument,
        value: boolean | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('write_bool', BUFFER_WRITER_MODULE_NAME),
            arguments: [writer, asBool(tx, value)],
        })
    }

    /**
     * Write a u8 value to the buffer writer
     * @param tx - The transaction to add the move call to
     * @param writer - The buffer writer instance or transaction argument
     * @param value - The u8 value to write or transaction argument
     * @returns Transaction result containing the updated writer
     */
    writeU8MoveCall(
        tx: Transaction,
        writer: TransactionArgument,
        value: number | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('write_u8', BUFFER_WRITER_MODULE_NAME),
            arguments: [writer, asU8(tx, value)],
        })
    }

    /**
     * Write a u16 value to the buffer writer
     * @param tx - The transaction to add the move call to
     * @param writer - The buffer writer instance or transaction argument
     * @param value - The u16 value to write or transaction argument
     * @returns Transaction result containing the updated writer
     */
    writeU16MoveCall(
        tx: Transaction,
        writer: TransactionArgument,
        value: number | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('write_u16', BUFFER_WRITER_MODULE_NAME),
            arguments: [writer, asU16(tx, value)],
        })
    }

    /**
     * Write a u32 value to the buffer writer
     * @param tx - The transaction to add the move call to
     * @param writer - The buffer writer instance or transaction argument
     * @param value - The u32 value to write or transaction argument
     * @returns Transaction result containing the updated writer
     */
    writeU32MoveCall(
        tx: Transaction,
        writer: TransactionArgument,
        value: number | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('write_u32', BUFFER_WRITER_MODULE_NAME),
            arguments: [writer, asU32(tx, value)],
        })
    }

    /**
     * Write a u64 value to the buffer writer
     * @param tx - The transaction to add the move call to
     * @param writer - The buffer writer instance or transaction argument
     * @param value - The u64 value to write or transaction argument
     * @returns Transaction result containing the updated writer
     */
    writeU64MoveCall(
        tx: Transaction,
        writer: TransactionArgument,
        value: bigint | number | string | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('write_u64', BUFFER_WRITER_MODULE_NAME),
            arguments: [writer, asU64(tx, value)],
        })
    }

    /**
     * Write a u128 value to the buffer writer
     * @param tx - The transaction to add the move call to
     * @param writer - The buffer writer instance or transaction argument
     * @param value - The u128 value to write or transaction argument
     * @returns Transaction result containing the updated writer
     */
    writeU128MoveCall(
        tx: Transaction,
        writer: TransactionArgument,
        value: bigint | number | string | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('write_u128', BUFFER_WRITER_MODULE_NAME),
            arguments: [writer, asU128(tx, value)],
        })
    }

    /**
     * Write a u256 value to the buffer writer
     * @param tx - The transaction to add the move call to
     * @param writer - The buffer writer instance or transaction argument
     * @param value - The u256 value to write or transaction argument
     * @returns Transaction result containing the updated writer
     */
    writeU256MoveCall(
        tx: Transaction,
        writer: TransactionArgument,
        value: bigint | number | string | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('write_u256', BUFFER_WRITER_MODULE_NAME),
            arguments: [writer, asU256(tx, value)],
        })
    }

    /**
     * Write a byte array to the buffer writer
     * @param tx - The transaction to add the move call to
     * @param writer - The buffer writer instance or transaction argument
     * @param bytes - The byte array to write or transaction argument
     * @returns Transaction result containing the updated writer
     */
    writeBytesMoveCall(
        tx: Transaction,
        writer: TransactionArgument,
        bytes: Uint8Array | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('write_bytes', BUFFER_WRITER_MODULE_NAME),
            arguments: [writer, asBytes(tx, bytes)],
        })
    }

    /**
     * Write an address to the buffer writer
     * @param tx - The transaction to add the move call to
     * @param writer - The buffer writer instance or transaction argument
     * @param address - The address to write or transaction argument
     * @returns Transaction result containing the updated writer
     */
    writeAddressMoveCall(
        tx: Transaction,
        writer: TransactionArgument,
        address: string | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('write_address', BUFFER_WRITER_MODULE_NAME),
            arguments: [writer, asAddress(tx, address)],
        })
    }

    /**
     * Write a bytes32 value to the buffer writer
     * @param tx - The transaction to add the move call to
     * @param writer - The buffer writer instance or transaction argument
     * @param bytes32 - The bytes32 value to write (as Uint8Array) or transaction argument
     * @returns Transaction result containing the updated writer
     */
    writeBytes32MoveCall(
        tx: Transaction,
        writer: TransactionArgument,
        bytes32: Uint8Array | TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('write_bytes32', BUFFER_WRITER_MODULE_NAME),
            arguments: [writer, asBytes32(tx, bytes32, this)],
        })
    }

    // === Package Functions ===

    /**
     * Get the original package address where a type was first defined
     * @param tx - The transaction to add the move call to
     * @param typeArgument - The type to get the original package address for
     * @returns Transaction result containing the original package address
     */
    originalPackageOfTypeMoveCall(tx: Transaction, typeArgument: string): TransactionResult {
        return tx.moveCall({
            target: this.#target('original_package_of_type', PACKAGE_MODULE_NAME),
            typeArguments: [typeArgument],
        })
    }

    /**
     * Get the current package address where a type is defined
     * @param tx - The transaction to add the move call to
     * @param typeArgument - The type to get the current package address for
     * @returns Transaction result containing the current package address
     */
    packageOfTypeMoveCall(tx: Transaction, typeArgument: string): TransactionResult {
        return tx.moveCall({
            target: this.#target('package_of_type', PACKAGE_MODULE_NAME),
            typeArguments: [typeArgument],
        })
    }

    // === Other Package Utils Functions ===

    /**
     * Create an Option<Coin<SUI>> from a value
     * @param tx - The transaction to add the move call to
     * @param value - The amount of SUI to wrap in the option
     * @returns Transaction result containing option::none for zero values, option::some otherwise
     * @note TransactionArguments always create option::some (runtime values cannot be evaluated)
     */
    createOptionSuiMoveCall(tx: Transaction, value: bigint | number | string | TransactionArgument): TransactionResult {
        if (this.isZeroValue(value)) {
            return this._createOptionNone(tx)
        }

        return this._createOptionSome(tx, value)
    }

    /**
     * Check if a static value (non-TransactionArgument) is zero
     */
    private isZeroValue(value: bigint | number | string | TransactionArgument): boolean {
        if (typeof value === 'bigint' || typeof value === 'number' || typeof value === 'string') {
            return this._isZeroValue(value)
        }

        return false
    }

    /**
     * Check if a primitive value equals zero
     */
    private _isZeroValue(value: bigint | number | string): boolean {
        switch (typeof value) {
            case 'bigint':
                return value === 0n
            case 'number':
                return value === 0
            case 'string':
                return value === '0' || Number(value) === 0
            default:
                return false
        }
    }

    /**
     * Create option::none for Coin<SUI>
     */
    private _createOptionNone(tx: Transaction): TransactionResult {
        return tx.moveCall({
            target: '0x1::option::none',
            typeArguments: ['0x2::coin::Coin<0x2::sui::SUI>'],
            arguments: [],
        })
    }

    /**
     * Create option::some for Coin<SUI>
     */
    private _createOptionSome(
        tx: Transaction,
        value: bigint | number | string | TransactionArgument
    ): TransactionResult {
        const coin = tx.splitCoins(tx.gas, [asU64(tx, value)])
        return tx.moveCall({
            target: '0x1::option::some',
            typeArguments: ['0x2::coin::Coin<0x2::sui::SUI>'],
            arguments: [coin],
        })
    }

    /**
     * Splits specified amount of coins from user's wallet
     * @param tx - The transaction to add the move call to
     * @param coinType - The type of coin to split
     * @param owner - Address of the user whose coins to split
     * @param amount - Amount of coins to split (in smallest units)
     * @param limit - Maximum total number of coins to collect across all pages (default: 200)
     * @param pageSize - Maximum number of coins to fetch per page (default: 50)
     * @returns Promise resolving to split coin as TransactionResult
     * @throws Error if insufficient coins balance or no coins found
     */
    async splitCoinMoveCall(
        tx: Transaction,
        coinType: string,
        owner: string,
        amount: bigint,
        limit = 200,
        pageSize = 50
    ): Promise<TransactionResult> {
        const sufficientCoins = await this.#fetchSufficientCoins(owner, coinType, amount, limit, pageSize)
        const totalBalance = sufficientCoins.reduce((sum, coin) => sum + BigInt(coin.balance), 0n)

        // Use single coin if available, otherwise merge multiple coins
        const primaryCoin = sufficientCoins.find((coin) => BigInt(coin.balance) >= amount) ?? sufficientCoins[0]
        const primaryCoinObj = tx.object(primaryCoin.coinObjectId)

        // Merge additional coins if needed
        if (primaryCoin === sufficientCoins[0] && sufficientCoins.length > 1) {
            tx.mergeCoins(
                primaryCoinObj,
                sufficientCoins.slice(1).map((coin) => tx.object(coin.coinObjectId))
            )
        }

        // Split the required amount
        const splitCoin = tx.splitCoins(primaryCoinObj, [asU64(tx, amount)])

        // Destroy zero-value remainder if total balance equals requested amount
        if (totalBalance === amount) {
            this.#destroyZeroCoin(tx, primaryCoinObj, coinType)
        }

        return splitCoin
    }

    // === Internal Functions ===

    /**
     * Generate the full target path for move calls
     * @param name - The function name to call
     * @param module_name - The module name (defaults to BYTES32_MODULE_NAME)
     * @returns The full module path for the move call
     * @private
     */
    #target(name: string, module_name = BYTES32_MODULE_NAME): string {
        return `${this.packageId}::${module_name}::${name}`
    }

    /**
     * Destroy a zero-value coin to clean up wallet state
     * @param tx - The transaction to add the move call to
     * @param coinObj - The coin object to destroy
     * @param coinType - The coin type
     * @private
     */
    #destroyZeroCoin(tx: Transaction, coinObj: TransactionArgument, coinType: string): void {
        tx.moveCall({
            target: '0x2::coin::destroy_zero',
            arguments: [coinObj],
            typeArguments: [coinType],
        })
    }

    /**
     * Fetches coins incrementally until sufficient balance is reached
     * This method paginates through the user's ZRO coins to collect enough balance
     * @param owner - Address of the user whose ZRO coins to fetch
     * @param coinType - ZRO coin type string
     * @param amount - Required amount to reach
     * @param limit - Maximum total number of coins to collect across all pages
     * @param pageSize - Maximum coins to fetch per page
     * @returns Promise resolving to array of coin objects with sufficient total balance
     */
    async #fetchSufficientCoins(
        owner: string,
        coinType: string,
        amount: bigint,
        limit: number,
        pageSize: number
    ): Promise<CoinStruct[]> {
        const coins: CoinStruct[] = []
        let accumulatedBalance = 0n
        let cursor: string | null = null

        do {
            const coinsResponse = await this.client.getCoins({ owner, coinType, cursor, limit: pageSize })
            if (coinsResponse.data.length === 0) {
                break
            }

            for (const coin of coinsResponse.data) {
                coins.push(coin)
                accumulatedBalance += BigInt(coin.balance)

                // Stop if we have enough balance
                if (accumulatedBalance >= amount) {
                    return coins
                }

                // Throw error if we have reached the maximum coin limit
                if (coins.length >= limit) {
                    throw new Error(
                        `Insufficient ${coinType} balance: reached maximum coin limit (${limit}) but still need ${amount - accumulatedBalance} more`
                    )
                }
            }

            cursor = coinsResponse.hasNextPage ? coinsResponse.nextCursor ?? null : null
        } while (cursor != null)

        // If we've exhausted all coins but still don't have sufficient balance
        if (accumulatedBalance < amount) {
            throw new Error(
                `Insufficient ${coinType} balance: only found ${accumulatedBalance} but need ${amount} (shortfall: ${amount - accumulatedBalance})`
            )
        }

        return coins
    }
}
