/* eslint-disable @typescript-eslint/require-await */

// 1. init native OFT from existing mint
// 2. init adapter OFT from existing mint, optionally an existing escrow
// 3. Wire a peer
// 4. Set the DVN etc. options
import { hexlify } from '@ethersproject/bytes'
import {
    AccountMeta,
    Cluster,
    Program,
    ProgramError,
    ProgramRepositoryInterface,
    PublicKey,
    RpcInterface,
    Signer,
    WrappedInstruction,
    createNullRpc,
    defaultPublicKey,
    none,
    some,
} from '@metaplex-foundation/umi'
import { createDefaultProgramRepository } from '@metaplex-foundation/umi-program-repository'
import { fromWeb3JsPublicKey, toWeb3JsInstruction, toWeb3JsPublicKey } from '@metaplex-foundation/umi-web3js-adapters'
import { TOKEN_PROGRAM_ID } from '@solana/spl-token'
import { ComputeBudgetProgram } from '@solana/web3.js'

import {
    EndpointPDA,
    EndpointProgram,
    EventPDA,
    MessageLibInterface,
    SetConfigType,
    SimpleMessageLibProgram,
    SolanaPacketPath,
    UlnPDA,
    UlnProgram,
    simulateWeb3JsTransaction,
} from '@layerzerolabs/lz-solana-sdk-v2/umi'

import { OFT_DECIMALS } from './consts'
import * as OFTAccounts from './generated/oft302/accounts'
import * as errors from './generated/oft302/errors'
import * as instructions from './generated/oft302/instructions'
import * as types from './generated/oft302/types'
import { OftPDA } from './pda'
import {
    SetOFTConfigParams,
    SetPeerAddressParam,
    SetPeerEnforcedOptionsParam,
    SetPeerFeeBpsParam,
    SetPeerRateLimitParam,
} from './types'

export * as accounts from './generated/oft302/accounts'
export * as instructions from './generated/oft302/instructions'
export * as programs from './generated/oft302/programs'
export * as shared from './generated/oft302/shared'
export * as types from './generated/oft302/types'
export * as errors from './generated/oft302/errors'

const ENDPOINT_PROGRAM_ID: PublicKey = EndpointProgram.ENDPOINT_PROGRAM_ID
const ULN_PROGRAM_ID: PublicKey = UlnProgram.ULN_PROGRAM_ID

export function createOFTProgramRepo(oftProgram: PublicKey, rpc?: RpcInterface): ProgramRepositoryInterface {
    if (rpc === undefined) {
        rpc = createNullRpc()
        rpc.getCluster = (): Cluster => 'custom'
    }
    return createDefaultProgramRepository({ rpc: rpc }, [
        {
            name: 'oft',
            publicKey: oftProgram,
            getErrorFromCode(code: number, cause?: Error): ProgramError | null {
                return errors.getOftErrorFromCode(code, this, cause)
            },
            getErrorFromName(name: string, cause?: Error): ProgramError | null {
                return errors.getOftErrorFromName(name, this, cause)
            },
            isOnCluster(): boolean {
                return true
            },
        } satisfies Program,
    ])
}

export function initOft(
    accounts: { payer: Signer; admin: PublicKey; mint: PublicKey; escrow: Signer },
    oftType: types.OFTType,
    sharedDecimals = OFT_DECIMALS,
    programs: { oft: PublicKey; endpoint?: PublicKey; token?: PublicKey }
): WrappedInstruction {
    const programsRepo = typeof programs.oft === 'string' ? createOFTProgramRepo(programs.oft) : programs.oft
    const deriver = new OftPDA(programsRepo.getPublicKey('oft'))
    const endpoint = new EndpointProgram.Endpoint(programs.endpoint ?? ENDPOINT_PROGRAM_ID)
    const { payer, admin, mint, escrow } = accounts
    const [oftStore] = deriver.oftStore(escrow.publicKey)
    const [lzReceiveTypes] = deriver.lzReceiveTypesAccounts(oftStore)

    const txBuilder = instructions.initOft(
        {
            payer: payer,
            programs: programsRepo,
        },
        {
            // accounts
            oftStore: oftStore,
            lzReceiveTypesAccounts: lzReceiveTypes,
            tokenMint: mint,
            tokenEscrow: escrow,
            tokenProgram: programs.token ?? fromWeb3JsPublicKey(TOKEN_PROGRAM_ID),
            // params
            oftType: oftType,
            admin: admin,
            sharedDecimals: sharedDecimals,
            endpointProgram: endpoint.programId,
        }
    )
    const retval = txBuilder.addRemainingAccounts(
        endpoint.getRegisterOappIxAccountMetaForCPI(payer.publicKey, oftStore).map((acc) => {
            return {
                pubkey: acc.pubkey,
                isSigner: acc.isSigner,
                isWritable: acc.isWritable,
            }
        })
    ).items[0]
    retval.signers = [payer, escrow]
    return retval
}

