import type { Address } from '@solana/addresses';
import type { ReadonlyUint8Array } from '@solana/codecs-core';
import {
    SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE,
    SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND,
    SOLANA_ERROR__TRANSACTION__INSTRUCTION_HEADERS_PAYLOADS_MISMATCH,
    SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED,
    SolanaError,
} from '@solana/errors';
import { type AccountMeta, AccountRole, type Instruction } from '@solana/instructions';
import type { CompiledTransactionMessage } from '@solana/transaction-messages';

import type { LoadedAddresses } from './loaded-addresses';

/**
 * An outer transaction instruction with its account indices resolved to
 * full {@link AccountMeta}s and its data exposed as a `ReadonlyUint8Array`.
 *
 * Following the kit `Instruction` conventions, `accounts` and `data` are
 * present only when non-empty, so `isInstructionWithAccounts` and
 * `isInstructionWithData` from `@solana/instructions` behave as expected
 * and can be used to narrow before passing the instruction to the
 * auto-generated `parseXInstruction` helpers.
 *
 * @example
 * ```ts
 * for (const ix of getInstructionsFromCompiledTransactionMessage(compiled)) {
 *     // `ix` is a `ResolvedInstruction` — usable with `isInstructionForProgram`
 *     // directly, and with the auto-generated `identifyXInstruction` helpers
 *     // after narrowing `data`.
 *     if (isInstructionWithData(ix)) {
 *         identifyTokenInstruction(ix);
 *     }
 * }
 * ```
 */
export type ResolvedInstruction<TProgramAddress extends string = string> = Instruction<
    TProgramAddress,
    readonly AccountMeta[]
>;

/**
 * The normalized shape of an instruction inside a compiled transaction
 * message — `legacy`, `0`, and `1` are all reduced to this form before
 * resolution.
 */
type NormalizedCompiledInstruction = Readonly<{
    accountIndices: readonly number[];
    data: ReadonlyUint8Array;
    programAddressIndex: number;
}>;

/**
 * Builds the full ordered list of {@link AccountMeta}s for a compiled
 * transaction message.
 *
 * The order matches the runtime's resolution order:
 *
 * 1. Static accounts, with role bits derived from the message header
 *    (writable signers, readonly signers, writable non-signers, readonly
 *    non-signers).
 * 2. ALT-loaded writable accounts (always non-signer, writable).
 * 3. ALT-loaded readonly accounts (always non-signer, readonly).
 *
 * Inner-instruction account indices reference the same flat list, so this
 * helper is also useful for resolving inner instructions.
 */
export function getAccountMetasFromCompiledTransactionMessage(
    compiledMessage: CompiledTransactionMessage,
    loadedAddresses?: LoadedAddresses | null,
): AccountMeta[] {
    const { header, staticAccounts } = compiledMessage;
    const numWritableSignerAccounts = header.numSignerAccounts - header.numReadonlySignerAccounts;
    const numWritableNonSignerAccounts =
        staticAccounts.length - header.numSignerAccounts - header.numReadonlyNonSignerAccounts;

    const metas: AccountMeta[] = [];
    let i = 0;
    for (let n = 0; n < numWritableSignerAccounts; n++, i++) {
        metas.push({ address: staticAccounts[i], role: AccountRole.WRITABLE_SIGNER });
    }
    for (let n = 0; n < header.numReadonlySignerAccounts; n++, i++) {
        metas.push({ address: staticAccounts[i], role: AccountRole.READONLY_SIGNER });
    }
    for (let n = 0; n < numWritableNonSignerAccounts; n++, i++) {
        metas.push({ address: staticAccounts[i], role: AccountRole.WRITABLE });
    }
    for (let n = 0; n < header.numReadonlyNonSignerAccounts; n++, i++) {
        metas.push({ address: staticAccounts[i], role: AccountRole.READONLY });
    }

    if (loadedAddresses) {
        for (const address of loadedAddresses.writable) {
            metas.push({ address, role: AccountRole.WRITABLE });
        }
        for (const address of loadedAddresses.readonly) {
            metas.push({ address, role: AccountRole.READONLY });
        }
    }

    return metas;
}

