import algosdk, { decodeSignedTransaction } from 'algosdk';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { withCommonParams } from '../commonParams.js';
// Tool schemas
export const generalTransactionSchemas = {
    assignGroupId: {
        type: 'object',
        properties: {
            transactions: {
                type: 'array',
                items: { type: 'object' },
                description: 'Array of transaction objects to be assigned a group ID'
            }
        },
        required: ['transactions']
    },
    signTransaction: {
        type: 'object',
        properties: {
            transaction: { type: 'object', description: 'Transaction object to be signed' },
            sk: { type: 'string', description: 'Secret key in hexadecimal format to sign the transaction with' }
        },
        required: ['transaction', 'sk']
    },
    encodeUnsignedTransaction: {
        type: 'object',
        properties: {
            transaction: { type: 'object', description: 'Transaction object (from make_*_txn or assign_group_id) to encode as unsigned transaction bytes' }
        },
        required: ['transaction']
    },
    decodeSignedTransaction: {
        type: 'object',
        properties: {
            bytes: { type: 'string', description: 'Base64-encoded signed transaction bytes (blob from sign_transaction or wallet_sign_transaction)' }
        },
        required: ['bytes']
    }
};
// Tool definitions
export const generalTransactionTools = [
    {
        name: 'assign_group_id',
        description: 'Assign a group ID to a list of transactions',
        inputSchema: withCommonParams(generalTransactionSchemas.assignGroupId),
    },
    {
        name: 'sign_transaction',
        description: 'Sign a transaction with a secret key',
        inputSchema: withCommonParams(generalTransactionSchemas.signTransaction),
    },
    {
        name: 'encode_unsigned_transaction',
        description: 'Encode a transaction object into base64 unsigned transaction bytes (msgpack). Accepts output from make_*_txn or assign_group_id.',
        inputSchema: withCommonParams(generalTransactionSchemas.encodeUnsignedTransaction),
    },
    {
        name: 'decode_signed_transaction',
        description: 'Decode base64 signed transaction bytes back into a transaction object with signature details. Accepts the blob from sign_transaction or wallet_sign_transaction.',
        inputSchema: withCommonParams(generalTransactionSchemas.decodeSignedTransaction),
    }
];
/**
 * Converts a flat transaction object (as returned by make_*_txn tools)
 * into the format expected by algosdk v3's Transaction constructor.
 */