export function setOFTConfig(
    accounts: {
        oftStore: PublicKey
        admin: Signer
    },
    params: SetOFTConfigParams,
    programs: {
        oft: PublicKey
        endpoint?: PublicKey
    }
): WrappedInstruction {
    let actualParams: types.SetOFTConfigParamsArgs
    const { oftStore, admin } = accounts
    const remainingAccounts: AccountMeta[] = []
    if (params.__kind === 'Admin') {
        if (params.admin === undefined) {
            throw new Error('Admin is required')
        }
        actualParams = {
            __kind: 'Admin',
            fields: [params.admin],
        }
    } else if (params.__kind === 'Delegate') {
        if (params.delegate === undefined) {
            throw new Error('Delegate is required')
        }
        actualParams = {
            __kind: 'Delegate',
            fields: [params.delegate],
        }
        const endpointProgram = programs.endpoint ?? ENDPOINT_PROGRAM_ID
        const endpointSDK = new EndpointProgram.Endpoint(endpointProgram)
        const keys = endpointSDK.getSetDelegateIxAccountMetaForCPI(oftStore)
        for (const acc of keys) {
            acc.isSigner = false
        }
        remainingAccounts.push(...keys)
    } else if (params.__kind === 'DefaultFee') {
        if (params.defaultFee === undefined) {
            throw new Error('DefaultFee is required')
        }
        actualParams = {
            __kind: 'DefaultFee',
            fields: [params.defaultFee],
        }
    } else if (params.__kind === 'Paused') {
        if (params.paused === undefined) {
            throw new Error('Paused is required')
        }
        actualParams = {
            __kind: 'Paused',
            fields: [params.paused],
        }
    } else if (params.__kind === 'Pauser') {
        actualParams = {
            __kind: 'Pauser',
            fields: [params.pauser ? some(params.pauser) : none()],
        }
    } else {
        actualParams = {
            __kind: 'Unpauser',
            fields: [params.unpauser ? some(params.unpauser) : none()],
        }
    }

    const txBuilder = instructions.setOftConfig(
        { programs: createOFTProgramRepo(programs.oft) },
        {
            admin: admin,
            oftStore: oftStore,
            params: actualParams,
        }
    )
    return txBuilder.addRemainingAccounts(
        remainingAccounts.map((acc) => {
            return {
                pubkey: acc.pubkey,
                isSigner: acc.isSigner,
                isWritable: acc.isWritable,
            }
        })
    ).items[0]
}

/**
 * Sets the peer configuration.
 *
 * @param {Object} accounts - The accounts object.
 * @param {Signer} accounts.admin - The admin signer.
 * @param {PublicKey} accounts.oftStore - The OFT store public key.
 * @param {Object} param - The parameter object.
 * @param {number} param.remote - The remote endpoint ID.
 * @param {string} [param.__kind] - The kind of parameter.
 * @param {Uint8Array} [param.peer] - The peer address (for PeerAddress kind).
 * @param {number} [param.feeBps] - The fee basis points (for FeeBps kind).
 * @param {Uint8Array} [param.send] - The send option (for EnforcedOptions kind).
 * @param {Uint8Array} [param.sendAndCall] - The send and call option (for EnforcedOptions kind).
 * @param {Object} [param.rateLimit] - The rate limit option (for OutboundRateLimit or InboundRateLimit kind).
 * @param {bigint} [param.rateLimit.refillPerSecond] - The rate limit refill per second.
 * @param {bigint} [param.rateLimit.capacity] - The rate limit capacity.
 * @param {PublicKey} oftProgramId - The OFT program ID.
 * @throws {Error} If the remote ID is invalid or if the peer address is not 32 bytes.
 * @returns {WrappedInstruction} - The wrapped instruction.
 */
