import { PublicKey } from '@solana/web3.js';
import BN from 'bn.js';
import copy from 'fast-copy';
import { HealthType } from './accounts/mangoAccount';
import { I80F48, ONE_I80F48, ZERO_I80F48 } from './numbers/I80F48';
import { buildFetch, toUiDecimals, toUiDecimalsForQuote } from './utils';
/**
 * Returns price impact in bps i.e. 0 to 10,000
 * returns -1 if data is missing
 */
export function computePriceImpactOnJup(pis, usdcAmount, tokenName) {
    try {
        const closestTo = [
            1_000, 5_000, 20_000, 100_000, 250_000, 500_000, 1_000_000, 5_000_000,
        ].reduce((prev, curr) => Math.abs(curr - usdcAmount) < Math.abs(prev - usdcAmount) ? curr : prev);
        // Workaround api
        if (tokenName == 'ETH (Portal)') {
            tokenName = 'ETH';
        }
        const filteredPis = pis.filter((pi) => pi.symbol == tokenName && pi.target_amount == closestTo);
        if (filteredPis.length > 0) {
            return (filteredPis[0].p90 * 10000) / 100;
        }
        else {
            return -1;
        }
    }
    catch (e) {
        return -1;
    }
}
export async function getOnChainPriceForMints(mints) {
    return await Promise.all(mints.map(async (mint) => {
        const resp = await (await buildFetch())(`https://public-api.birdeye.so/public/price?address=${mint}`, {
            headers: {
                'Content-Type': 'application/json',
            },
        });
        const data = await resp.json();
        return data?.data?.value;
    }));
}
export async function getPriceImpactForLiqor(group, pis, mangoAccounts) {
    const mangoAccounts_ = mangoAccounts.filter((a) => a.getHealth(group, HealthType.maint).lt(ZERO_I80F48()));
    const mangoAccountsWithHealth = mangoAccounts_.map((a) => {
        return {
            account: a,
            health: a.getHealth(group, HealthType.liquidationEnd),
            healthRatio: a.getHealthRatioUi(group, HealthType.liquidationEnd),
        };
    });
    const usdcBank = group.getFirstBankByTokenIndex(0);
    const usdcMint = usdcBank.mint;
    return await Promise.all(Array.from(group.banksMapByMint.values())
        .sort((a, b) => a[0].name.localeCompare(b[0].name))
        // .filter((banks) => banks[0].name == 'MSOL')
        .map(async (banks) => {
        const bank = banks[0];
        // Sum of all liabs, these liabs would be acquired by liqor,
        // who would immediately want to reduce them to 0
        // Assuming liabs need to be bought using USDC
        const liabs = 
        // Max liab of a particular token that would be liquidated to bring health above 0
        mangoAccountsWithHealth.reduce((sum, a) => {
            // How much would health increase for every unit liab moved to liqor
            // liabprice * (liabweight - (1+liabfees)*(1+assetfees)*assetweight)
            // Choose the most valuable asset the user has
            const assetBank = Array.from(group.banksMapByTokenIndex.values())
                .flat()
                .reduce((prev, curr) => prev.initAssetWeight
                .mul(a.account.getEffectiveTokenBalance(group, prev))
                .mul(prev._price)
                .gt(curr.initAssetWeight.mul(a.account
                .getEffectiveTokenBalance(group, curr)
                .mul(curr._price)))
                ? prev
                : curr);
            const feeFactor = ONE_I80F48()
                .add(bank.liquidationFee)
                .add(bank.platformLiquidationFee)
                .mul(ONE_I80F48()
                .add(assetBank.liquidationFee)
                .add(assetBank.platformLiquidationFee));
            const tokenLiabHealthContrib = bank.price.mul(bank.initLiabWeight.sub(feeFactor.mul(assetBank.initAssetWeight)));
            // Abs liab/borrow
            const maxTokenLiab = a.account
                .getEffectiveTokenBalance(group, bank)
                .min(ZERO_I80F48())
                .abs();
            if (tokenLiabHealthContrib.eq(ZERO_I80F48())) {
                return sum.add(maxTokenLiab);
            }
            // Health under 0
            const maxLiab = a.health
                .min(ZERO_I80F48())
                .abs()
                .div(tokenLiabHealthContrib)
                .min(maxTokenLiab);
            return sum.add(maxLiab);
        }, ZERO_I80F48());
        const liabsInUsdc = 
        // convert to usdc, this is an approximation
        liabs
            .mul(bank.price)
            .floor()
            // jup oddity
            .min(I80F48.fromNumber(99999999999));
        // Sum of all assets which would be acquired in exchange for also acquiring
        // liabs by the liqor, who would immediately want to reduce to 0
        // Assuming assets need to be sold to USDC
        const assets = mangoAccountsWithHealth.reduce((sum, a) => {
            // How much would health increase for every unit liab moved to liqor
            // assetprice * (liabweight/(1+liabliqfee) - assetweight)
            // Choose the smallest liability the user has
            const liabBank = Array.from(group.banksMapByTokenIndex.values())
                .flat()
                .reduce((prev, curr) => prev.initLiabWeight
                .mul(a.account.getEffectiveTokenBalance(group, prev))
                .mul(prev._price)
                .lt(curr.initLiabWeight.mul(a.account
                .getEffectiveTokenBalance(group, curr)
                .mul(curr._price)))
                ? prev
                : curr);
            const tokenAssetHealthContrib = bank.price.mul(liabBank.initLiabWeight
                .div(ONE_I80F48().add(liabBank.liquidationFee))
                .sub(bank.initAssetWeight));
            // Abs collateral/asset
            const maxTokenHealthAsset = a.account
                .getEffectiveTokenBalance(group, bank)
                .max(ZERO_I80F48());
            if (tokenAssetHealthContrib.eq(ZERO_I80F48())) {
                return sum.add(maxTokenHealthAsset);
            }
            const maxAsset = a.health
                .min(ZERO_I80F48())
                .abs()
                .div(tokenAssetHealthContrib)
                .min(maxTokenHealthAsset);
            return sum.add(maxAsset);
        }, ZERO_I80F48());
        const pi1 = !liabsInUsdc.eq(ZERO_I80F48()) &&
            usdcMint.toBase58() !== bank.mint.toBase58()
            ? computePriceImpactOnJup(pis, toUiDecimalsForQuote(liabsInUsdc), bank.name)
            : 0;
        const pi2 = !assets.eq(ZERO_I80F48()) &&
            usdcMint.toBase58() !== bank.mint.toBase58()
            ? computePriceImpactOnJup(pis, toUiDecimals(assets.mul(bank.price), bank.mintDecimals), bank.name)
            : 0;
        return {
            Coin: { val: bank.name, highlight: false },
            'Oracle Price': {
                val: bank['oldUiPrice'] ? bank['oldUiPrice'] : bank._uiPrice,
                highlight: false,
            },
            'Jup Price': {
                val: bank['onChainPrice'],
                highlight: Math.abs((bank['onChainPrice'] -
                    (bank['oldUiPrice'] ? bank['oldUiPrice'] : bank._uiPrice)) /
                    (bank['oldUiPrice'] ? bank['oldUiPrice'] : bank._uiPrice)) > 0.05,
            },
            'Future Price': { val: bank._uiPrice, highlight: false },
            'V4 Liq Fee': {
                val: Math.round(bank.liquidationFee.toNumber() * 10000),
                highlight: false,
            },
            Liabs: {
                val: Math.round(toUiDecimalsForQuote(liabsInUsdc)),
                highlight: Math.round(toUiDecimalsForQuote(liabsInUsdc)) > 5000,
            },
            'Liabs Slippage': {
                val: Math.round(pi1),
                highlight: Math.round(pi1) >
                    Math.round(bank.liquidationFee.toNumber() * 10000),
            },
            Assets: {
                val: Math.round(toUiDecimals(assets, bank.mintDecimals) * bank.uiPrice),
                highlight: Math.round(toUiDecimals(assets, bank.mintDecimals) * bank.uiPrice) > 5000,
            },
            'Assets Slippage': {
                val: Math.round(pi2),
                highlight: Math.round(pi2) >
                    Math.round(bank.liquidationFee.toNumber() * 10000),
            },
        };
    }));
}
export async function getPerpPositionsToBeLiquidated(group, mangoAccounts) {
    const mangoAccountsWithHealth = mangoAccounts.map((a) => {
        return {
            account: a,
            health: a.getHealth(group, HealthType.liquidationEnd),
            healthRatio: a.getHealthRatioUi(group, HealthType.liquidationEnd),
        };
    });
    return Array.from(group.perpMarketsMapByMarketIndex.values())
        .filter((pm) => !pm.name.includes('OLD'))
        .map((pm) => {
        const baseLots = mangoAccountsWithHealth
            .filter((a) => a.account.getPerpPosition(pm.perpMarketIndex))
            .reduce((sum, a) => {
            const baseLots = a.account.getPerpPosition(pm.perpMarketIndex).basePositionLots;
            const unweightedHealthPerLot = baseLots.gt(new BN(0))
                ? I80F48.fromNumber(-1)
                    .mul(pm.price)
                    .mul(I80F48.fromU64(pm.baseLotSize))
                    .mul(pm.initBaseAssetWeight)
                    .add(I80F48.fromU64(pm.baseLotSize)
                    .mul(pm.price)
                    .mul(ONE_I80F48() // quoteInitAssetWeight
                    .mul(ONE_I80F48().sub(pm.baseLiquidationFee))))
                : pm.price
                    .mul(I80F48.fromU64(pm.baseLotSize))
                    .mul(pm.initBaseLiabWeight)
                    .sub(I80F48.fromU64(pm.baseLotSize)
                    .mul(pm.price)
                    .mul(ONE_I80F48()) // quoteInitLiabWeight
                    .mul(ONE_I80F48().add(pm.baseLiquidationFee)));
            const maxBaseLots = a.health
                .min(ZERO_I80F48())
                .abs()
                .div(unweightedHealthPerLot.abs())
                .min(I80F48.fromU64(baseLots).abs());
            return sum.add(maxBaseLots);
        }, ONE_I80F48());
        const notionalPositionUi = toUiDecimalsForQuote(baseLots.mul(I80F48.fromU64(pm.baseLotSize).mul(pm.price)));
        return {
            Market: { val: pm.name, highlight: false },
            Price: { val: pm['oldUiPrice'], highlight: false },
            'Future Price': { val: pm._uiPrice, highlight: false },
            'Notional Position': {
                val: Math.round(notionalPositionUi),
                highlight: Math.round(notionalPositionUi) > 5000,
            },
        };
    });
}
export async function getEquityForMangoAccounts(client, group, mangoAccountPks, allMangoAccounts) {
    const mangoAccounts = allMangoAccounts.filter((a) => mangoAccountPks.find((pk) => pk.equals(a.publicKey)));
    const accountsWithEquity = mangoAccounts.map((a) => {
        return {
            Account: { val: a.publicKey, highlight: false },
            Equity: {
                val: Math.round(toUiDecimalsForQuote(a.getEquity(group))),
                highlight: false,
            },
        };
    });
    accountsWithEquity.sort((a, b) => b.Equity.val - a.Equity.val);
    return accountsWithEquity;
}
export function findLargestAssetBatchUi(pisForLiqor, coin, startFromChange = 99, maxChange = 1, stepSize = 1) {
    let start = startFromChange;
    let largestBatchUi = 0;
    let largestBatchQuoteUi = 0;
    console.log(`___`);
    console.log(`${'start'.padStart(3)}: ${'liq$'.padStart(10)}, ${`prev`.padStart(3)}: ${'liq'.padStart(10)}, ${'largestBatchUi $'.padStart(15)}`);
    const liquidationBatches = [];
    while (start > 0) {
        const piForLiqor = pisForLiqor[start].filter((pi) => pi.Coin.val == coin)[0];
        // Compare entry to another entry, with max change difference
        const prev = Math.min(99, start + Math.round(start / maxChange));
        const prevPiForLiqor = pisForLiqor[prev].filter((pi) => pi.Coin.val == coin)[0];
        // Note: Assets.val is asset in $ amount that would need to be liquidated when price drops to a certain point
        const changeQuoteUi = piForLiqor.Assets.val - prevPiForLiqor.Assets.val;
        const changeUi = piForLiqor.Assets.val / piForLiqor['Future Price'].val -
            prevPiForLiqor.Assets.val / prevPiForLiqor['Future Price'].val;
        console.log(`${start.toString().padStart(3)}: ${piForLiqor.Assets.val
            .toLocaleString()
            .padStart(10)}, ${prev
            .toString()
            .padStart(3)}: ${prevPiForLiqor.Assets.val
            .toLocaleString()
            .padStart(10)}, ${largestBatchQuoteUi.toLocaleString().padStart(15)}`);
        liquidationBatches.push({
            start: start,
            startAssets: piForLiqor.Assets.val,
            prev: prev,
            prevAssets: prevPiForLiqor.Assets.val,
            largestBatchQuoteUi: largestBatchQuoteUi,
        });
        if (changeQuoteUi > largestBatchQuoteUi) {
            largestBatchUi = changeUi;
            largestBatchQuoteUi = changeQuoteUi;
        }
        start -= stepSize;
    }
    console.log(`___`);
    return [largestBatchQuoteUi, largestBatchUi, liquidationBatches];
}
export async function buildGroupGrid(group, allMangoAccounts, stepSize = 1) {
    const mangoAccountsSubset = allMangoAccounts.filter((a) => toUiDecimalsForQuote(a.getEquity(group)) > 100);
    {
        let pis;
        try {
            pis = await (await (await buildFetch())(`https://api.mngo.cloud/data/v4/risk/listed-tokens-one-week-price-impacts`, {
                mode: 'cors',
                headers: {
                    'Content-Type': 'application/json',
                    'Access-Control-Allow-Origin': '*',
                },
            })).json();
        }
        catch (error) {
            pis = [];
        }
        // Build groups where price has changed from 0 to -99% for each (non stable coin) assets simultaneously
        const groups = new Array(200);
        let change = stepSize;
        while (change < 101) {
            groups[change] = copy(group);
            const groupToModify = groups[change.toString()];
            const change_ = change / 100;
            Array.from(groupToModify.banksMapByTokenIndex.values())
                .flat()
                .filter((b) => !b.name.includes('USD'))
                .forEach((b) => {
                b['oldUiPrice'] = b._uiPrice;
                b._uiPrice = b._uiPrice * change_;
                b._price = b._price?.mul(I80F48.fromNumber(change_));
            });
            Array.from(groupToModify.perpMarketsMapByMarketIndex.values()).forEach((p) => {
                p['oldUiPrice'] = p._uiPrice;
                p._uiPrice = p._uiPrice * change_;
                p._price = p._price?.mul(I80F48.fromNumber(change_));
            });
            change += stepSize;
        }
        // Compute how much of an asset would need to be liquidated
        // when group (i.e. asset prices) reach a specific state
        return await Promise.all(groups.map((g) => getPriceImpactForLiqor(g, pis, mangoAccountsSubset)));
    }
}
export async function getLiquidationBatches(client, group) {
    const allMangoAccounts = await client.getAllMangoAccounts(group, true);
    const stepSize = 1;
    const pisForLiqor = await buildGroupGrid(group, allMangoAccounts, stepSize);
    const r = Array.from(group.banksMapByTokenIndex.values())
        .map((banks) => banks[0])
        .map((bank) => {
        return {
            token: bank.tokenIndex,
            liquidationPoints: findLargestAssetBatchUi(pisForLiqor, bank.name, Math.round(bank.maintAssetWeight.toNumber() * 100), 100 - Math.round(bank.maintAssetWeight.toNumber() * 100), stepSize)[2],
        };
    });
    return r;
}
export async function getRiskStats(client, group, change = 0.4) {
    let pis;
    try {
        pis = await (await (await buildFetch())(`https://api.mngo.cloud/data/v4/risk/listed-tokens-one-week-price-impacts`, {
            mode: 'cors',
            headers: {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*',
            },
        })).json();
    }
    catch (error) {
        pis = [];
    }
    // Get known liqors
    let liqors;
    try {
        liqors = (await (await (await buildFetch())(`https://api.mngo.cloud/data/v4/stats/liqors-over_period?over_period=1MONTH`, {
            mode: 'cors',
            headers: {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*',
            },
        })).json()).map((data) => new PublicKey(data['liqor']));
    }
    catch (error) {
        liqors = [new PublicKey('2T1taZuQwy7izxvjbAeiVjDhCEFYjWhLuv4U36XD1rL1')];
        liqors = [new PublicKey('Dr1wLHRKQSir4UgGphZ29ZcPhGvJrdDnrLTDgbz73bDs')];
        liqors = [new PublicKey('BNTDZJQrjNkjFxYAMCdKH2ShSM6Uwc28aAgit7ytVQJc')];
        liqors = [new PublicKey('BmAXMP5yUeagSsJa1PPMNyzRR5x2J4nJPrNrUGryCsnD')];
    }
    // Get known mms
    let mms;
    try {
        mms = (await (await (await buildFetch())(`https://api.mngo.cloud/data/v4/stats/perp-makers-over_period?over-period=1WEEK`, {
            mode: 'cors',
            headers: {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*',
            },
        })).json()).map((data) => new PublicKey(data['liqor']));
    }
    catch (error) {
        mms = [
            new PublicKey('BLgb4NFwhpurMrGX5LQfb8D8dBpGSGtBqqew2Em8uyRT'),
            new PublicKey('4hXPGTmR6dKNNqjLYdfDRSrTaa1Wt2GZoZnQ9hAJEeev'),
            new PublicKey('BGYWnqfaauCeebFQXEfYuDCktiVG8pqpprrsD4qfqL53'),
            new PublicKey('F1SZxEDxxCSLVjEBbMEjDYqajWRJQRCZBwPQnmcVvTLV'),
        ];
    }
    // Get all mango accounts
    const mangoAccounts = await client.getAllMangoAccounts(group, true);
    // Get on chain prices
    const mints = [
        ...new Set(Array.from(group.banksMapByTokenIndex.values())
            .flat()
            .map((bank) => bank.mint.toString())),
    ];
    // Note:
    // Disable for now
    // Getting rate limited
    // const prices = await getOnChainPriceForMints([
    //   ...new Set(
    //     Array.from(group.banksMapByTokenIndex.values())
    //       .flat()
    //       .map((bank) => bank.mint.toString()),
    //   ),
    // ]);
    // const onChainPrices = Object.fromEntries(
    //   prices.map((price, i) => [mints[i], price]),
    // );
    Array.from(group.banksMapByTokenIndex.values())
        .flat()
        .forEach((b) => {
        b['onChainPrice'] = b.uiPrice;
    });
    // Clone group, and simulate change % price drop for all assets except stables
    const drop = 1 - change;
    const groupDrop = copy(group);
    Array.from(groupDrop.banksMapByTokenIndex.values())
        .flat()
        .filter((b) => !b.name.includes('USD'))
        .forEach((b) => {
        b['oldUiPrice'] = b._uiPrice;
        b._uiPrice = b._uiPrice * drop;
        b._price = b._price?.mul(I80F48.fromNumber(drop));
    });
    Array.from(groupDrop.perpMarketsMapByMarketIndex.values()).forEach((p) => {
        p['oldUiPrice'] = p._uiPrice;
        p._uiPrice = p._uiPrice * drop;
        p._price = p._price?.mul(I80F48.fromNumber(drop));
    });
    // Clone group, and simulate change % price drop for usdc
    const groupUsdcDepeg = copy(group);
    Array.from(groupUsdcDepeg.banksMapByTokenIndex.values())
        .flat()
        .filter((b) => b.name.includes('USDC'))
        .forEach((b) => {
        b['oldUiPrice'] = b._uiPrice;
        b._uiPrice = b._uiPrice * drop;
        b._price = b._price?.mul(I80F48.fromNumber(drop));
    });
    // Clone group, and simulate change % price drop for usdt
    const groupUsdtDepeg = copy(group);
    Array.from(groupUsdtDepeg.banksMapByTokenIndex.values())
        .flat()
        .filter((b) => b.name.includes('USDT'))
        .forEach((b) => {
        b['oldUiPrice'] = b._uiPrice;
        b._uiPrice = b._uiPrice * drop;
        b._price = b._price?.mul(I80F48.fromNumber(drop));
    });
    // Clone group, and simulate change % price rally for all assets except stables
    const rally = 1 + change;
    const groupRally = copy(group);
    Array.from(groupRally.banksMapByTokenIndex.values())
        .flat()
        .filter((b) => !b.name.includes('USD'))
        .forEach((b) => {
        b['oldUiPrice'] = b._uiPrice;
        b._uiPrice = b._uiPrice * rally;
        b._price = b._price?.mul(I80F48.fromNumber(rally));
    });
    Array.from(groupRally.perpMarketsMapByMarketIndex.values()).forEach((p) => {
        p['oldUiPrice'] = p._uiPrice;
        p._uiPrice = p._uiPrice * rally;
        p._price = p._price?.mul(I80F48.fromNumber(rally));
    });
    const [assetDrop, assetRally, usdcDepeg, usdtDepeg, perpDrop, perpRally, liqorEquity, marketMakerEquity,] = await Promise.all([
        getPriceImpactForLiqor(groupDrop, pis, mangoAccounts),
        getPriceImpactForLiqor(groupRally, pis, mangoAccounts),
        getPriceImpactForLiqor(groupUsdcDepeg, pis, mangoAccounts),
        getPriceImpactForLiqor(groupUsdtDepeg, pis, mangoAccounts),
        getPerpPositionsToBeLiquidated(groupDrop, mangoAccounts),
        getPerpPositionsToBeLiquidated(groupRally, mangoAccounts),
        getEquityForMangoAccounts(client, group, liqors, mangoAccounts),
        getEquityForMangoAccounts(client, group, mms, mangoAccounts),
    ]);
    return {
        assetDrop: {
            title: `Table 1a: Liqors acquire liabs and assets. The assets and liabs are sum of max assets and max
    liabs for any token which would be liquidated to fix the health of a mango account.
    This would be the slippage they would face on buying-liabs/offloading-assets tokens acquired from unhealth accounts after a 40% drop to all non-stable oracles`,
            data: assetDrop,
        },
        assetRally: {
            title: `Table 1b: ... same as above but with a 40% rally to all non-stable oracles instead of drop`,
            data: assetRally,
        },
        usdcDepeg: {
            title: `Table 1c: ... same as above but with a 40% drop to only usdc oracle`,
            data: usdcDepeg,
        },
        usdtDepeg: {
            title: `Table 1d: ... same as above but with a 40% drop to only usdt oracle`,
            data: usdtDepeg,
        },
        perpDrop: {
            title: `Table 2a: Perp notional that liqor need to liquidate after a  40% drop`,
            data: perpDrop,
        },
        perpRally: {
            title: `Table 2b: Perp notional that liqor need to liquidate after a  40% rally`,
            data: perpRally,
        },
        liqorEquity: {
            title: `Table 3: Equity of known liqors from last month`,
            data: liqorEquity,
        },
        marketMakerEquity: {
            title: `Table 4: Equity of known makers from last month`,
            data: marketMakerEquity,
        },
    };
}
