import { BigNumber, ethers } from 'ethers';
import { ChainID, EvmChainID, PublicKey as WirePubKey } from '@wireio/core';
import {
    IStakingClient,
    OPPAssertion,
    Portfolio,
    StakerConfig,
    TrancheSnapshot
} from '../../types';
import { EthereumContractService } from './contract';
import { preLaunchReceipt, WithdrawReceipt } from './types';
import { buildEthereumTrancheSnapshot, formatContractErrors } from './utils';
import { ConvertClient } from './clients/convert.client';
import { EthereumInstaswapClient } from './clients/instaswap.client';
import { StakeClient } from './clients/stake.client';
import { PretokenClient } from './clients/pretoken.client';
import { OPPClient } from './clients/opp.client';
import { ReceiptClient } from './clients/receipt.client';
import { ValidatorClient } from './clients/validator.client';

export const INITIAL_TRANCHE_SUPPLY = 35000;

export class EthereumStakingClient implements IStakingClient {
    private readonly provider: ethers.providers.Web3Provider | ethers.providers.JsonRpcProvider;
    public readonly pubKey?: WirePubKey;
    private readonly signer?: ethers.Signer;
    private readonly contractService: EthereumContractService;

    private convertClient: ConvertClient;
    private pretokenClient: PretokenClient;
    private stakeClient: StakeClient;
    private oppClient: OPPClient;
    private receiptClient: ReceiptClient;
    private validatorClient: ValidatorClient;
    public readonly instaswapClient: EthereumInstaswapClient;

    get contract() { return this.contractService.contract; }
    get network() { return this.config.network; }
    get address() { return this.signer?.getAddress(); }

    constructor(private config: StakerConfig) {
        try {
            if (config.provider) {
                this.provider = config.provider as ethers.providers.Web3Provider;
                this.signer = this.provider.getSigner();
            }
            else {
                this.provider = new ethers.providers.JsonRpcProvider(config.network.rpcUrls[0]);
            }
            this.pubKey = config.pubKey;

            this.contractService = new EthereumContractService({
                provider: this.provider,
                signer: this.signer,
                chainId: config.network.chainId as EvmChainID,
            });

            this.convertClient = new ConvertClient(this.contractService);
            this.pretokenClient = new PretokenClient(this.contractService);
            this.stakeClient = new StakeClient(this.contractService);
            this.oppClient = new OPPClient(this.contractService);
            this.receiptClient = new ReceiptClient(this.contractService);
            this.validatorClient = new ValidatorClient(this.contractService);
            this.instaswapClient = new EthereumInstaswapClient(
                this.contractService,
                this.signer,
                config.network.chainId as number,
                config.network.rpcUrls[0],
            );
        }
        catch (error) {
            // console.error('Error initializing EthereumStakingClient:', error);
            throw error;
        }
    }

    // ---------------------------------------------------------------------
    // Public IStakingClient Interface Methods
    // ---------------------------------------------------------------------

    /**
     * Deposit native ETH into the liqETH protocol via DepositManager.
     * @param amount  Amount in wei (or something convertible to BigNumber).
     *                Keep this as a bigint / string in the caller; avoid JS floats.
     * @returns transaction hash
     */
    async deposit(amount: number | string | bigint | BigNumber): Promise<string> {
        this.ensureUser();

        const amountWei = BigNumber.isBigNumber(amount)
            ? amount
            : BigNumber.from(amount);

        const result = await this.convertClient.performDeposit(amountWei);
        return result.txHash;
    }

    /**
     * Withdraw native ETH from the liqETH protocol via the liqeth safeBurn function, which burns the LiqETH and adds the user to the withdrawal queue.
     * @param amount  Amount in wei (or something convertible to BigNumber).
     * @returns transaction hash
     */
    async withdraw(amount: bigint): Promise<string> {
        this.ensureUser();

        const address = await this.address!;
        const amountWei = BigNumber.from(amount);

        const result = await this.convertClient.performWithdraw(address, amountWei)
        return result.txHash;
    }