export function setPeerConfig(
    accounts: {
        admin: Signer
        oftStore: PublicKey
    },
    param: (SetPeerAddressParam | SetPeerFeeBpsParam | SetPeerEnforcedOptionsParam | SetPeerRateLimitParam) & {
        remote: number
    },
    oftProgramId: PublicKey | ProgramRepositoryInterface
): WrappedInstruction {
    const programsRepo = typeof oftProgramId === 'string' ? createOFTProgramRepo(oftProgramId) : oftProgramId
    const { remote: remoteId } = param
    if (remoteId % 30000 == 0) {
        throw new Error('Invalid remote ID')
    }
    const { admin, oftStore } = accounts
    const [peerPda] = new OftPDA(programsRepo.getPublicKey('oft')).peer(oftStore, remoteId)
    let config: types.PeerConfigParamArgs
    if (param.__kind === 'PeerAddress') {
        if (param.peer.length !== 32) {
            throw new Error('Peer must be 32 bytes (left-padded with zeroes)')
        }
        config = types.peerConfigParam('PeerAddress', [param.peer])
    } else if (param.__kind === 'FeeBps') {
        config = { __kind: 'FeeBps', fields: [some(param.feeBps)] }
    } else if (param.__kind === 'EnforcedOptions') {
        config = {
            __kind: 'EnforcedOptions',
            send: param.send,
            sendAndCall: param.sendAndCall,
        }
        // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
    } else if (param.__kind === 'OutboundRateLimit' || param.__kind === 'InboundRateLimit') {
        config = {
            __kind: param.__kind,
            fields: [
                param.rateLimit
                    ? some({
                        refillPerSecond: some(param.rateLimit.refillPerSecond),
                        capacity: some(param.rateLimit.capacity),
                    })
                    : null,
            ],
        }
    } else {
        throw new Error('Invalid peer config')
    }

    return instructions.setPeerConfig(
        { programs: programsRepo },
        {
            admin: admin,
            peer: peerPda,
            oftStore: oftStore,
            // params
            remoteEid: remoteId,
            config: config,
        }
    ).items[0]
}

export function initSendLibrary(
    accounts: {
        admin: Signer
        oftStore: PublicKey
    },
    remoteEid: number,
    endpointProgram: PublicKey = ENDPOINT_PROGRAM_ID
): WrappedInstruction {
    const { admin, oftStore } = accounts
    const endpoint = new EndpointProgram.Endpoint(endpointProgram)
    return endpoint.initOAppSendLibrary(admin, { sender: oftStore, remote: remoteEid })
}

export function initReceiveLibrary(
    accounts: {
        admin: Signer
        oftStore: PublicKey
    },
    remoteEid: number,
    endpointProgram: PublicKey = ENDPOINT_PROGRAM_ID
): WrappedInstruction {
    const { admin, oftStore } = accounts
    const endpoint = new EndpointProgram.Endpoint(endpointProgram)
    return endpoint.initOAppReceiveLibrary(admin, { receiver: oftStore, remote: remoteEid })
}

export function setSendLibrary(
    accounts: {
        admin: Signer
        oftStore: PublicKey
    },
    params: {
        sendLibraryProgram: PublicKey
        remoteEid: number
    },
    endpointProgram: PublicKey = ENDPOINT_PROGRAM_ID
): WrappedInstruction {
    const { sendLibraryProgram, remoteEid } = params
    const { admin, oftStore } = accounts
    const endpoint = new EndpointProgram.Endpoint(endpointProgram)
    return endpoint.setOAppSendLibrary(admin, {
        sender: oftStore,
        remote: remoteEid,
        msgLibProgram: sendLibraryProgram,
    })
}

export function setReceiveLibrary(
    accounts: {
        admin: Signer
        oftStore: PublicKey
    },
    params: {
        receiveLibraryProgram: PublicKey
        remoteEid: number
        gracePeriod?: bigint
    },
    endpointProgram: PublicKey = ENDPOINT_PROGRAM_ID
): WrappedInstruction {
    const { receiveLibraryProgram, remoteEid, gracePeriod } = params
    const { admin, oftStore } = accounts
    const endpoint = new EndpointProgram.Endpoint(endpointProgram)
    return endpoint.setOAppReceiveLibrary(admin, {
        receiver: oftStore,
        remote: remoteEid,
        msgLibProgram: receiveLibraryProgram,
        gracePeriod: gracePeriod,
    })
}