/**
 * Returns the outer instructions of a compiled transaction message as kit
 * {@link Instruction} objects.
 *
 * Each returned instruction has its account indices resolved to
 * {@link AccountMeta}s (with the proper signer/writable bits) and its data
 * exposed as a `ReadonlyUint8Array` — the form the auto-generated
 * `@solana-program/*` `parseXInstruction` functions expect. `accounts` and
 * `data` are omitted when empty.
 *
 * Supports `legacy`, `v0`, and `v1` compiled messages. Throws
 * {@link SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED} for any
 * other version,
 * {@link SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND}
 * if a `programAddressIndex` falls outside the resolved account list, and
 * {@link SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE}
 * if an account index does.
 *
 * @example
 * ```ts
 * const instructions = getInstructionsFromCompiledTransactionMessage(
 *     compiled,
 *     rpcResponse.meta?.loadedAddresses,
 * );
 * for (const ix of instructions) {
 *     if (ix.programAddress === TOKEN_PROGRAM_ADDRESS && isInstructionWithData(ix)) {
 *         const kind = identifyTokenInstruction(ix);
 *         // ...
 *     }
 * }
 * ```
 */
export function getInstructionsFromCompiledTransactionMessage(
    compiledMessage: CompiledTransactionMessage,
    loadedAddresses?: LoadedAddresses | null,
): ResolvedInstruction[] {
    const metas = getAccountMetasFromCompiledTransactionMessage(compiledMessage, loadedAddresses);
    return normalizeCompiledInstructions(compiledMessage).map(ix => resolveInstruction(ix, metas));
}

/**
 * Internal variant of {@link getInstructionsFromCompiledTransactionMessage}
 * that takes pre-built {@link AccountMeta}s. Used by {@link walkInstructions}
 * to avoid rebuilding the meta list when it is already needed for resolving
 * inner instructions.
 *
 * @internal
 */
export function getInstructionsFromCompiledTransactionMessageWithMetas(
    compiledMessage: CompiledTransactionMessage,
    accountMetas: readonly AccountMeta[],
): ResolvedInstruction[] {
    return normalizeCompiledInstructions(compiledMessage).map(ix => resolveInstruction(ix, accountMetas));
}

function normalizeCompiledInstructions(compiledMessage: CompiledTransactionMessage): NormalizedCompiledInstruction[] {
    if (compiledMessage.version === 'legacy' || compiledMessage.version === 0) {
        return compiledMessage.instructions.map(ix => ({
            accountIndices: ix.accountIndices ?? [],
            data: ix.data ?? new Uint8Array(),
            programAddressIndex: ix.programAddressIndex,
        }));
    }
    if (compiledMessage.version === 1) {
        const { instructionHeaders, instructionPayloads } = compiledMessage;
        if (instructionHeaders.length !== instructionPayloads.length) {
            throw new SolanaError(SOLANA_ERROR__TRANSACTION__INSTRUCTION_HEADERS_PAYLOADS_MISMATCH, {
                numInstructionHeaders: instructionHeaders.length,
                numInstructionPayloads: instructionPayloads.length,
            });
        }
        return instructionHeaders.map((header, i) => ({
            accountIndices: instructionPayloads[i].instructionAccountIndices,
            data: instructionPayloads[i].instructionData,
            programAddressIndex: header.programAccountIndex,
        }));
    }
    // Compile-time exhaustiveness: if a future `CompiledTransactionMessage`
    // variant is added, `compiledMessage` will no longer narrow to `never`
    // here and this assignment will fail to typecheck — forcing us to handle
    // the new version explicitly.
    const _exhaustiveCheck: never = compiledMessage;
    throw new SolanaError(SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED, {
        unsupportedVersion: (_exhaustiveCheck as { version: number }).version,
    });
}

function resolveInstruction(ix: NormalizedCompiledInstruction, metas: readonly AccountMeta[]): ResolvedInstruction {
    const programMeta = metas[ix.programAddressIndex];
    if (!programMeta) {
        throw new SolanaError(SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND, {
            index: ix.programAddressIndex,
        });
    }
    const accounts: AccountMeta[] = ix.accountIndices.map(i => {
        const accountMeta = metas[i];
        if (!accountMeta) {
            throw new SolanaError(
                SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE,
                {
                    index: i,
                },
            );
        }
        return accountMeta;
    });
    return {
        ...(accounts.length ? { accounts } : null),
        ...(ix.data.byteLength ? { data: ix.data } : null),
        programAddress: programMeta.address as Address,
    };
}