    /**
     * List withdrawal queue receipts for a wallet (pending / in-progress exits after safeBurn).
     * @param address  Owner to query; defaults to the connected wallet.
     * @returns Withdrawal receipt NFT entries from the protocol view
     */
    async getPendingWithdraws(address?: string): Promise<WithdrawReceipt[]> {
        this.ensureUser();
        const owner = address ?? await this.address!;
        return await this.receiptClient.fetchWithdrawReceipts(owner);
    }

    /**
     * Claim native ETH for a matured withdrawal using the WithdrawalQueue NFT token id
     * (via DepositManager.claim after the delay has passed).
     * @param tokenId  Withdrawal request / queue NFT token id
     * @returns transaction hash
     */
    async claimWithdraw(tokenId: bigint): Promise<string> {
        this.ensureUser();

        const tokenIdBigNum = BigNumber.from(tokenId)
        const result = await this.convertClient.claimWithdraw(tokenIdBigNum)
        return result.txHash;
    }


    /**
     * Stake liqETH via DepositManager.
     * @param amount  Amount in wei - Keep this as a bigint / string in the caller; avoid JS floats.
     * @returns transaction hash
     */
    async stake(amount: bigint, wireAccount?: string): Promise<string> {
        this.ensureUser();

        const walletAddress = await this.address!;
        const amountWei = BigNumber.from(amount);

        const result = await this.stakeClient.performStakeToWire(amountWei, walletAddress, wireAccount);
        return result.txHash;
    }


    // TODO
    async unstake(): Promise<string> {
        throw new Error("Method not yet implemented.");
    }

    /**
     * Deposit ETH to liqETH, then stake the liqETH.
     * @param amountWei  Amount in wei.
     * @returns transaction hash
     */
    async depositAndStake(amount: bigint, wireAccount?: string): Promise<string> {
        this.ensureUser();
        if (!amount || amount <= BigInt(0))
            throw new Error('Amount must be greater than zero.');


        const walletAddress = await this.address!;
        let amountWei = BigNumber.from(amount);


        const result = await this.convertClient.performDeposit(amountWei);
        console.log('result of performDeposit()', result);

        const result2 = await this.stakeClient.performStakeToWire(amountWei, walletAddress, wireAccount);
        console.log('result of performStakeToWire()', result2);

        // const result = await this.stakeClient.performDepositAndStakeToWire(BigNumber.from(amountWei), wireAccount);
        return result2.txHash;
    }

    /**
     * Deposit ETH to liqETH, then purchase pretokens in a single transaction.
     * @param amountWei  Amount in wei.
     * @returns transaction hash
     */
    async depositAndBuy(amountWei: bigint): Promise<string> {
        this.ensureUser();
        if (!amountWei || amountWei <= BigInt(0))
            throw new Error('Amount must be greater than zero.');

        const result = await this.stakeClient.performDepositAndPurchase(BigNumber.from(amountWei));
        return result.txHash;
    }

    /**
     * Unstake liqETH, then request a withdrawal in a single transaction.
     * @param tokenId  The ReceiptNFT token ID for the stake position
     * @returns transaction hash
     */
    async unstakeAndWithdraw(tokenId: bigint): Promise<string> {
        this.ensureUser();

        const result = await this.stakeClient.performUnstakeAndRequestWithdraw(BigNumber.from(tokenId));
        return result.txHash;
    }


    /**
     * ETH Prelaunch function to unstake liqEth
     * @param tokenId ReceiptNFT tokenId for the owned NFT that will be burned
     * @param recipient Address to receive the liqEth funds linked to the burned NFT
     * @returns the transaction hash
     */
    async unstakePrelaunch(tokenId: bigint, recipient: string): Promise<string> {
        this.ensureUser();

        const tokenIdBigNum = BigNumber.from(tokenId)
        const result = await this.stakeClient.performWithdrawStake(tokenIdBigNum, recipient);
        return result.txHash;
    }