export function initConfig(
    accounts: {
        admin: Signer
        oftStore: PublicKey
        payer: Signer
    },
    remoteEid: number,
    programs?: {
        msgLib?: PublicKey
        endpoint?: PublicKey
    }
): WrappedInstruction {
    const { admin, oftStore, payer } = accounts

    let msgLibProgram: PublicKey, endpointProgram: PublicKey
    if (programs === undefined) {
        msgLibProgram = ULN_PROGRAM_ID
        endpointProgram = ENDPOINT_PROGRAM_ID
    } else {
        msgLibProgram = programs.msgLib ?? ULN_PROGRAM_ID
        endpointProgram = programs.endpoint ?? ENDPOINT_PROGRAM_ID
    }

    const endpoint = new EndpointProgram.Endpoint(endpointProgram)
    let msgLib: MessageLibInterface
    if (msgLibProgram === SimpleMessageLibProgram.SIMPLE_MESSAGELIB_PROGRAM_ID) {
        msgLib = new SimpleMessageLibProgram.SimpleMessageLib(SimpleMessageLibProgram.SIMPLE_MESSAGELIB_PROGRAM_ID)
    } else {
        msgLib = new UlnProgram.Uln(msgLibProgram)
    }
    return endpoint.initOAppConfig(
        {
            delegate: admin,
            payer: payer.publicKey,
        },
        {
            msgLibSDK: msgLib,
            oapp: oftStore,
            remote: remoteEid,
        }
    )
}

export async function setConfig(
    rpc: RpcInterface,
    accounts: {
        signer: Signer
        oftStore: PublicKey
    },
    params: {
        remoteEid: number
        configType: SetConfigType
        config: UlnProgram.types.ExecutorConfig | UlnProgram.types.UlnConfig
    },
    programs?: {
        msgLib?: PublicKey
        endpoint?: PublicKey
    }
): Promise<WrappedInstruction> {
    const { signer, oftStore } = accounts
    const { remoteEid, configType, config } = params
    let msgLibProgram: PublicKey, endpointProgram: PublicKey
    if (programs === undefined) {
        msgLibProgram = ULN_PROGRAM_ID
        endpointProgram = ENDPOINT_PROGRAM_ID
    } else {
        msgLibProgram = programs.msgLib ?? ULN_PROGRAM_ID
        endpointProgram = programs.endpoint ?? ENDPOINT_PROGRAM_ID
    }
    const endpoint = new EndpointProgram.Endpoint(endpointProgram)
    return endpoint.setOAppConfig(rpc, signer, {
        oapp: oftStore,
        eid: remoteEid,
        config: {
            configType,
            value: config,
        },
        msgLibProgram: msgLibProgram,
    })
}

export function withdrawFee(
    accounts: {
        admin: Signer
        mint: PublicKey
        escrow: PublicKey
        dest: PublicKey
    },
    amount: bigint,
    programs: {
        oft: PublicKey | ProgramRepositoryInterface
        token?: PublicKey
    }
): WrappedInstruction {
    const { admin, mint, escrow, dest } = accounts
    const programsRepo = typeof programs.oft === 'string' ? createOFTProgramRepo(programs.oft) : programs.oft
    const [oftStore] = new OftPDA(programsRepo.getPublicKey('oft')).oftStore(escrow)
    return instructions.withdrawFee(
        { programs: programsRepo },
        {
            admin: admin,
            tokenEscrow: escrow,
            tokenDest: dest,
            tokenProgram: programs.token ?? fromWeb3JsPublicKey(TOKEN_PROGRAM_ID),
            oftStore: oftStore,
            tokenMint: mint,
            // params
            feeLd: amount,
        }
    ).items[0]
}

