import client, { Gauge, register } from "prom-client";
client.collectDefaultMetrics();

import express from "express";
import fetch from "node-fetch";

import Fuse from "./fuse.node.commonjs2.js";

const fuse = new Fuse(
  "https://eth-mainnet.alchemyapi.io/v2/dBZwIrYUWCOiGx_fbA-s1xp4gABRir5A"
);
const alcxStakingAccount = "0x5ea4a9a7592683bf0bc187d6da706c6c4770976f";
const alcxStakingContract = new fuse.web3.eth.Contract(
  require("../abis/StakingPools.json"),
  "0xab8e74017a8cc7c15ffccd726603790d26d7deca"
);

let twaps = new Gauge({
  name: "fuse_twaps",
  help: "Stores if Fuse TWAPs need updating. 0 indicates no, 1 indicates yes.",
  labelNames: ["ticker"] as const
});

let poolAssetsInterestRate = new Gauge({
  name: "fuse_pool_assets_interest_rate",
  help: "Stores the interest rates of each asset in each pool.",
  // Side: borrow, supply
  labelNames: ["id", "symbol", "side"] as const
});

let poolRSS = new Gauge({
  name: "fuse_pool_rss",
  help: "Stores the RSS score of each pool.",
  labelNames: ["id"] as const
});

let poolSuppliedAssetsAmount = new Gauge({
  name: "fuse_pool_assets_supply_amount",
  help: "Stores how much of each asset is supplied in each pool.",
  labelNames: ["id", "symbol"] as const
});

let poolBorrowedAssetsAmount = new Gauge({
  name: "fuse_pool_assets_borrow_amount",
  help: "Stores how much of each asset is borrowed in each pool.",
  labelNames: ["id", "symbol"] as const
});

let poolSuppliedAssetsUSD = new Gauge({
  name: "fuse_pool_assets_supply_usd",
  help: "Stores how much of each asset is supplied in each pool.",
  labelNames: ["id", "symbol"] as const
});

let poolBorrowedAssetsUSD = new Gauge({
  name: "fuse_pool_assets_borrow_usd",
  help: "Stores how much of each asset is borrowed in each pool.",
  labelNames: ["id", "symbol"] as const
});

let poolAssetLiquidations = new Gauge({
  name: "fuse_pool_assets_liquidations",
  help: "Stores how many liquidations have been performed on each asset in a each pool.",
  labelNames: ["id", "symbol"] as const
});

let poolAssetsReservesAmount = new Gauge({
  name: "fuse_pool_assets_reserves_amount",
  help: "Stores how much of each asset is in reserves in each pool.",
  labelNames: ["id", "symbol"] as const
});

let poolAssetsReservesUSD = new Gauge({
  name: "fuse_pool_assets_reserves_usd",
  help: "Stores how much of each asset is in reserves in each pool.",
  labelNames: ["id", "symbol"] as const
});

let poolAssetsFeesAmount = new Gauge({
  name: "fuse_pool_assets_fees_amount",
  help: "Stores how much of each asset has been taken as fees in each pool.",
  labelNames: ["id", "symbol"] as const
});

let poolAssetsFeesUSD = new Gauge({
  name: "fuse_pool_assets_fees_usd",
  help: "Stores how much of each asset has been taken as fees in each pool.",
  labelNames: ["id", "symbol"] as const
});

let stakedALCXUSD = new Gauge({
  name: "fuse_staked_alcx_usd",
  help: "Stores how much protocol controlled ALCX is currently being staked."
});

let stakedALCXUnclaimedUSD = new Gauge({
  name: "fuse_staked_alcx_unclaimed_usd",
  help: "Stores how much protocol controlled ALCX is claimable from staking."
});

let stakedALCXAmount = new Gauge({
  name: "fuse_staked_alcx_amount",
  help: "Stores how much protocol controlled ALCX is currently being staked."
});

let stakedALCXUnclaimedAmount = new Gauge({
  name: "fuse_staked_alcx_unclaimed_amount",
  help: "Stores how much protocol controlled ALCX is claimable from staking."
});

export interface FuseAsset {
  cToken: string;