    async buy(amount: bigint): Promise<string> {
        this.ensureUser();

        const buyer = await this.address!;

        let result = await this.pretokenClient.purchasePretokensWithLiqETH(amount, buyer);
        return result && result.txHash ? result.txHash : "Error - no resulting txHash";
    }


    /**
     * Validator functions
     */
    async validatorDeposit(): Promise<string> {
        this.ensureUser();

        let result = await this.validatorClient.validatorDepositAndLockBond();
        return result && result.txHash ? result.txHash : "Error - no resulting txHash";
    }
    




    // ! Testnet only - check if the mock aggregator price is stale, and if so, update it before submitting the buy request
    // async crankTestnetAggregator(): Promise<string> {
    //     this.ensureUser();

    //     const network = await this.provider.getNetwork();
    //     const chainId = network.chainId;
    //     const allowedTestChains = new Set([560048, 31337]);
    //     if (!allowedTestChains.has(chainId)) return "Testnet crank is only intended for test/local networks - current chain is "+chainId+". Skipping aggregator update";

    //     const [agg, opp] = await Promise.allSettled([
    //         this.updateMockAggregatorPrice(),
    //         this.fixOppTooManyAttestations(),
    //     ]);

    //     const fmt = (name: string, r: PromiseSettledResult<unknown>) => {
    //         if (r.status === 'fulfilled') {
    //             const suffix = r.value !== undefined ? ` (${String(r.value)})` : '';
    //             return `${name}: ok${suffix}`;
    //         }
    //         return `${name}: failed (${String(r.reason)})`;
    //     };

    //     console.log('crankTestnetAggregator()', { agg, opp });
    //     return `${fmt('updateMockAggregatorPrice', agg)}; ${fmt('fixOppTooManyAttestations', opp)}`;
    // }



    /**
     * Resolve the user's ETH + liqETH balances.
     *
     * native  = ETH in wallet
     * actual  = liqETH token balance (ERC-20)
     * tracked = liqETH tracked balance (protocol/accounting view)
     */
    async getPortfolio(address?: string): Promise<Portfolio | null> {

        try {
            if (!this.signer) return Promise.resolve(null);

            const walletAddress = address ?? await this.address!;

            // 1) Native ETH balance
            const nativeBalance = await this.provider.getBalance(walletAddress);
            const nativeDecimals = this.network?.nativeCurrency?.decimals ?? 18;
            const nativeSymbol = this.network?.nativeCurrency?.symbol ?? 'ETH';

            // 2) liqETH ERC-20 balance (actual)
            const liqBalance: ethers.BigNumber = await this.contract.LiqEthToken.balanceOf(walletAddress);
            const liqSymbol = 'Liq' + (this.network?.nativeCurrency?.symbol ?? 'ETH');

            // 3) staked liqEth ERC-20 balance (calculate from receipts)
            let stakeReceipts = await this.receiptClient.stakeReceipts(walletAddress);

            let stakeBalanceBN = BigNumber.from(0);
            for (let r of stakeReceipts) {
                stakeBalanceBN = stakeBalanceBN.add(BigNumber.from(r.receipt.principal.amount));
            }
            let stakeSharesBN = BigNumber.from(0);
            for (let r of stakeReceipts) {
                stakeSharesBN = stakeSharesBN.add(BigNumber.from(r.receipt.shares.amount));
            }

            // 4) WIRE pretoken balance
            const wireBalance: ethers.BigNumber = await this.contract.Pretoken.balanceOf(walletAddress);

            // 5) Calculate staking yield 
            let currentIndex = BigInt(0);
            let totalShares = BigInt(0);
            let userShares = BigInt(0);
            const indexScale = BigInt(1e27);
            try {
                // These may throw if not implemented on contract
                const [indexBn, totalSharesBn] = await Promise.all([
                    this.contract.Depositor.index().catch(() => BigNumber.from(0)),
                    this.contract.Depositor.totalShares().catch(() => BigNumber.from(0)),
                ]);

                const userSharesBn = stakeSharesBN;
                currentIndex = BigInt(indexBn.toString());
                totalShares = BigInt(totalSharesBn.toString());
                userShares = BigInt(userSharesBn.toString());
            } catch (error) {
                console.log('Error fetching staking index/shares:', error);
            }

            // sharesToTokens(userShares, currentIndex) = userShares * currentIndex / indexScale
            let estimatedClaim = BigInt(0);
            let estimatedYield = BigInt(0);
            
            if (userShares > BigInt(0) && currentIndex > BigInt(0)) {
                estimatedClaim = (userShares * currentIndex) / indexScale;
                if (estimatedClaim > stakeBalanceBN.toBigInt()) {
                    estimatedYield = estimatedClaim - stakeBalanceBN.toBigInt();
                }
            }


            const portfolio: Portfolio = {
                native: {
                    amount: nativeBalance.toBigInt(),
                    decimals: nativeDecimals,
                    symbol: nativeSymbol,
                },
                liq: {
                    amount: liqBalance.toBigInt(),
                    decimals: nativeDecimals,
                    symbol: liqSymbol,
                },
                staked: {
                    amount: stakeBalanceBN.toBigInt(),
                    decimals: nativeDecimals,
                    symbol: liqSymbol,
                },
                wire: {
                    amount: wireBalance.toBigInt(),
                    decimals: 18,
                    symbol: '$WIRE',
                },

                yield: {
                    currentIndex,
                    indexScale,
                    totalShares,
                    userShares,
                    estimatedClaim,
                    estimatedYield,
                },
                chainID: this.network.chainId
            }
            return portfolio;
        }
        catch (error) {
            // console.log('Error fetching Ethereum portfolio:', error);
            throw error;
        }
    }