export async function send(
    rpc: RpcInterface,
    accounts: {
        payer: Signer
        tokenMint: PublicKey
        tokenEscrow: PublicKey
        tokenSource: PublicKey
        peerAddr?: Uint8Array
    },
    sendParams: {
        dstEid: number
        to: Uint8Array
        amountLd: bigint
        minAmountLd: bigint
        options?: Uint8Array
        composeMsg?: Uint8Array
        nativeFee: bigint
        lzTokenFee?: bigint
    },
    programs: {
        oft: PublicKey
        endpoint?: PublicKey // default is ENDPOINT_PROGRAM(76y77prsiCMvXMjuoZ5VRrhG5qYBrUMYTE5WgHqgjEn6)
        token?: PublicKey // default is TOKEN_PROGRAM_ID(TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA)
    },
    remainingAccounts?: AccountMeta[]
): Promise<WrappedInstruction> {
    const { payer, tokenMint, tokenEscrow, tokenSource } = accounts
    const { dstEid, to, amountLd, minAmountLd, options, composeMsg, nativeFee, lzTokenFee } = sendParams
    const deriver = new OftPDA(programs.oft)
    const [oftStore] = deriver.oftStore(tokenEscrow)
    const [peer] = deriver.peer(oftStore, dstEid)

    if (remainingAccounts === undefined || remainingAccounts.length === 0) {
        const peerAddr: Uint8Array =
            accounts.peerAddr ??
            (await OFTAccounts.fetchPeerConfig({ rpc }, peer).then((peerInfo) => peerInfo.peerAddress))

        const endpoint = new EndpointProgram.Endpoint(programs.endpoint ?? ENDPOINT_PROGRAM_ID)
        const msgLibProgram = await getSendLibraryProgram(rpc, endpoint, payer.publicKey, oftStore, dstEid)
        const packetPath: SolanaPacketPath = {
            dstEid,
            sender: oftStore,
            receiver: peerAddr,
        }
        remainingAccounts = await endpoint.getSendIXAccountMetaForCPI(rpc, payer.publicKey, {
            path: packetPath,
            msgLibProgram: msgLibProgram,
        })
    }

    const [eventAuthorityPDA] = new EventPDA(programs.oft).eventAuthority()
    const tokenProgram: PublicKey = programs.token ?? fromWeb3JsPublicKey(TOKEN_PROGRAM_ID)
    const txBuilder = instructions.send(
        { programs: createOFTProgramRepo(programs.oft) },
        {
            signer: payer,
            peer: peer,
            oftStore: oftStore,
            tokenSource: tokenSource,
            tokenEscrow: tokenEscrow,
            tokenMint: tokenMint,
            tokenProgram: tokenProgram,
            eventAuthority: eventAuthorityPDA,
            program: programs.oft,
            // params
            dstEid: dstEid,
            to: to,
            amountLd,
            minAmountLd,
            options: options ?? new Uint8Array(),
            composeMsg: composeMsg ?? null,
            nativeFee,
            lzTokenFee: lzTokenFee ?? 0,
        }
    )

    // Get remaining accounts from msgLib(simple_msgLib or uln)
    return txBuilder.addRemainingAccounts(
        remainingAccounts.map((acc) => {
            return {
                pubkey: acc.pubkey,
                isSigner: acc.isSigner,
                isWritable: acc.isWritable,
            }
        })
    ).items[0]
}

export async function quote(
    rpc: RpcInterface,
    accounts: {
        payer: PublicKey
        tokenMint: PublicKey
        tokenEscrow: PublicKey
        peerAddr?: Uint8Array
    },
    quoteParams: {
        dstEid: number
        to: Uint8Array
        amountLd: bigint
        minAmountLd: bigint
        options?: Uint8Array
        payInLzToken?: boolean
        composeMsg?: Uint8Array
    },
    programs: {
        oft: PublicKey
        endpoint?: PublicKey
    },
    remainingAccounts?: AccountMeta[],
    addressLookupTables?: PublicKey | PublicKey[]
): Promise<{ nativeFee: bigint; lzTokenFee: bigint }> {
    const { dstEid, to, amountLd, minAmountLd, options, payInLzToken, composeMsg } = quoteParams
    const { payer, tokenMint, tokenEscrow } = accounts

    const deriver = new OftPDA(programs.oft)
    const [oftStore] = deriver.oftStore(tokenEscrow)
    const [peer] = deriver.peer(oftStore, dstEid)

    if (remainingAccounts === undefined || remainingAccounts.length === 0) {
        const peerAddr: Uint8Array =
            accounts.peerAddr ??
            (await OFTAccounts.fetchPeerConfig({ rpc }, peer).then((peerInfo) => peerInfo.peerAddress))

        const endpoint = new EndpointProgram.Endpoint(programs.endpoint ?? ENDPOINT_PROGRAM_ID)
        const messageLib = await getSendLibraryProgram(rpc, endpoint, payer, oftStore, dstEid)
        remainingAccounts = await endpoint.getQuoteIXAccountMetaForCPI(rpc, payer, {
            path: { sender: oftStore, dstEid: dstEid, receiver: peerAddr },
            msgLibProgram: messageLib,
        })
    }

    let txBuilder = instructions.quoteSend(
        { programs: createOFTProgramRepo(programs.oft) },
        {
            oftStore: oftStore,
            peer: peer,
            tokenMint: tokenMint,
            // params
            dstEid: dstEid,
            to: to,
            amountLd: amountLd,
            minAmountLd: minAmountLd,
            options: options ?? new Uint8Array(),
            payInLzToken: payInLzToken ?? false,
            composeMsg: composeMsg ?? null,
        }
    )

    txBuilder = txBuilder.addRemainingAccounts(
        // Get remaining accounts from msgLib(simple_msgLib or uln)
        remainingAccounts.map((acc) => {
            return {
                pubkey: acc.pubkey,
                isSigner: acc.isSigner,
                isWritable: acc.isWritable,
            }
        })
    )
    const web3Ix = toWeb3JsInstruction(txBuilder.getInstructions()[0])
    const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({
        units: 1000000,
    })
    return simulateWeb3JsTransaction(
        rpc,
        [modifyComputeUnits, web3Ix],
        web3Ix.programId,
        toWeb3JsPublicKey(payer),
        EndpointProgram.types.getMessagingFeeSerializer(),
        'confirmed',
        undefined,
        addressLookupTables !== undefined
            ? Array.isArray(addressLookupTables)
                ? addressLookupTables.map(toWeb3JsPublicKey)
                : toWeb3JsPublicKey(addressLookupTables)
            : undefined
    )
}