function toV3TransactionParams(flat) {
    // If already in v3 format (has suggestedParams), return as-is
    if (flat.suggestedParams)
        return flat;
    const suggestedParams = {
        fee: flat.fee,
        firstValid: flat.firstValid,
        lastValid: flat.lastValid,
        genesisID: flat.genesisID,
        flatFee: true,
    };
    // genesisHash may be a base64 string, a Uint8Array, or an object {0:72,...} from JSON round-trip
    if (typeof flat.genesisHash === 'string') {
        suggestedParams.genesisHash = algosdk.base64ToBytes(flat.genesisHash);
    }
    else if (flat.genesisHash instanceof Uint8Array) {
        suggestedParams.genesisHash = flat.genesisHash;
    }
    else if (typeof flat.genesisHash === 'object' && flat.genesisHash !== null) {
        // Reconstruct Uint8Array from {0: byte, 1: byte, ...} JSON form
        const keys = Object.keys(flat.genesisHash).map(Number).sort((a, b) => a - b);
        suggestedParams.genesisHash = new Uint8Array(keys.map(k => flat.genesisHash[k]));
    }
    const params = {
        type: flat.type,
        sender: flat.sender,
        suggestedParams,
    };
    if (flat.note)
        params.note = typeof flat.note === 'string' ? algosdk.base64ToBytes(flat.note) : flat.note;
    if (flat.rekeyTo)
        params.rekeyTo = flat.rekeyTo;
    if (flat.lease)
        params.lease = typeof flat.lease === 'string' ? algosdk.base64ToBytes(flat.lease) : flat.lease;
    // group is set after Transaction construction (mutable field, not a constructor param)
    if (flat.group) {
        params._group = typeof flat.group === 'string' ? algosdk.base64ToBytes(flat.group) : flat.group;
    }
    switch (flat.type) {
        case 'pay':
            params.paymentParams = {
                receiver: flat.receiver,
                amount: flat.amount ?? 0,
            };
            if (flat.closeRemainderTo)
                params.paymentParams.closeRemainderTo = flat.closeRemainderTo;
            break;
        case 'axfer':
            params.assetTransferParams = {
                assetIndex: flat.assetIndex,
                receiver: flat.receiver,
                amount: flat.amount ?? 0,
            };
            if (flat.closeRemainderTo)
                params.assetTransferParams.closeRemainderTo = flat.closeRemainderTo;
            break;
        case 'acfg':
            params.assetConfigParams = {
                assetIndex: flat.assetIndex,
                total: flat.total,
                decimals: flat.decimals,
                defaultFrozen: flat.defaultFrozen,
                unitName: flat.unitName,
                assetName: flat.assetName,
                assetURL: flat.assetURL || flat.url,
                assetMetadataHash: flat.assetMetadataHash,
                manager: flat.manager,
                reserve: flat.reserve,
                freeze: flat.freeze,
                clawback: flat.clawback,
            };
            break;
        case 'afrz':
            params.assetFreezeParams = {
                assetIndex: flat.assetIndex,
                freezeTarget: flat.freezeTarget || flat.freezeAccount,
                frozen: flat.frozen ?? flat.assetFrozen,
            };
            break;
        case 'appl':
            params.appCallParams = {
                appIndex: flat.appIndex ?? flat.appId ?? 0,
                onComplete: flat.onComplete ?? flat.appOnComplete ?? 0,
            };
            if (flat.approvalProgram || flat.appApprovalProgram) {
                const prog = flat.approvalProgram || flat.appApprovalProgram;
                params.appCallParams.approvalProgram = typeof prog === 'string' ? algosdk.base64ToBytes(prog) : prog;
            }
            if (flat.clearProgram || flat.appClearProgram) {
                const prog = flat.clearProgram || flat.appClearProgram;
                params.appCallParams.clearProgram = typeof prog === 'string' ? algosdk.base64ToBytes(prog) : prog;
            }
            if (flat.numGlobalInts !== undefined)
                params.appCallParams.numGlobalInts = flat.numGlobalInts;
            if (flat.numGlobalByteSlices !== undefined)
                params.appCallParams.numGlobalByteSlices = flat.numGlobalByteSlices;
            if (flat.numLocalInts !== undefined)
                params.appCallParams.numLocalInts = flat.numLocalInts;
            if (flat.numLocalByteSlices !== undefined)
                params.appCallParams.numLocalByteSlices = flat.numLocalByteSlices;
            if (flat.appArgs) {
                params.appCallParams.appArgs = flat.appArgs.map((a) => typeof a === 'string' ? algosdk.base64ToBytes(a) : a);
            }
            if (flat.accounts || flat.appAccounts)
                params.appCallParams.accounts = flat.accounts || flat.appAccounts;
            if (flat.foreignApps || flat.appForeignApps)
                params.appCallParams.foreignApps = flat.foreignApps || flat.appForeignApps;
            if (flat.foreignAssets || flat.appForeignAssets)
                params.appCallParams.foreignAssets = flat.foreignAssets || flat.appForeignAssets;
            break;
        case 'keyreg':
            params.keyregParams = {
                voteKey: flat.voteKey,
                selectionKey: flat.selectionKey,
                stateProofKey: flat.stateProofKey,
                voteFirst: flat.voteFirst,
                voteLast: flat.voteLast,
                voteKeyDilution: flat.voteKeyDilution,
                nonParticipation: flat.nonParticipation ?? false,
            };
            break;
    }
    return params;
}
/**
 * Creates a Transaction from flat JSON, applying the group field if present.
 */
function createTransactionFromFlat(flat) {
    const v3Params = toV3TransactionParams(flat);
    const groupBytes = v3Params._group;
    delete v3Params._group;
    const txn = new algosdk.Transaction(v3Params);
    if (groupBytes) {
        txn.group = groupBytes;
    }
    return txn;
}
/**
 * Serializes a Transaction object to flat JSON format matching make_*_txn output.
 * Converts Uint8Arrays to base64 strings for safe JSON serialization.
 */