    /**
     * ETH Prelaunch function to list the Stake ReceiptNFTs owned by a specific user
     * @param address address to query the receipts for
     * @returns array of receipts
     */
    async fetchPrelaunchReceipts(address?: string): Promise<preLaunchReceipt[]> {
        this.ensureUser();

        if (address === undefined) address = await this.address!;

        //default to stake receipts
        return await this.receiptClient.stakeReceipts(address);
    }

    async getOPPMessages(address?: string): Promise<OPPAssertion[]> {
        this.ensureUser();

        if (!address) address = await this.address!;

        return await this.oppClient.getMessages(address);
    }



    // Ensure that signer wallet is available for write operations
    private ensureUser() {
        if (!this.signer) {
            throw new Error(
                'EthereumStakingClient: write operation requires a wallet-connected Web3 provider',
            );
        }
    }


    // ---------------------------------------------------------------------
    // READ-ONLY Public Methods
    // ---------------------------------------------------------------------

    // Protocol-wide ETH staking APY in percent, e.g. 3.0 => "3.00%"
    async getSystemAPY(): Promise<number> {
        // NOTE: despite the name, this value is effectively *annual* BPS on-chain.
        // e.g. 300 => 3% APY
        const annualBpsBn = await this.contract.DepositManager.dailyRateBPS();
        const annualBps = annualBpsBn.toNumber(); // e.g. 300 for 3%

        // Convert basis points (1/100 of 1%) to percent:
        // 10,000 bps = 100% ⇒ 100 bps = 1% ⇒ divide by 100.
        const apyPercent = annualBps / 100;

        return apyPercent; // 3 => "3.00%"
    }

    // Protocol fee charged for deposit from Native to LIQ
    async getDepositFee(amountWei: bigint): Promise<bigint> {
        const feeBn: BigNumber = await this.contract.DepositManager.procFee(amountWei);
        return BigInt(feeBn.toString());
    }

    async getOPPStatus(): Promise<any> {
        return await this.oppClient.getStatus();
    }

    async getEthStats(): Promise<any> {
        let withdrawDelay = await this.contract.WithdrawalQueue.withdrawDelay();
        let minDeposit = await this.contract.DepositManager.minDeposit();
        let rewardCooldown = await this.contract.DepositManager.rewardCooldown();

        return {
            withdrawDelay,
            minDeposit,
            rewardCooldown,
        }
    }