export async function quoteOft(
    rpc: RpcInterface,
    accounts: {
        payer: PublicKey
        tokenMint: PublicKey
        tokenEscrow: PublicKey
    },
    quoteParams: {
        dstEid: number
        to: Uint8Array
        amountLd: bigint
        minAmountLd: bigint
        options?: Uint8Array
        payInLzToken?: boolean
        composeMsg?: Uint8Array
    },
    oftProgram: PublicKey
): Promise<types.QuoteOFTResult> {
    const { payer, tokenMint, tokenEscrow } = accounts
    const { dstEid, to, amountLd, minAmountLd, options, payInLzToken, composeMsg } = quoteParams
    const deriver = new OftPDA(oftProgram)
    const [oftStore] = deriver.oftStore(tokenEscrow)
    const [peer] = deriver.peer(oftStore, dstEid)
    const ix = instructions
        .quoteOft(
            { programs: createOFTProgramRepo(oftProgram) },
            {
                oftStore: oftStore,
                peer: peer,
                tokenMint: tokenMint,
                // params
                dstEid: dstEid,
                to: to,
                amountLd: amountLd,
                minAmountLd: minAmountLd,
                options: options ?? new Uint8Array(),
                payInLzToken: payInLzToken ?? false,
                composeMsg: composeMsg ?? null,
            }
        )
        .getInstructions()[0]

    const web3Ix = toWeb3JsInstruction(ix)
    return simulateWeb3JsTransaction(
        rpc.getEndpoint(),
        [web3Ix],
        web3Ix.programId,
        toWeb3JsPublicKey(payer),
        types.getQuoteOFTResultSerializer(),
        'confirmed'
    )
}

export function initOAppNonce(
    accounts: {
        admin: Signer
        oftStore: PublicKey
    },
    remoteEid: number,
    remoteOappAddr: Uint8Array, // must be 32 bytes
    endpointProgram: PublicKey = ENDPOINT_PROGRAM_ID
): WrappedInstruction {
    const { admin, oftStore } = accounts
    const endpoint = new EndpointProgram.Endpoint(endpointProgram)

    return endpoint.initOAppNonce(admin, {
        localOApp: oftStore,
        remote: remoteEid,
        remoteOApp: remoteOappAddr,
    })
}