function serializeTransaction(txn) {
    const result = {
        type: txn.type,
        sender: txn.sender.toString(),
        fee: Number(txn.fee),
        firstValid: Number(txn.firstValid),
        lastValid: Number(txn.lastValid),
        genesisID: txn.genesisID,
        genesisHash: txn.genesisHash ? algosdk.bytesToBase64(txn.genesisHash) : undefined,
    };
    // Group ID (present after assignGroupID)
    if (txn.group) {
        result.group = algosdk.bytesToBase64(txn.group);
    }
    // Note
    if (txn.note && txn.note.length > 0) {
        result.note = algosdk.bytesToBase64(txn.note);
    }
    // Lease
    if (txn.lease && txn.lease.length > 0) {
        result.lease = algosdk.bytesToBase64(txn.lease);
    }
    // Rekey
    if (txn.rekeyTo) {
        result.rekeyTo = txn.rekeyTo.toString();
    }
    // Payment fields
    if (txn.type === 'pay' && txn.payment) {
        result.receiver = txn.payment.receiver.toString();
        result.amount = Number(txn.payment.amount);
        if (txn.payment.closeRemainderTo) {
            result.closeRemainderTo = txn.payment.closeRemainderTo.toString();
        }
    }
    // Asset transfer fields
    if (txn.type === 'axfer' && txn.assetTransfer) {
        result.assetIndex = Number(txn.assetTransfer.assetIndex);
        result.receiver = txn.assetTransfer.receiver.toString();
        result.amount = Number(txn.assetTransfer.amount);
        if (txn.assetTransfer.closeRemainderTo) {
            result.closeRemainderTo = txn.assetTransfer.closeRemainderTo.toString();
        }
    }
    // Asset config fields
    if (txn.type === 'acfg' && txn.assetConfig) {
        const p = txn.assetConfig;
        if (p.assetIndex)
            result.assetIndex = Number(p.assetIndex);
        if (p.total !== undefined)
            result.total = Number(p.total);
        if (p.decimals !== undefined)
            result.decimals = p.decimals;
        if (p.defaultFrozen !== undefined)
            result.defaultFrozen = p.defaultFrozen;
        if (p.unitName)
            result.unitName = p.unitName;
        if (p.assetName)
            result.assetName = p.assetName;
        if (p.assetURL)
            result.assetURL = p.assetURL;
        if (p.assetMetadataHash)
            result.assetMetadataHash = algosdk.bytesToBase64(p.assetMetadataHash);
        if (p.manager)
            result.manager = p.manager.toString();
        if (p.reserve)
            result.reserve = p.reserve.toString();
        if (p.freeze)
            result.freeze = p.freeze.toString();
        if (p.clawback)
            result.clawback = p.clawback.toString();
    }
    // Asset freeze fields
    if (txn.type === 'afrz' && txn.assetFreeze) {
        result.assetIndex = Number(txn.assetFreeze.assetIndex);
        result.freezeTarget = txn.assetFreeze.freezeAccount.toString();
        result.frozen = txn.assetFreeze.frozen;
    }
    // Application call fields
    if (txn.type === 'appl' && txn.applicationCall) {
        const p = txn.applicationCall;
        result.appIndex = Number(p.appIndex ?? 0n);
        result.onComplete = p.onComplete ?? 0;
        if (p.approvalProgram && p.approvalProgram.length > 0)
            result.approvalProgram = algosdk.bytesToBase64(p.approvalProgram);
        if (p.clearProgram && p.clearProgram.length > 0)
            result.clearProgram = algosdk.bytesToBase64(p.clearProgram);
        if (p.numGlobalInts !== undefined)
            result.numGlobalInts = p.numGlobalInts;
        if (p.numGlobalByteSlices !== undefined)
            result.numGlobalByteSlices = p.numGlobalByteSlices;
        if (p.numLocalInts !== undefined)
            result.numLocalInts = p.numLocalInts;
        if (p.numLocalByteSlices !== undefined)
            result.numLocalByteSlices = p.numLocalByteSlices;
        if (p.appArgs && p.appArgs.length > 0) {
            result.appArgs = p.appArgs.map((a) => algosdk.bytesToBase64(a));
        }
        if (p.accounts && p.accounts.length > 0) {
            result.accounts = p.accounts.map((a) => a.toString());
        }
        if (p.foreignApps && p.foreignApps.length > 0) {
            result.foreignApps = p.foreignApps.map((a) => Number(a));
        }
        if (p.foreignAssets && p.foreignAssets.length > 0) {
            result.foreignAssets = p.foreignAssets.map((a) => Number(a));
        }
    }
    // Key registration fields
    if (txn.type === 'keyreg' && txn.keyreg) {
        const p = txn.keyreg;
        if (p.voteKey)
            result.voteKey = algosdk.bytesToBase64(p.voteKey);
        if (p.selectionKey)
            result.selectionKey = algosdk.bytesToBase64(p.selectionKey);
        if (p.stateProofKey)
            result.stateProofKey = algosdk.bytesToBase64(p.stateProofKey);
        if (p.voteFirst !== undefined)
            result.voteFirst = Number(p.voteFirst);
        if (p.voteLast !== undefined)
            result.voteLast = Number(p.voteLast);
        if (p.voteKeyDilution !== undefined)
            result.voteKeyDilution = Number(p.voteKeyDilution);
        result.nonParticipation = p.nonParticipation ?? false;
    }
    return result;
}
export class GeneralTransactionManager {
    // Tool handlers
    static async handleTool(name, args) {
        switch (name) {
            case 'assign_group_id': {
                if (!args.transactions || !Array.isArray(args.transactions)) {
                    throw new McpError(ErrorCode.InvalidParams, 'Transactions array is required');
                }
                // Validate each transaction object
                for (const txn of args.transactions) {
                    if (typeof txn !== 'object' || txn === null) {
                        throw new McpError(ErrorCode.InvalidParams, 'Each transaction must be a valid transaction object');
                    }
                }
                // Create all transactions first
                let txns;
                try {
                    txns = args.transactions.map(txn => createTransactionFromFlat(txn));
                }
                catch (error) {
                    throw new McpError(ErrorCode.InvalidParams, `Failed to assign group ID: ${error instanceof Error ? error.message : String(error)}`);
                }
                // Then try to assign group ID
                try {
                    const groupedTxns = algosdk.assignGroupID(txns);
                    // Serialize each grouped transaction to flat JSON format
                    // matching the output of make_*_txn tools (base64 strings, not raw Uint8Arrays)
                    const serialized = groupedTxns.map(txn => serializeTransaction(txn));
                    return {
                        content: [{
                                type: 'text',
                                text: JSON.stringify(serialized, null, 2)
                            }]
                    };
                }
                catch (error) {
                    throw new McpError(ErrorCode.InvalidParams, `Failed to assign group ID: ${error instanceof Error ? error.message : String(error)}`);
                }
            }
            case 'sign_transaction': {
                if (!args.transaction || typeof args.transaction !== 'object' || !args.sk || typeof args.sk !== 'string') {
                    throw new McpError(ErrorCode.InvalidParams, 'Transaction object and sk (secret key hex) are required');
                }
                try {
                    const transaction = args.transaction;
                    const txn = createTransactionFromFlat(transaction);
                    const sk = algosdk.hexToBytes(args.sk);
                    const signedTxn = algosdk.signTransaction(txn, sk);
                    return {
                        content: [{
                                type: 'text',
                                text: JSON.stringify({
                                    txID: signedTxn.txID,
                                    blob: algosdk.bytesToBase64(signedTxn.blob)
                                }, null, 2)
                            }]
                    };
                }
                catch (error) {
                    throw new McpError(ErrorCode.InvalidParams, `Failed to sign transaction: ${error instanceof Error ? error.message : String(error)}`);
                }
            }
            case 'encode_unsigned_transaction': {
                if (!args.transaction || typeof args.transaction !== 'object') {
                    throw new McpError(ErrorCode.InvalidParams, 'Transaction object is required');
                }
                try {
                    const transaction = args.transaction;
                    const txn = createTransactionFromFlat(transaction);
                    const encoded = algosdk.encodeUnsignedTransaction(txn);
                    return {
                        content: [{
                                type: 'text',
                                text: JSON.stringify({
                                    txID: txn.txID(),
                                    bytes: algosdk.bytesToBase64(encoded)
                                }, null, 2)
                            }]
                    };
                }
                catch (error) {
                    throw new McpError(ErrorCode.InvalidParams, `Failed to encode unsigned transaction: ${error instanceof Error ? error.message : String(error)}`);
                }
            }
            case 'decode_signed_transaction': {
                if (!args.bytes || typeof args.bytes !== 'string') {
                    throw new McpError(ErrorCode.InvalidParams, 'Base64-encoded signed transaction bytes are required');
                }
                try {
                    const rawBytes = algosdk.base64ToBytes(args.bytes);
                    const decoded = decodeSignedTransaction(rawBytes);
                    const txn = decoded.txn;
                    const result = {
                        txID: txn.txID(),
                        transaction: serializeTransaction(txn),
                    };
                    if (decoded.sig) {
                        result.signature = algosdk.bytesToBase64(decoded.sig);
                    }
                    if (decoded.sgnr) {
                        result.authAddr = decoded.sgnr.toString();
                    }
                    if (decoded.msig) {
                        result.multisig = {
                            version: decoded.msig.v,
                            threshold: decoded.msig.thr,
                            subsigs: decoded.msig.subsig?.map((s) => ({
                                publicKey: s.pk ? algosdk.bytesToBase64(s.pk) : undefined,
                                signature: s.s ? algosdk.bytesToBase64(s.s) : undefined,
                            })),
                        };
                    }
                    if (decoded.lsig) {
                        result.logicSig = {
                            logic: algosdk.bytesToBase64(decoded.lsig.logic),
                            args: decoded.lsig.args?.map((a) => algosdk.bytesToBase64(a)),
                        };
                    }
                    return {
                        content: [{
                                type: 'text',
                                text: JSON.stringify(result, null, 2)
                            }]
                    };
                }
                catch (error) {
                    throw new McpError(ErrorCode.InvalidParams, `Failed to decode signed transaction: ${error instanceof Error ? error.message : String(error)}`);
                }
            }
            default:
                throw new McpError(ErrorCode.MethodNotFound, `Unknown general transaction tool: ${name}`);
        }
    }
}