    /**
     * Program-level prelaunch WIRE / tranche snapshot for Ethereum
     * 
     * SUPPORTS READ-ONLY ACcESS
     */
    async getTrancheSnapshot(chainID: ChainID): Promise<TrancheSnapshot> {
        try {
            const blockNumber = await this.provider.getBlockNumber();
            const blockTag = { blockTag: blockNumber };

            // Fetch all required contract data
            const [totalSharesBn, indexBn, trancheNumberBn, trancheSupplyBn, tranchePriceUsdBn, totalSupplyBn, supplyGrowthBps, priceGrowthCents, minPriceUsd, maxPriceUsd] = await Promise.all([
                this.contract.Depositor.totalShares(blockTag),
                this.contract.Depositor.index(blockTag),
                this.contract.Pretoken.trancheNumber(blockTag),
                this.contract.Pretoken.trancheSupply(blockTag),
                this.contract.Pretoken.tranchePriceUsd(blockTag),
                this.contract.Pretoken.totalSupply(blockTag),
                this.contract.Pretoken.supplyGrowthBps(blockTag),
                this.contract.Pretoken.priceIncrementUsd(blockTag),
                this.contract.EthUsdPriceConsumer.MIN_PRICE(),
                this.contract.EthUsdPriceConsumer.MAX_PRICE(),
            ]);

            const totalTrancheSupply = BigInt(totalSupplyBn.toString()) / BigInt(1e10);
            const currentTrancheSupply = BigInt(trancheSupplyBn.toString()) / BigInt(1e10);

            // fetch price and timestamp from aggregator
            // const [roundId, answer, startedAt, updatedAt, answeredInRound] = await this.contract.MockAggregator.latestRoundData();
            //originally fetched ethPrice from aggregator - pulling from EthUsdPriceConsumer instead - this is where the actual price gets pulled from when purchasing pretokens.
            // let ethPriceUsd: bigint = BigInt(answer.toString());)

            let ethPriceUsdBn = await this.contract.EthUsdPriceConsumer.getPrice18Decimals();
            let ethPriceUsd: bigint = BigInt(ethPriceUsdBn.toString()) / BigInt(1e10);

            // let nativePriceTimestamp: number = Number(updatedAt);

            const initialTrancheSupply = BigInt(INITIAL_TRANCHE_SUPPLY) * BigInt(1e8);

            return buildEthereumTrancheSnapshot({
                chainID,
                totalSharesBn,
                indexBn,
                trancheNumberBn,
                currentTrancheSupply,
                tranchePriceUsdBn,
                totalTrancheSupply,
                initialTrancheSupply,
                supplyGrowthBps,
                priceGrowthCents,
                minPriceUsd,
                maxPriceUsd,
                ethPriceUsd,
            });
        }
        catch (err: any) {
            throw new Error(`Error fetching Ethereum tranche snapshot: ${err?.message || err}`);
        }
    }