export async function getEndpointConfig(
    rpc: RpcInterface,
    oftStore: PublicKey,
    endpointId: number,
    programs?: {
        msgLib?: PublicKey
        endpoint?: PublicKey
    }
): Promise<{
    sendLibraryConfig: EndpointProgram.accounts.SendLibraryConfig & { ulnSendConfig?: UlnProgram.accounts.SendConfig }
    receiveLibraryConfig: EndpointProgram.accounts.ReceiveLibraryConfig & {
        ulnReceiveConfig?: UlnProgram.accounts.ReceiveConfig
    }
}> {
    let msgLibProgram: PublicKey, endpointProgram: PublicKey
    if (programs === undefined) {
        msgLibProgram = ULN_PROGRAM_ID
        endpointProgram = ENDPOINT_PROGRAM_ID
    } else {
        msgLibProgram = programs.msgLib ?? ULN_PROGRAM_ID
        endpointProgram = programs.endpoint ?? ENDPOINT_PROGRAM_ID
    }
    const endpointDeriver = new EndpointPDA(endpointProgram)
    const ulnDeriver = new UlnPDA(msgLibProgram)

    const [sendLib] = endpointDeriver.sendLibraryConfig(oftStore, endpointId)
    const [defaultSendLib] = endpointDeriver.defaultSendLibraryConfig(endpointId)
    const [receiveLib] = endpointDeriver.receiveLibraryConfig(oftStore, endpointId)
    const [defaultReceiveLib] = endpointDeriver.defaultReceiveLibraryConfig(endpointId)

    const [msgLib] = ulnDeriver.messageLib()
    let sendLibraryConfig: EndpointProgram.accounts.SendLibraryConfig & {
        ulnSendConfig?: UlnProgram.accounts.SendConfig
    } = await EndpointProgram.accounts.fetchSendLibraryConfig({ rpc }, sendLib)

    let receiveLibraryConfig: EndpointProgram.accounts.ReceiveLibraryConfig & {
        ulnReceiveConfig?: UlnProgram.accounts.ReceiveConfig
    } = await EndpointProgram.accounts.fetchReceiveLibraryConfig({ rpc }, receiveLib)

    const defaultSendLibraryConfig: EndpointProgram.accounts.SendLibraryConfig & {
        ulnSendConfig?: UlnProgram.accounts.SendConfig
    } = await EndpointProgram.accounts.fetchSendLibraryConfig({ rpc }, defaultSendLib)

    const defaultReceiveLibraryConfig: EndpointProgram.accounts.ReceiveLibraryConfig & {
        ulnReceiveConfig?: UlnProgram.accounts.ReceiveConfig
    } = await EndpointProgram.accounts.fetchReceiveLibraryConfig({ rpc }, defaultReceiveLib)

    const nil64 = 18446744073709551615n // max u64
    const nil8 = 255

    if (sendLibraryConfig.messageLib === defaultPublicKey()) {
        sendLibraryConfig = defaultSendLibraryConfig
    }
    if (receiveLibraryConfig.messageLib === defaultPublicKey()) {
        receiveLibraryConfig = defaultReceiveLibraryConfig
    }
    // get the uln config if necessary
    if (sendLibraryConfig.messageLib === msgLib) {
        const [ulnDefaultSendConfigPDA] = ulnDeriver.defaultSendConfig(endpointId)
        const [ulnSendConfigPDA] = ulnDeriver.sendConfig(endpointId, oftStore)
        const ulnSendConfig = await UlnProgram.accounts.fetchSendConfig({ rpc }, ulnSendConfigPDA)
        const ulnDefaultSendConfig = await UlnProgram.accounts.fetchSendConfig({ rpc }, ulnDefaultSendConfigPDA)
        // get the uln config for the send library
        if (nil64 === ulnSendConfig.uln.confirmations) {
            ulnSendConfig.uln.confirmations = 0n
        } else if (ulnSendConfig.uln.confirmations == 0n) {
            ulnSendConfig.uln.confirmations = ulnDefaultSendConfig.uln.confirmations
        }
        if (ulnSendConfig.uln.requiredDvnCount == nil8) {
            ulnSendConfig.uln.requiredDvnCount = 0
        } else if (ulnSendConfig.uln.requiredDvnCount == 0) {
            ulnSendConfig.uln.requiredDvnCount = ulnDefaultSendConfig.uln.requiredDvnCount
            ulnSendConfig.uln.requiredDvns = ulnDefaultSendConfig.uln.requiredDvns
        }
        if (ulnSendConfig.uln.optionalDvnCount == nil8) {
            ulnSendConfig.uln.optionalDvnCount = 0
            ulnSendConfig.uln.optionalDvnThreshold = 0
        } else if (ulnSendConfig.uln.optionalDvnCount == 0) {
            ulnSendConfig.uln.optionalDvnCount = ulnDefaultSendConfig.uln.optionalDvnCount
            ulnSendConfig.uln.optionalDvnThreshold = ulnDefaultSendConfig.uln.optionalDvnThreshold
            ulnSendConfig.uln.optionalDvns = ulnDefaultSendConfig.uln.optionalDvns
        }
        sendLibraryConfig.ulnSendConfig = ulnSendConfig
    }
    if (receiveLibraryConfig.messageLib === msgLib) {
        const [ulnDefaultReceiveConfigPDA] = ulnDeriver.defaultReceiveConfig(endpointId)
        const [ulnReceiveConfigPDA] = ulnDeriver.receiveConfig(endpointId, oftStore)
        const ulnReceiveConfig = await UlnProgram.accounts.fetchReceiveConfig({ rpc }, ulnReceiveConfigPDA)
        const ulnDefaultReceiveConfig = await UlnProgram.accounts.fetchReceiveConfig(
            { rpc },
            ulnDefaultReceiveConfigPDA
        )
        if (nil64 === ulnReceiveConfig.uln.confirmations) {
            ulnReceiveConfig.uln.confirmations = 0n
        } else if (ulnReceiveConfig.uln.confirmations == 0n) {
            ulnReceiveConfig.uln.confirmations = ulnDefaultReceiveConfig.uln.confirmations
        }
        if (ulnReceiveConfig.uln.requiredDvnCount == nil8) {
            ulnReceiveConfig.uln.requiredDvnCount = 0
        } else if (ulnReceiveConfig.uln.requiredDvnCount == 0) {
            ulnReceiveConfig.uln.requiredDvnCount = ulnDefaultReceiveConfig.uln.requiredDvnCount
            ulnReceiveConfig.uln.requiredDvns = ulnDefaultReceiveConfig.uln.requiredDvns
        }
        if (ulnReceiveConfig.uln.optionalDvnCount == nil8) {
            ulnReceiveConfig.uln.optionalDvnCount = 0
            ulnReceiveConfig.uln.optionalDvnThreshold = 0
        } else if (ulnReceiveConfig.uln.optionalDvnCount == 0) {
            ulnReceiveConfig.uln.optionalDvnCount = ulnDefaultReceiveConfig.uln.optionalDvnCount
            ulnReceiveConfig.uln.optionalDvnThreshold = ulnDefaultReceiveConfig.uln.optionalDvnThreshold
            ulnReceiveConfig.uln.optionalDvns = ulnDefaultReceiveConfig.uln.optionalDvns
        }
        receiveLibraryConfig.ulnReceiveConfig = ulnReceiveConfig
    }
    return {
        sendLibraryConfig,
        receiveLibraryConfig,
    }
    // send lib address, if blocked then just return that, otherwise return the uln config
    // send lib executor, dvns etc.
    // receive lib executor, dvns
}

