import { getDeployWithMaxDataLenInstruction, getInitializeBufferInstruction, getUpgradeInstruction, getWriteInstruction, } from '@solana-program/loader-v3';
import { getCreateAccountInstruction } from '@solana-program/system';
import { generateKeyPairSigner, getAddressCodec, getAddressDecoder, getProgramDerivedAddress, } from '@solana/kit';
import { assert, rootLogger } from '@hyperlane-xyz/utils';
import { LOADER_V3_PROGRAM_ADDRESS } from '../constants.js';
import { DEFAULT_WRITE_CHUNK_SIZE } from '../tx.js';
const logger = rootLogger.child({ module: 'ProgramDeployer' });
const ADDRESS_CODEC = getAddressCodec();
const BUFFER_METADATA_SIZE = 37;
const PROGRAM_ACCOUNT_SIZE = 36;
export const DeployStageKind = {
    Init: 'init',
    Write: 'write',
    Finalize: 'finalize',
};
export async function createDeployProgramPlan(args) {
    const authority = args.authority ?? args.payer;
    const programSigner = args.programSigner ?? (await generateKeyPairSigner());
    const bufferSigner = args.bufferSigner ?? (await generateKeyPairSigner());
    const chunkSize = args.writeChunkSize ?? DEFAULT_WRITE_CHUNK_SIZE;
    assert(chunkSize > 0, 'writeChunkSize must be positive');
    const maxDataLen = args.maxDataLen ?? BigInt(args.programBytes.length * 2);
    const bufferSize = BUFFER_METADATA_SIZE + args.programBytes.length;
    const bufferRent = await args.getMinimumBalanceForRentExemption(bufferSize);
    const programRent = await args.getMinimumBalanceForRentExemption(PROGRAM_ACCOUNT_SIZE);
    const programDataAddress = await deriveProgramDataAddress(programSigner.address);
    const createBufferInstruction = getCreateAccountInstruction({
        payer: args.payer,
        newAccount: bufferSigner,
        lamports: bufferRent,
        space: BigInt(bufferSize),
        programAddress: LOADER_V3_PROGRAM_ADDRESS,
    });
    const initializeBufferInstruction = getInitializeBufferInstruction({
        sourceAccount: bufferSigner.address,
        bufferAuthority: authority.address,
    });
    const stages = [
        {
            label: 'create-and-init-buffer',
            kind: DeployStageKind.Init,
            instructions: [createBufferInstruction, initializeBufferInstruction],
            additionalSigners: [bufferSigner],
        },
    ];
    for (let offset = 0; offset < args.programBytes.length; offset += chunkSize) {
        const bytes = args.programBytes.slice(offset, offset + chunkSize);
        stages.push({
            label: `write-${offset}`,
            kind: DeployStageKind.Write,
            instructions: [
                getWriteInstruction({
                    bufferAccount: bufferSigner.address,
                    bufferAuthority: authority,
                    offset,
                    bytes,
                }),
            ],
        });
    }
    const createProgramInstruction = getCreateAccountInstruction({
        payer: args.payer,
        newAccount: programSigner,
        lamports: programRent,
        space: BigInt(PROGRAM_ACCOUNT_SIZE),
        programAddress: LOADER_V3_PROGRAM_ADDRESS,
    });
    const deployInstruction = getDeployWithMaxDataLenInstruction({
        payerAccount: args.payer,
        programDataAccount: programDataAddress,
        programAccount: programSigner.address,
        bufferAccount: bufferSigner.address,
        authority,
        maxDataLen,
    });
    stages.push({
        label: 'create-and-deploy-program',
        kind: DeployStageKind.Finalize,
        instructions: [createProgramInstruction, deployInstruction],
        additionalSigners: [programSigner],
    });
    return {
        programAddress: programSigner.address,
        programDataAddress,
        stages,
    };
}
export async function createUpgradeProgramPlan(args) {
    const bufferSigner = args.bufferSigner ?? (await generateKeyPairSigner());
    const chunkSize = args.writeChunkSize ?? DEFAULT_WRITE_CHUNK_SIZE;
    assert(chunkSize > 0, 'writeChunkSize must be positive');
    const bufferSize = BUFFER_METADATA_SIZE + args.newProgramBytes.length;
    const bufferRent = await args.getMinimumBalanceForRentExemption(bufferSize);
    const programDataAddress = await deriveProgramDataAddress(args.programAddress);
    const stages = [
        {
            label: 'create-and-init-buffer',
            kind: DeployStageKind.Init,
            instructions: [
                getCreateAccountInstruction({
                    payer: args.payer,
                    newAccount: bufferSigner,
                    lamports: bufferRent,
                    space: BigInt(bufferSize),
                    programAddress: LOADER_V3_PROGRAM_ADDRESS,
                }),
                getInitializeBufferInstruction({
                    sourceAccount: bufferSigner.address,
                    bufferAuthority: args.authority.address,
                }),
            ],
            additionalSigners: [bufferSigner],
        },
    ];
    for (let offset = 0; offset < args.newProgramBytes.length; offset += chunkSize) {
        const bytes = args.newProgramBytes.slice(offset, offset + chunkSize);
        stages.push({
            label: `write-${offset}`,
            kind: DeployStageKind.Write,
            instructions: [
                getWriteInstruction({
                    bufferAccount: bufferSigner.address,
                    bufferAuthority: args.authority,
                    offset,
                    bytes,
                }),
            ],
        });
    }
    stages.push({
        label: 'upgrade-program',
        kind: DeployStageKind.Finalize,
        instructions: [
            getUpgradeInstruction({
                programDataAccount: programDataAddress,
                programAccount: args.programAddress,
                bufferAccount: bufferSigner.address,
                spillAccount: args.payer.address,
                authority: args.authority,
            }),
        ],
    });
    return {
        programAddress: args.programAddress,
        programDataAddress,
        stages,
    };
}
export async function executeDeployPlan(args) {
    const receipts = [];
    // Partition stages by kind
    const initStages = [];
    const writeStages = [];
    const finalStages = [];
    for (const stage of args.plan.stages) {
        switch (stage.kind) {
            case DeployStageKind.Init:
                initStages.push(stage);
                break;
            case DeployStageKind.Write:
                writeStages.push(stage);
                break;
            case DeployStageKind.Finalize:
                finalStages.push(stage);
                break;
            default: {
                const _exhaustive = stage.kind;
                throw new Error(`Unknown stage kind: ${_exhaustive}`);
            }
        }
    }
    // Init stages must complete before writes begin
    for (const stage of initStages) {
        receipts.push(await args.executeStage(stage));
    }
    // Write stages are independent — send in parallel batches
    const batchSize = args.writeBatchSize ?? 10;
    assert(Number.isInteger(batchSize) && batchSize > 0, 'writeBatchSize must be a positive integer');
    for (let currentStageIdx = 0; currentStageIdx < writeStages.length; currentStageIdx += batchSize) {
        const batch = writeStages.slice(currentStageIdx, currentStageIdx + batchSize);
        const results = await Promise.allSettled(batch.map((stage) => args.executeStage(stage)));
        const fulfilled = [];
        const failedStages = [];
        results.forEach((currWriteStageRes, idx) => {
            if (currWriteStageRes.status === 'fulfilled') {
                fulfilled.push(currWriteStageRes.value);
            }
            else {
                logger.debug(`Write stage ${batch[idx].label} failed, will retry`, {
                    error: currWriteStageRes.reason,
                });
                failedStages.push(batch[idx]);
            }
        });
        receipts.push(...fulfilled);
        // Retry failed writes sequentially
        for (const stage of failedStages) {
            receipts.push(await args.executeStage(stage));
        }
    }
    // Final stages (deploy/upgrade) must wait for all writes
    for (const stage of finalStages) {
        receipts.push(await args.executeStage(stage));
    }
    return receipts;
}
export async function deriveProgramDataAddress(programAddress) {
    const pda = await getProgramDerivedAddress({
        programAddress: LOADER_V3_PROGRAM_ADDRESS,
        seeds: [ADDRESS_CODEC.encode(programAddress)],
    });
    return pda[0];
}
/**
 * Reads the BPF loader upgradeable ProgramData account to return the current
 * upgrade authority, or null if the program is immutable or not found.
 *
 * ProgramData binary layout:
 *   [0-3]  u32 discriminant (= 3)
 *   [4-11] u64 slot
 *   [12]   u8  option tag (0 = None, 1 = Some)
 *   [13-44] [u8; 32] upgrade authority pubkey (only when tag = 1)
 */
export async function getProgramUpgradeAuthority(rpc, programAddress) {
    const programDataAddress = await deriveProgramDataAddress(programAddress);
    const account = await rpc
        .getAccountInfo(programDataAddress, { encoding: 'base64' })
        .send();
    if (!account.value)
        return null;
    const data = Buffer.from(account.value.data[0], 'base64');
    if (data.length < 45)
        return null;
    const hasAuthority = data[12] === 1;
    if (!hasAuthority)
        return null;
    return getAddressDecoder().decode(data.slice(13, 45));
}
//# sourceMappingURL=program-deployer.js.map