import { SuiClient, SuiExecutionResult, SuiTransactionBlockResponse } from '@mysten/sui/client'
import { Keypair } from '@mysten/sui/cryptography'
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'
import { Transaction } from '@mysten/sui/transactions'
import { fromBase64 } from '@mysten/sui/utils'

import { MoveAbortError, SimulateResult, UnclassifiedError } from '../types'

export async function simulateTransaction(
    suiClient: SuiClient,
    tx: Transaction,
    sender?: string
): Promise<SimulateResult[]> {
    const resolvedSender = determineSenderAddress(tx, sender)
    const { results, error } = await suiClient.devInspectTransactionBlock({
        transactionBlock: tx,
        sender: resolvedSender,
    })

    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
    if (error != undefined && error != null) {
        throw handleError(error)
    }
    if (results === undefined || results === null) {
        throw new Error('No results found')
    }
    const lastCommandResults: SuiExecutionResult = results[results.length - 1]

    if (lastCommandResults.returnValues === undefined) {
        throw new Error('No return values found')
    }
    return lastCommandResults.returnValues.map((result) => {
        return {
            value: typeof result[0] === 'string' ? fromBase64(result[0]) : new Uint8Array(result[0]),
            type: result[1],
        }
    })
}

export async function simulateTransactionMultiResult(
    suiClient: SuiClient,
    tx: Transaction,
    resultIndex: number[],
    sender?: string
): Promise<SimulateResult[][]> {
    const resolvedSender = determineSenderAddress(tx, sender)
    const { results, error } = await suiClient.devInspectTransactionBlock({
        transactionBlock: tx,
        sender: resolvedSender,
    })

    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
    if (error != undefined && error != null) {
        throw handleError(error)
    }
    if (results === undefined || results === null) {
        throw new Error('No results found')
    }

    const selectedResults: SuiExecutionResult[] = resultIndex.map((idx) => {
        return results[idx]
    })

    return selectedResults.map((result) => {
        if (result.returnValues === undefined) {
            throw new Error('No return values found')
        }
        return result.returnValues.map((result) => {
            return {
                value: typeof result[0] === 'string' ? fromBase64(result[0]) : new Uint8Array(result[0]),
                type: result[1],
            }
        })
    })
}

export async function validateTransaction(
    client: SuiClient,
    signer: Keypair,
    tx: Transaction
): Promise<SuiTransactionBlockResponse> {
    tx.setSenderIfNotSet(signer.getPublicKey().toSuiAddress())
    const result = await client.signAndExecuteTransaction({
        signer,
        transaction: tx,
        options: {
            showEffects: true,
            showObjectChanges: true,
            showEvents: true,
        },
    })

    if (result.effects?.status.status !== 'success') {
        throw new Error(result.effects?.status.error)
    }

    await client.waitForTransaction({ digest: result.digest })
    return result
}

/**
 * Execute simulate using a moveCall function
 * @param client - The Sui client
 * @param moveCallFn - Function that adds moveCall to the transaction (supports both sync and async, any return type)
 * @param parser - Function to parse the result
 * @returns The parsed result
 */
export async function executeSimulate<T>(
    client: SuiClient,
    moveCallFn: (tx: Transaction) => unknown,
    parser: (result: { value: Uint8Array }[]) => T
): Promise<T> {
    const tx = new Transaction()
    await moveCallFn(tx)
    const result = await simulateTransaction(client, tx)
    return parser(result)
}

/**
 * Determines the appropriate sender address using fallback hierarchy:
 * 1. Use provided sender if available
 * 2. Use transaction's existing sender if valid
 * 3. Generate a random sender as last resort
 *
 * @param tx - The transaction to get sender from
 * @param preferredSender - Optional sender address to prioritize
 * @returns The determined sender address
 */
function determineSenderAddress(tx: Transaction, preferredSender?: string): string {
    //eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
    if (preferredSender) return preferredSender

    const existingSender = tx.getData().sender
    if (typeof existingSender === 'string') return existingSender

    return Ed25519Keypair.generate().toSuiAddress()
}

export function handleError(e: string): unknown {
    if (e.includes('ExecutionError')) {
        const majorStatusMatch = e.match(/major_status:\s([A-Z_]+)/)
        if (majorStatusMatch !== null) {
            const major_status = majorStatusMatch[1]
            if (major_status === 'ABORTED') {
                const subStatusMatch = e.match(/sub_status:\sSome\((\d+)\)/)
                if (subStatusMatch === null) {
                    return new MoveAbortError(Number.NEGATIVE_INFINITY, e)
                } else {
                    return new MoveAbortError(Number(subStatusMatch[1]), e)
                }
            }
        } else {
            return new UnclassifiedError(e)
        }
    }
    return new Error(e)
}
