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

import { asObject, executeSimulate } from '../utils'

const DEFAULT_MODULE_NAME = 'call'

export const CallErrorCode = {
    // Call related errors (matching call.move)
    Call_ECallNotActive: 1,
    Call_ECallNotCompleted: 2,
    Call_ECallNotCreating: 3,
    Call_ECallNotRoot: 4,
    Call_ECallNotWaiting: 5,
    Call_EInvalidChild: 6,
    Call_EInvalidNonce: 7,
    Call_EInvalidParent: 8,
    Call_EParameterNotMutable: 9,
    Call_EUnauthorized: 10,

    // CallCap related errors (matching call_cap.move)
    CallCap_EBadWitness: 1,

    // MultiCall related errors (matching dynamic-call/multi-call/multi_call.move)
    MultiCall_ENoMoreCalls: 1,
    MultiCall_EUnauthorized: 2,
} as const

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

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

    // === Set function ===

    /**
     * Create a new CallCap for a user (direct interaction)
     * Creates a user-based CallCap that uses its own UID as the identifier
     * @param tx - The transaction to add the move call to
     * @returns Transaction result containing the new individual CallCap
     */
    newIndividualCapMoveCall(tx: Transaction): TransactionResult {
        return tx.moveCall({
            target: this.#target('new_individual_cap', 'call_cap'),
            arguments: [],
        })
    }

    /**
     * Create a void result for calls with no meaningful return value
     * @param tx - The transaction to add the move call to
     * @returns Transaction result containing a void result
     */
    voidMoveCall(tx: Transaction): TransactionResult {
        return tx.moveCall({
            target: this.#target('void'),
            arguments: [],
        })
    }

    // === View function ===

    /**
     * Get the result from a call
     * @param tx - The transaction to add the move call to
     * @param paramType - The parameter type for the call
     * @param resultType - The result type for the call
     * @param call - The call transaction result or transaction argument
     * @returns Transaction result containing the call result
     */
    resultMoveCall(
        tx: Transaction,
        paramType: string,
        resultType: string,
        call: TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('result'),
            typeArguments: [paramType, resultType],
            arguments: [call],
        })
    }

    /**
     * Get the recipient from a call
     * @param tx - The transaction to add the move call to
     * @param paramType - The parameter type for the call
     * @param resultType - The result type for the call
     * @param call - The call transaction result or transaction argument
     * @returns Transaction result containing the call recipient
     */
    recipientMoveCall(
        tx: Transaction,
        paramType: string,
        resultType: string,
        call: TransactionArgument
    ): TransactionResult {
        return tx.moveCall({
            target: this.#target('recipient'),
            typeArguments: [paramType, resultType],
            arguments: [call],
        })
    }

    /**
     * Get the unique identifier for a CallCap
     * Returns the appropriate identifier based on the source type
     * @param tx - The transaction to add the move call to
     * @param callCap - The CallCap object ID or transaction argument
     * @returns Transaction result containing the CallCap identifier
     */
    callCapIdMoveCall(tx: Transaction, callCap: string | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('id', 'call_cap'),
            arguments: [asObject(tx, callCap)],
        })
    }

    /**
     * Get the unique identifier for a CallCap
     * Returns the appropriate identifier based on the source type
     * @param callCap - The CallCap object ID
     * @returns Promise resolving to the CallCap identifier as a string
     */
    async getCallCapId(callCap: string): Promise<string> {
        return executeSimulate(
            this.client,
            (tx) => {
                this.callCapIdMoveCall(tx, callCap)
            },
            (result) => bcs.Address.parse(result[0].value)
        )
    }

    /**
     * Check if this is an Individual CallCap
     * @param tx - The transaction to add the move call to
     * @param callCap - The CallCap object ID or transaction argument
     * @returns Transaction result containing a boolean indicating if it's an Individual CallCap
     */
    isIndividualCapMoveCall(tx: Transaction, callCap: string | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('is_individual', 'call_cap'),
            arguments: [asObject(tx, callCap)],
        })
    }

    /**
     * Check if this is an Individual CallCap
     * @param callCap - The CallCap object ID
     * @returns Promise resolving to true if it's an Individual CallCap
     */
    async isIndividualCap(callCap: string): Promise<boolean> {
        return executeSimulate(
            this.client,
            (tx) => {
                this.isIndividualCapMoveCall(tx, callCap)
            },
            (result) => bcs.Bool.parse(result[0].value)
        )
    }

    /**
     * Check if this is a Package CallCap
     * @param tx - The transaction to add the move call to
     * @param callCap - The CallCap object ID or transaction argument
     * @returns Transaction result containing a boolean indicating if it's a Package CallCap
     */
    isPackageCapMoveCall(tx: Transaction, callCap: string | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('is_package', 'call_cap'),
            arguments: [asObject(tx, callCap)],
        })
    }

    /**
     * Check if this is a Package CallCap
     * @param callCap - The CallCap object ID
     * @returns Promise resolving to true if it's a Package CallCap
     */
    async isPackageCap(callCap: string): Promise<boolean> {
        return executeSimulate(
            this.client,
            (tx) => {
                this.isPackageCapMoveCall(tx, callCap)
            },
            (result) => bcs.Bool.parse(result[0].value)
        )
    }

    /**
     * Get the package address for a Package CallCap
     * Returns None if this is a User CallCap
     * @param tx - The transaction to add the move call to
     * @param callCap - The CallCap object ID or transaction argument
     * @returns Transaction result containing the package address option
     */
    packageAddressMoveCall(tx: Transaction, callCap: string | TransactionArgument): TransactionResult {
        return tx.moveCall({
            target: this.#target('package_address', 'call_cap'),
            arguments: [asObject(tx, callCap)],
        })
    }

    /**
     * Get the package address for a Package CallCap
     * Returns null if this is an Individual CallCap
     * @param callCap - The CallCap object ID
     * @returns Promise resolving to the package address or null if Individual CallCap
     */
    async getPackageAddress(callCap: string): Promise<string | null> {
        return executeSimulate(
            this.client,
            (tx) => {
                this.packageAddressMoveCall(tx, callCap)
            },
            (result) => {
                try {
                    // Try to parse as Option<address>
                    const option = bcs.option(bcs.Address).parse(result[0].value)
                    return option ?? null
                } catch {
                    // If parsing fails, it might be None
                    return null
                }
            }
        )
    }

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