  borrowBalance: number;
  supplyBalance: number;
  liquidity: number;

  membership: boolean;

  underlyingName: string;
  underlyingSymbol: string;
  underlyingToken: string;
  underlyingDecimals: number;
  underlyingPrice: number;

  collateralFactor: number;
  reserveFactor: number;

  adminFee: number;
  fuseFee: number;

  borrowRatePerBlock: number;
  supplyRatePerBlock: number;

  totalBorrow: number;
  totalSupply: number;
}

type Task =
  | "rss"
  | "liquidations"
  | "user_leverage"
  | "reserves_fees"
  | "staked_alcx"
  | "borrowers";

let lastRun: { [key in Task]: number } = {
  rss: 0,
  liquidations: 0,
  user_leverage: 0,
  borrowers: 0,
  reserves_fees: 0,
  staked_alcx: 0
};

function runEvery(key: Task, seconds: number, instantLastRunUpdate?: boolean) {
  const ms = seconds * 1000;

  const now = Date.now();

  const msPassed = Date.now() - lastRun[key];

  if (msPassed >= ms) {
    if (instantLastRunUpdate) {
      lastRun[key] = now;
    } else {
      setTimeout(() => {
        lastRun[key] = now;
      }, 10_000);
    }

    return true;
  }
}

async function eventLoop() {
  const [{ 0: ids, 1: fusePools }, ethPrice] = await Promise.all([
    fuse.contracts.FusePoolLens.methods
      .getPublicPoolsByVerificationWithData(true)
      .call({ gas: 1e18 }),
    fuse.web3.utils.fromWei(await fuse.getEthUsdPriceBN()) as number
  ]);

  for (let i = 0; i < ids.length; i++) {
    const id = ids[i];

    if (id == 4) {
      // Pool 4 is broken, we'll just skip it for now.
      continue;
    }

    ///////////////////// Assets /////////////////////

    fuse.contracts.FusePoolLens.methods
      .getPoolAssetsWithData(fusePools[i].comptroller)
      .call({
        from: "0x0000000000000000000000000000000000000000",
        gas: 1e18
      })
      .then((assets: FuseAsset[]) => {
        for (const asset of assets) {
          ////////////////// USD //////////////////

          const usdTVL =
            ((asset.totalSupply * asset.underlyingPrice) / 1e36) * ethPrice;

          const usdTVB =
            ((asset.totalBorrow * asset.underlyingPrice) / 1e36) * ethPrice;

          // If no one is lending the asset,
          // we don't need to fetch anything else.
          if (usdTVL == 0) {
            continue;
          }

          poolSuppliedAssetsUSD.set(
            { id, symbol: asset.underlyingSymbol },
            usdTVL
          );

          poolBorrowedAssetsUSD.set(
            { id, symbol: asset.underlyingSymbol },
            usdTVB
          );

          ////////////////// Amount //////////////////

          poolSuppliedAssetsAmount.set(
            { id, symbol: asset.underlyingSymbol },
            asset.totalSupply / 10 ** asset.underlyingDecimals
          );

          poolBorrowedAssetsAmount.set(
            { id, symbol: asset.underlyingSymbol },
            asset.totalBorrow / 10 ** asset.underlyingDecimals
          );

          // If no one is borrowing the asset,
          // we don't need to fetch anything else.
          if (usdTVB == 0) {
            continue;
          }

          ////////////// Interest Rates ///////////////

          const supplyAPY =
            (Math.pow(
              (asset.supplyRatePerBlock / 1e18) * (4 * 60 * 24) + 1,
              365
            ) -
              1) *
            100;

          const borrowAPY =
            (Math.pow(
              (asset.borrowRatePerBlock / 1e18) * (4 * 60 * 24) + 1,
              365
            ) -
              1) *
            100;

          poolAssetsInterestRate.set(
            { id, symbol: asset.underlyingSymbol, side: "supply" },
            supplyAPY
          );

          poolAssetsInterestRate.set(
            { id, symbol: asset.underlyingSymbol, side: "borrow" },
            borrowAPY
          );

          ////////////// Fees And Reserves ///////////////

          if (runEvery("reserves_fees", 600 /* 10 minutes */)) {
            const cToken = new fuse.web3.eth.Contract(
              JSON.parse(
                fuse.compoundContracts[
                  "contracts/CEtherDelegate.sol:CEtherDelegate"
                ].abi
              ),
              asset.cToken
            );

            cToken.methods
              .totalReserves()
              .call()
              .then(reserves => {
                poolAssetsReservesAmount.set(
                  { symbol: asset.underlyingSymbol, id },
                  reserves / 10 ** asset.underlyingDecimals
                );

                poolAssetsReservesUSD.set(
                  { symbol: asset.underlyingSymbol, id },
                  ((reserves * asset.underlyingPrice) / 1e36) * ethPrice
                );
              });

            cToken.methods
              .totalFuseFees()
              .call()
              .then(fuseFees => {
                poolAssetsFeesAmount.set(
                  { symbol: asset.underlyingSymbol, id },
                  fuseFees / 10 ** asset.underlyingDecimals
                );

                poolAssetsFeesUSD.set(
                  { symbol: asset.underlyingSymbol, id },
                  ((fuseFees * asset.underlyingPrice) / 1e36) * ethPrice
                );
              });
          }

          ///////////////// Liquidations /////////////////

          if (runEvery("liquidations", 600 /* 10 minutes */)) {
            const cToken = new fuse.web3.eth.Contract(
              JSON.parse(
                fuse.compoundContracts[
                  "contracts/CEtherDelegate.sol:CEtherDelegate"
                ].abi
              ),
              asset.cToken
            );

            cToken
              .getPastEvents("LiquidateBorrow", {
                fromBlock: 12060000,
                toBlock: "latest"
              })
              .then(events => {
                if (events.length != 0) {
                  poolAssetLiquidations.set(
                    { id, symbol: asset.underlyingSymbol },
                    events.length
                  );
                }
              });
          }

          //////////////// Staked ALCX ////////////////

          if (
            asset.underlyingToken.toLowerCase() ===
              "0xdbdb4d16eda451d0503b854cf79d55697f90c8df".toLowerCase() &&
            runEvery("staked_alcx", 600 /* 10 minutes */, true)
          ) {
            alcxStakingContract.methods
              .getStakeTotalDeposited(alcxStakingAccount, "1")
              .call()
              .then(staked => {
                stakedALCXUSD.set(
                  ((staked * asset.underlyingPrice) / 1e36) * ethPrice
                );
                stakedALCXAmount.set(staked / 1e18);
              });

            alcxStakingContract.methods
              .getStakeTotalUnclaimed(alcxStakingAccount, "1")
              .call()
              .then(unclaimed => {
                stakedALCXUnclaimedUSD.set(
                  ((unclaimed * asset.underlyingPrice) / 1e36) * ethPrice
                );
                stakedALCXUnclaimedAmount.set(unclaimed / 1e18);
              });
          }
        }
      });

    /////////////////////// RSS /////////////////////

    if (runEvery("rss", 600 /* 10 mins */)) {
      fetch(`https://app.rari.capital/api/rss?poolID=${id}`)
        .then(res => res.json())
        .then(data => {
          poolRSS.set({ id }, data.totalScore);
        });
    }

    /////////////////// TWAPS //////////////////

    fetch(`https://api.rari.capital/fuse/twaps`)
      .then(res => res.json())
      .then(data => {
        for (const twap of Object.values(data) as {
          ticker: string;
          workable: boolean;
        }[]) {
          twaps.set({ ticker: twap.ticker }, twap.workable ? 1 : 0);
        }
      });
  }
}

// Event loop (every 60 seconds)
setInterval(eventLoop, 60_000);

// Run instantly the first time.
eventLoop();

const app = express();
const port = 1336;

let lastRestart = Date.now();

app.get("/metrics", async (_, res) => {
  res.set("Content-Type", register.contentType);
  res.end(await register.metrics());
});

app.get("/ops", async (_, res) => {
  res.json({ lastRestart });
});

app.listen(port, () => {
  console.log(`Target server started at http://localhost:${port}/metrics`);
});