    /**
     * Estimate a conservative native ETH buffer (in wei) to leave in the wallet
     * so the user can pay gas for the current deposit and at least one more tx.
     *
     * Typical usage in UI:
     *   const buffer = await client.estimateGasBuffer();
     *   const maxSpendable = balanceWei > buffer ? balanceWei - buffer : 0n;
     *
     * @param options.txCount         How many transactions to cover (default 2: deposit + 1 more)
     * @param options.safetyMultiplier Additional safety multiplier on top of txCount (default 1.5x)
     * @param options.minBufferWei    Optional override minimum buffer (defaults ~0.002 ETH)
     */
    async getDepositBuffer(options?: {
        txCount?: number;
        safetyMultiplier?: number;
        minBufferWei?: bigint;
        balanceOverrideLamports?: bigint;
    }): Promise<bigint> {
        this.ensureUser();

        const walletAddress = await this.address!;

        // 1) Estimate a baseline gas usage using a simple self-transfer.
        //    This is cheap and doesn't depend on your contract ABI at all.
        const baseGas = await this.provider.estimateGas({
            from: walletAddress,
            to: walletAddress,
            value: ethers.constants.Zero,
        });

        // 2) Fetch current gas price / max fee per gas.
        const feeData = await this.provider.getFeeData();
        let gasPrice =
            feeData.maxFeePerGas ??
            feeData.gasPrice ??
            ethers.utils.parseUnits('20', 'gwei'); // conservative fallback

        // 3) How many txs do we want to cover?
        //    Default: 2 (deposit + one extra action such as stake or small follow-up).
        const txCount = options?.txCount ?? 2;

        // We also assume that contract interactions are more expensive than a simple transfer.
        // Use a multiplier (e.g., 5x) on baseGas to approximate a more complex tx.
        const COMPLEX_TX_MULTIPLIER = 5; // tuning knob
        const totalGasUnits = baseGas
            .mul(COMPLEX_TX_MULTIPLIER)
            .mul(txCount);

        const baseCost = totalGasUnits.mul(gasPrice);

        // 4) Safety multiplier on top of that (e.g. 1.5x).
        const safetyMultiplier = options?.safetyMultiplier ?? 1.5;
        const safetyScaled = Math.round(safetyMultiplier * 100); // e.g. 150

        const bufferedCost = baseCost
            .mul(safetyScaled)
            .div(100); // apply safety factor

        let bufferWei = bufferedCost.toBigInt();

        // 5) Enforce a minimum floor (e.g. ~0.002 ETH).
        const defaultMinBufferWei = BigInt(2_000_000_000_000_000); // 0.002 ETH
        const minBufferWei = options?.minBufferWei ?? defaultMinBufferWei;

        if (bufferWei < minBufferWei) {
            bufferWei = minBufferWei;
        }

        return bufferWei;
    }

    // ---------------------------------------------------------------------
    // Internal ETH Staking client helper functions
    // ---------------------------------------------------------------------