// returns the hex string of the peer address
export async function getPeerAddress(
    rpc: RpcInterface,
    oftInstance: PublicKey,
    remoteEid: number,
    oftProgramId: PublicKey
): Promise<string> {
    const [peer] = new OftPDA(oftProgramId).peer(oftInstance, remoteEid)
    const peerInfo = await OFTAccounts.fetchPeerConfig({ rpc }, peer)
    return hexlify(peerInfo.peerAddress)
}

export async function getDelegate(
    rpc: RpcInterface,
    oftInstance: PublicKey,
    endpointProgram: PublicKey = ENDPOINT_PROGRAM_ID
): Promise<PublicKey> {
    const [oAppRegistry] = new EndpointPDA(endpointProgram).oappRegistry(oftInstance)
    const oAppRegistryInfo = await EndpointProgram.accounts.fetchOAppRegistry({ rpc }, oAppRegistry)
    return oAppRegistryInfo.delegate
}

export async function getEnforcedOptions(
    rpc: RpcInterface,
    oftInstance: PublicKey,
    remoteEid: number,
    oftProgramId: PublicKey
): Promise<types.EnforcedOptions> {
    const [peer] = new OftPDA(oftProgramId).peer(oftInstance, remoteEid)
    const peerInfo = await OFTAccounts.fetchPeerConfig({ rpc }, peer)
    return peerInfo.enforcedOptions
}

async function getSendLibraryProgram(
    rpc: RpcInterface,
    endpoint: EndpointProgram.Endpoint,
    payer: PublicKey,
    oftStore: PublicKey,
    remoteEid: number
): Promise<SimpleMessageLibProgram.SimpleMessageLib | UlnProgram.Uln> {
    const sendLibInfo = await endpoint.getSendLibrary(rpc, oftStore, remoteEid)
    if (!sendLibInfo.programId) {
        throw new Error('Send library not initialized or blocked message library')
    }
    const { programId: msgLibProgram } = sendLibInfo
    const msgLibVersion = await endpoint.getMessageLibVersion(rpc, payer, msgLibProgram)
    if (msgLibVersion.major.toString() === '0' && msgLibVersion.minor == 0 && msgLibVersion.endpointVersion == 2) {
        return new SimpleMessageLibProgram.SimpleMessageLib(msgLibProgram)
    } else if (
        msgLibVersion.major.toString() === '3' &&
        msgLibVersion.minor == 0 &&
        msgLibVersion.endpointVersion == 2
    ) {
        return new UlnProgram.Uln(msgLibProgram)
    }
    throw new Error(`Unsupported message library version: ${JSON.stringify(msgLibVersion, null, 2)}`)
}
