import { getBase58Encoder } from '@solana/codecs-strings';
import {
    SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE,
    SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND,
    SolanaError,
} from '@solana/errors';
import type { AccountMeta } from '@solana/instructions';
import type { TransactionForFullMetaInnerInstructionsUnparsed } from '@solana/rpc-types';

import type { TracedInstruction } from './types';

/**
 * The shape of an inner-instructions group as returned by any JSON-RPC method
 * that includes transaction `meta` when not using `jsonParsed` encoding (e.g.
 * `getTransaction`, `getTransactionsForAddress`, `getBlock`). Derived from the
 * shared leaf type in `@solana/rpc-types` via indexed access so the two stay in
 * sync, without coupling to a specific method's response envelope.
 */
type RpcInnerInstructionsGroup = TransactionForFullMetaInnerInstructionsUnparsed['innerInstructions'][number];

/**
 * The minimum shape of `getTransaction`'s `meta` field that this helper
 * needs. Accepting a structural type keeps callers free to pass the full
 * RPC response without coupling to a specific overload.
 *
 * @example
 * ```ts
 * const inner = getInnerInstructionsFromMeta(rpcResponse.meta, accountMetas);
 * ```
 */
export type MetaWithInnerInstructions = Readonly<{
    innerInstructions?: readonly RpcInnerInstructionsGroup[] | null;
}>;

/**
 * Returns the inner instructions in a `getTransaction` response as
 * {@link TracedInstruction}s.
 *
 * The RPC returns inner instructions in a different shape from the wire
 * format: indices reference the same flat account list as the outer
 * instructions, but `data` is a base58-encoded string. This helper decodes
 * the data, resolves the indices against the supplied {@link AccountMeta}
 * list, and tags each instruction with an `inner` trace.
 *
 * Throws if any `programIdIndex` or account index falls outside the
 * supplied `accountMetas` list.
 *
 * @example
 * ```ts
 * const accountMetas = getAccountMetasFromCompiledTransactionMessage(compiledMessage, loadedAddresses);
 * const inner = getInnerInstructionsFromMeta(rpcResponse.meta, accountMetas);
 * ```
 */
export function getInnerInstructionsFromMeta(
    meta: MetaWithInnerInstructions,
    accountMetas: readonly AccountMeta[],
): TracedInstruction[] {
    if (!meta.innerInstructions) return [];
    const base58 = getBase58Encoder();
    const result: TracedInstruction[] = [];
    for (const group of meta.innerInstructions) {
        for (let innerIndex = 0; innerIndex < group.instructions.length; innerIndex++) {
            const ix = group.instructions[innerIndex];
            const programMeta = accountMetas[ix.programIdIndex];
            if (!programMeta) {
                throw new SolanaError(
                    SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND,
                    { index: ix.programIdIndex },
                );
            }
            const accounts: AccountMeta[] = ix.accounts.map(i => {
                const accountMeta = accountMetas[i];
                if (!accountMeta) {
                    throw new SolanaError(
                        SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE,
                        { index: i },
                    );
                }
                return accountMeta;
            });
            const data = base58.encode(ix.data);
            result.push({
                ...(accounts.length ? { accounts } : null),
                ...(data.byteLength ? { data } : null),
                programAddress: programMeta.address,
                trace: {
                    innerIndex,
                    kind: 'inner',
                    outerIndex: group.index,
                    ...(ix.stackHeight != null ? { stackHeight: ix.stackHeight } : {}),
                },
            });
        }
    }
    return result;
}