    // ! This is a temporary measure for testnet because there is no aggregator deployed
    /*
    private async updateMockAggregatorPrice() {
        const aggregator = this.contract.MockAggregator;

        // read latest round & compute age
        const [roundId, answer, startedAt, updatedAt, answeredInRound] = await aggregator.latestRoundData();
        const now = (await this.provider.getBlock("latest")).timestamp;
        const ageSec = Number(now) - Number(updatedAt);

        const ONE_HOUR = 1 * 3600;
        // const ONE_HOUR = 10;
        if (ageSec > ONE_HOUR) {
            console.log('Attempt to run updateMockAggregatorPrice - MockAggregator price is stale by', ageSec, 'seconds - updating price before proceeding with buy...');

            // safety check - only run in non-production contexts
            const network = await this.provider.getNetwork();
            const chainId = network.chainId;
            const allowedTestChains = new Set([560048, 31337]);
            if (!allowedTestChains.has(chainId)) {
                console.warn(`MockAggregator is stale (${ageSec}s) but chainId ${chainId} is not a test/local network — skipping update.`);
                return;
            }


            //fetch the current ETH / USD price
            const res = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd');
            const data = await res.json();
            const ethUsd = data.ethereum.usd;

            const currentEthPrice = ethers.utils.parseUnits(ethUsd.toString(), 8);

            console.log('currentEthPrice', currentEthPrice.toString())

            try {
                //update to be intentionally stale
                // const alttx = await aggregator.updateStale(currentEthPrice, now - 7200);
                // const altreceipt = await alttx.wait(1);
                // console.log('stale update receipt', altreceipt)

                //update answer with current timestamp
                const tx = await aggregator.updateAnswer(currentEthPrice);
                const txreceipt = await tx.wait(1);
                console.log('MockAggregator answer updated - receipt:', txreceipt)
                return txreceipt
            } catch (err: any) {
                console.error('MockAggregator updateAnswer failed', err?.message || err);
            }
        } else {
            console.log(`MockAggregator updated ${ageSec}s ago — no update needed`);
        }
    }


    private async fixOppTooManyAttestations() {
        const opp = this.contract.OPP;
        const outpostManager = this.contract.OutpostManager;

        const [authority, pendingCountRaw, maxRaw, sendModeTagRaw] = await Promise.all([
            opp.authority(),
            opp.pendingAttestationCount(),
            opp.MESSAGE_EPOCH_MAX_ATTESTATIONS(),
            opp.sendModeTag(),
        ]);

        const toBigInt = (v: any) => (typeof v === "bigint" ? v : BigInt(v.toString()));
        const pendingCount = toBigInt(pendingCountRaw);
        const maxAttestations = toBigInt(maxRaw);
        const sendModeTag = toBigInt(sendModeTagRaw);

        console.log(`OPP status: authority=${authority}, pendingAttestationCount=${pendingCount}, MESSAGE_EPOCH_MAX_ATTESTATIONS=${maxAttestations}, sendModeTag=${sendModeTag}`);
        
        if (sendModeTag === 0n) {
            console.log("OPP is not in send mode.");
            // return;
        }

        const shouldBumpMax = maxAttestations === 1n || pendingCount > maxAttestations;
        if (!shouldBumpMax) {
            console.log(
                `No fix needed: pendingAttestationCount=${pendingCount} <= MESSAGE_EPOCH_MAX_ATTESTATIONS=${maxAttestations}`,
            );
            // return;
        }

        console.log(
            `OPP bump needed (pending=${pendingCount}, max=${maxAttestations}) — attempting to set new max and ${sendModeTag === 0n ? 'skip' : 'call'} exitSendMode(${sendModeTag})...`
        );

        console.log(await this.provider.getNetwork());

        

        // safety check to only run in testnet
        const network = await this.provider.getNetwork();
        const chainId = toBigInt(network.chainId);
        const allowedTestChains = new Set<bigint>([560048n, 31337n]);
        if (!allowedTestChains.has(chainId)) {
            console.warn(
            `Refusing to modify OPP config on chainId ${chainId}. Add it to allowedTestChains if this is expected.`
            );
            return;
        }

        // minimal bump bump: set max to pendingCount (or a small default when max was 1) + buffer.
        const base = pendingCount > 0n ? pendingCount : 1n;
        // const newMax = base + 2n; // add a small buffer to prevent immediate recurrence
        const newMax = 1n; // add a small buffer to prevent immediate recurrence

        const encodeOppCall = (fn: string, args: any[]) =>
            opp.interface.encodeFunctionData(fn, args);

        try {
            const data = encodeOppCall('setMessageEpochMaxAttestations', [newMax]);
            await outpostManager.callStatic.execute(opp.address, data);
            console.log('OutpostManager.execute(callStatic) setMessageEpochMaxAttestations passed');
        } catch (e) {
            console.error('OutpostManager.execute(callStatic) setMessageEpochMaxAttestations revert:', e);
        }


        try {
            const data1 = encodeOppCall('setMessageEpochMaxAttestations', [newMax]);
            const tx1 = await outpostManager.execute(opp.address, data1, {
                gasLimit: 500_000,
            });
            const receipt1 = await tx1.wait(1);
            console.log("setMessageEpochMaxAttestations confirmed:", receipt1);

            if (sendModeTag !== 0n) {
                const data2 = encodeOppCall('exitSendMode', [sendModeTag]);
                const tx2 = await outpostManager.execute(opp.address, data2, {
                    gasLimit: 500_000,
                });
                const receipt2 = await tx2.wait(1);
                console.log("exitSendMode confirmed:", receipt2);
                return { setMaxReceipt: receipt1, exitReceipt: receipt2 };
            }

            return { setMaxReceipt: receipt1 };
        } catch (err: any) {
            let errorObj = formatContractErrors(err);
            console.error("fixOppTooManyAttestations failed:", err?.message || err, errorObj);
        }
    }
    */
}
