import { Address, HasAddress } from '@solana/addresses';

import { TransactionMessage } from './transaction-message';

/**
 * Represents a transaction message for which a fee payer has been declared. A transaction must
 * conform to this type to be compiled and landed on the network.
 */
export interface TransactionMessageWithFeePayer<TAddress extends string = string> {
    readonly feePayer: HasAddress<TAddress>;
}

/**
 * A helper type to exclude the fee payer from a transaction message.
 */
type ExcludeTransactionMessageFeePayer<TTransactionMessage extends TransactionMessage> =
    TTransactionMessage extends unknown ? Omit<TTransactionMessage, 'feePayer'> : never;

/**
 * Given a base58-encoded address of a system account, this method will return a new transaction
 * message having the same type as the one supplied plus the {@link TransactionMessageWithFeePayer}
 * type.
 *
 * @example
 * ```ts
 * import { address } from '@solana/addresses';
 * import { setTransactionMessageFeePayer } from '@solana/transaction-messages';
 *
 * const myAddress = address('mpngsFd4tmbUfzDYJayjKZwZcaR7aWb2793J6grLsGu');
 * const txPaidByMe = setTransactionMessageFeePayer(myAddress, tx);
 * ```
 */
export function setTransactionMessageFeePayer<
    TFeePayerAddress extends string,
    TTransactionMessage extends Partial<TransactionMessageWithFeePayer> & TransactionMessage,
>(
    feePayer: Address<TFeePayerAddress>,
    transactionMessage: TTransactionMessage,
): ExcludeTransactionMessageFeePayer<TTransactionMessage> & TransactionMessageWithFeePayer<TFeePayerAddress> {
    if (
        'feePayer' in transactionMessage &&
        feePayer === transactionMessage.feePayer?.address &&
        isAddressOnlyFeePayer(transactionMessage.feePayer)
    ) {
        return transactionMessage as ExcludeTransactionMessageFeePayer<TTransactionMessage> &
            TransactionMessageWithFeePayer<TFeePayerAddress>;
    }
    const out = {
        ...transactionMessage,
        feePayer: Object.freeze({ address: feePayer }),
    };
    Object.freeze(out);
    return out as ExcludeTransactionMessageFeePayer<TTransactionMessage> &
        TransactionMessageWithFeePayer<TFeePayerAddress>;
}

function isAddressOnlyFeePayer(
    feePayer: Partial<TransactionMessageWithFeePayer>['feePayer'],
): feePayer is { address: Address } {
    return (
        !!feePayer &&
        'address' in feePayer &&
        typeof feePayer.address === 'string' &&
        Object.keys(feePayer).length === 1
    );
}
