const ethers = require('ethers');
const BigNumber = require('bignumber.js');
const uniq = require('lodash/uniq');
const fs = require('fs');

const MAX_ATTEMPTS = 5;

const CONFIG = [
  {
    env: 'mainnet',
    contract: '0x23d1bfe8fa50a167816fbd79d7932577c06011f4',
    provider: 'https://mainnet.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8',
    startBlock: 12834524,
  },
];

const vestingAbi = [
  {
    inputs: [
      {
        internalType: 'address',
        name: 'token_v1_address',
        type: 'address',
      },
      {
        internalType: 'address',
        name: 'token_v2_address',
        type: 'address',
      },
      {
        internalType: 'address[]',
        name: 'old_addresses',
        type: 'address[]',
      },
      {
        internalType: 'address[]',
        name: 'new_addresses',
        type: 'address[]',
      },
    ],
    stateMutability: 'nonpayable',
    type: 'constructor',
  },
  {
    anonymous: false,
    inputs: [
      {
        indexed: true,
        internalType: 'address',
        name: 'new_controller',
        type: 'address',
      },
    ],
    name: 'Controller_Set',
    type: 'event',
  },
  {
    anonymous: false,
    inputs: [
      {
        indexed: true,
        internalType: 'address',
        name: 'issuer',
        type: 'address',
      },
      {
        indexed: false,
        internalType: 'uint256',
        name: 'amount',
        type: 'uint256',
      },
    ],
    name: 'Issuer_Permitted',
    type: 'event',
  },
  {
    anonymous: false,
    inputs: [
      {
        indexed: true,
        internalType: 'address',
        name: 'issuer',
        type: 'address',
      },
    ],
    name: 'Issuer_Revoked',
    type: 'event',
  },
  {
    anonymous: false,
    inputs: [
      {
        indexed: true,
        internalType: 'address',
        name: 'user',
        type: 'address',
      },
      {
        indexed: false,
        internalType: 'uint256',
        name: 'amount',
        type: 'uint256',
      },
      {
        indexed: true,
        internalType: 'bytes32',
        name: 'vega_public_key',
        type: 'bytes32',
      },
    ],
    name: 'Stake_Deposited',
    type: 'event',
  },
  {
    anonymous: false,
    inputs: [
      {
        indexed: true,
        internalType: 'address',
        name: 'user',
        type: 'address',
      },
      {
        indexed: false,
        internalType: 'uint256',
        name: 'amount',
        type: 'uint256',
      },
      {
        indexed: true,
        internalType: 'bytes32',
        name: 'vega_public_key',
        type: 'bytes32',
      },
    ],
    name: 'Stake_Removed',
    type: 'event',
  },
  {
    anonymous: false,
    inputs: [
      {
        indexed: true,
        internalType: 'address',
        name: 'from',
        type: 'address',
      },
      {
        indexed: false,
        internalType: 'uint256',
        name: 'amount',
        type: 'uint256',
      },
      {
        indexed: true,
        internalType: 'address',
        name: 'to',
        type: 'address',
      },
      {
        indexed: true,
        internalType: 'bytes32',
        name: 'vega_public_key',
        type: 'bytes32',
      },
    ],
    name: 'Stake_Transferred',
    type: 'event',
  },
  {
    anonymous: false,
    inputs: [
      {
        indexed: true,
        internalType: 'address',
        name: 'user',
        type: 'address',
      },
      {
        indexed: true,
        internalType: 'uint8',
        name: 'tranche_id',
        type: 'uint8',
      },
      {
        indexed: false,
        internalType: 'uint256',
        name: 'amount',
        type: 'uint256',
      },
    ],
    name: 'Tranche_Balance_Added',
    type: 'event',
  },
  {
    anonymous: false,
    inputs: [
      {
        indexed: true,
        internalType: 'address',
        name: 'user',
        type: 'address',
      },
      {
        indexed: true,
        internalType: 'uint8',
        name: 'tranche_id',
        type: 'uint8',
      },
      {
        indexed: false,
        internalType: 'uint256',
        name: 'amount',
        type: 'uint256',
      },
    ],
    name: 'Tranche_Balance_Removed',
    type: 'event',
  },
  {
    anonymous: false,
    inputs: [
      {
        indexed: true,
        internalType: 'uint8',
        name: 'tranche_id',
        type: 'uint8',
      },
      {
        indexed: false,
        internalType: 'uint256',
        name: 'cliff_start',
        type: 'uint256',
      },
      {
        indexed: false,
        internalType: 'uint256',
        name: 'duration',
        type: 'uint256',
      },
    ],
    name: 'Tranche_Created',
    type: 'event',
  },
  {
    inputs: [],
    name: 'accuracy_scale',
    outputs: [
      {
        internalType: 'uint256',
        name: '',
        type: 'uint256',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'address',
        name: '',
        type: 'address',
      },
    ],
    name: 'address_migration',
    outputs: [
      {
        internalType: 'address',
        name: '',
        type: 'address',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'uint8',
        name: 'tranche_id',
        type: 'uint8',
      },
      {
        internalType: 'address',
        name: 'target',
        type: 'address',
      },
    ],
    name: 'assisted_withdraw_from_tranche',
    outputs: [],
    stateMutability: 'nonpayable',
    type: 'function',
  },
  {
    inputs: [],
    name: 'controller',
    outputs: [
      {
        internalType: 'address',
        name: '',
        type: 'address',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'uint256',
        name: 'cliff_start',
        type: 'uint256',
      },
      {
        internalType: 'uint256',
        name: 'duration',
        type: 'uint256',
      },
    ],
    name: 'create_tranche',
    outputs: [],
    stateMutability: 'nonpayable',
    type: 'function',
  },
  {
    inputs: [],
    name: 'default_tranche_id',
    outputs: [
      {
        internalType: 'uint8',
        name: '',
        type: 'uint8',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'address',
        name: 'user',
        type: 'address',
      },
      {
        internalType: 'uint8',
        name: 'tranche_id',
        type: 'uint8',
      },
    ],
    name: 'get_tranche_balance',
    outputs: [
      {
        internalType: 'uint256',
        name: '',
        type: 'uint256',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'address',
        name: 'user',
        type: 'address',
      },
      {
        internalType: 'uint8',
        name: 'tranche_id',
        type: 'uint8',
      },
    ],
    name: 'get_vested_for_tranche',
    outputs: [
      {
        internalType: 'uint256',
        name: '',
        type: 'uint256',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'address',
        name: 'user',
        type: 'address',
      },
      {
        internalType: 'uint8',
        name: 'tranche_id',
        type: 'uint8',
      },
      {
        internalType: 'uint256',
        name: 'amount',
        type: 'uint256',
      },
    ],
    name: 'issue_into_tranche',
    outputs: [],
    stateMutability: 'nonpayable',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'address',
        name: 'user',
        type: 'address',
      },
      {
        internalType: 'uint8',
        name: 'tranche_id',
        type: 'uint8',
      },
      {
        internalType: 'uint256',
        name: 'amount',
        type: 'uint256',
      },
    ],
    name: 'move_into_tranche',
    outputs: [],
    stateMutability: 'nonpayable',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'address',
        name: 'issuer',
        type: 'address',
      },
      {
        internalType: 'uint256',
        name: 'amount',
        type: 'uint256',
      },
    ],
    name: 'permit_issuer',
    outputs: [],
    stateMutability: 'nonpayable',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'address',
        name: '',
        type: 'address',
      },
    ],
    name: 'permitted_issuance',
    outputs: [
      {
        internalType: 'uint256',
        name: '',
        type: 'uint256',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'uint256',
        name: 'amount',
        type: 'uint256',
      },
      {
        internalType: 'bytes32',
        name: 'vega_public_key',
        type: 'bytes32',
      },
    ],
    name: 'remove_stake',
    outputs: [],
    stateMutability: 'nonpayable',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'address',
        name: 'issuer',
        type: 'address',
      },
    ],
    name: 'revoke_issuer',
    outputs: [],
    stateMutability: 'nonpayable',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'address',
        name: 'new_controller',
        type: 'address',
      },
    ],
    name: 'set_controller',
    outputs: [],
    stateMutability: 'nonpayable',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'address',
        name: 'target',
        type: 'address',
      },
      {
        internalType: 'bytes32',
        name: 'vega_public_key',
        type: 'bytes32',
      },
    ],
    name: 'stake_balance',
    outputs: [
      {
        internalType: 'uint256',
        name: '',
        type: 'uint256',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'uint256',
        name: 'amount',
        type: 'uint256',
      },
      {
        internalType: 'bytes32',
        name: 'vega_public_key',
        type: 'bytes32',
      },
    ],
    name: 'stake_tokens',
    outputs: [],
    stateMutability: 'nonpayable',
    type: 'function',
  },
  {
    inputs: [],
    name: 'staking_token',
    outputs: [
      {
        internalType: 'address',
        name: '',
        type: 'address',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [],
    name: 'total_locked',
    outputs: [
      {
        internalType: 'uint256',
        name: '',
        type: 'uint256',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [],
    name: 'total_staked',
    outputs: [
      {
        internalType: 'uint256',
        name: '',
        type: 'uint256',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [],
    name: 'tranche_count',
    outputs: [
      {
        internalType: 'uint8',
        name: '',
        type: 'uint8',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'uint8',
        name: '',
        type: 'uint8',
      },
    ],
    name: 'tranches',
    outputs: [
      {
        internalType: 'uint256',
        name: 'cliff_start',
        type: 'uint256',
      },
      {
        internalType: 'uint256',
        name: 'duration',
        type: 'uint256',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'address',
        name: '',
        type: 'address',
      },
    ],
    name: 'user_stats',
    outputs: [
      {
        internalType: 'uint256',
        name: 'total_in_all_tranches',
        type: 'uint256',
      },
      {
        internalType: 'uint256',
        name: 'lien',
        type: 'uint256',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'address',
        name: 'user',
        type: 'address',
      },
    ],
    name: 'user_total_all_tranches',
    outputs: [
      {
        internalType: 'uint256',
        name: '',
        type: 'uint256',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [],
    name: 'v1_address',
    outputs: [
      {
        internalType: 'address',
        name: '',
        type: 'address',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'address',
        name: '',
        type: 'address',
      },
    ],
    name: 'v1_migrated',
    outputs: [
      {
        internalType: 'bool',
        name: '',
        type: 'bool',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [],
    name: 'v2_address',
    outputs: [
      {
        internalType: 'address',
        name: '',
        type: 'address',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'uint8',
        name: 'tranche_id',
        type: 'uint8',
      },
    ],
    name: 'withdraw_from_tranche',
    outputs: [],
    stateMutability: 'nonpayable',
    type: 'function',
  },
];

function addDecimal(value, decimals) {
  return value.dividedBy(Math.pow(10, decimals)).decimalPlaces(decimals);
}

function createUserTransactions(events, decimals) {
  return events.map((event) => {
    return {
      amount: addDecimal(
        new BigNumber(event.args?.amount.toString()),
        decimals
      ),
      user: event.args?.user,
      tranche_id: event.args?.tranche_id,
      tx: event.transactionHash,
    };
  });
}

function getUsersInTranche(
  balanceAddedEvents,
  balanceRemovedEvents,
  addresses,
  decimals
) {
  return addresses.map((address) => {
    const userDeposits = balanceAddedEvents.filter(
      (event) => event.args?.user === address
    );
    const userWithdraws = balanceRemovedEvents.filter(
      (event) => event.args?.user === address
    );
    const deposits = createUserTransactions(userDeposits, decimals);
    const withdrawals = createUserTransactions(userWithdraws, decimals);
    const total_tokens = deposits.reduce(
      (pre, cur) => pre.plus(cur.amount),
      new BigNumber(0)
    );
    const withdrawn_tokens = withdrawals.reduce(
      (pre, cur) => pre.plus(cur.amount),
      new BigNumber(0)
    );
    const remaining_tokens = total_tokens.minus(withdrawn_tokens);

    return {
      address,
      deposits,
      withdrawals,
      total_tokens,
      withdrawn_tokens,
      remaining_tokens,
    };
  });
}

function sumFromEvents(events, decimals) {
  const amounts = events.map((e) =>
    addDecimal(new BigNumber(e.args?.amount.toString()), decimals)
  );
  // Start with a 0 so if there are none there is no NaN
  return BigNumber.sum.apply(null, [new BigNumber(0), ...amounts]);
}

function getLockedAmount(totalAdded, cliffStart, trancheDuration) {
  let amount = new BigNumber(0);
  const ts = Math.round(new Date().getTime() / 1000);
  const tranche_progress = (ts - cliffStart) / trancheDuration;

  if (tranche_progress < 0) {
    amount = totalAdded;
  } else if (tranche_progress < 1) {
    amount = totalAdded.times(1 - tranche_progress);
  }

  return amount;
}

function createTransactions(events, decimals) {
  return events.map((event) => {
    return {
      amount: addDecimal(
        new BigNumber(event.args?.amount.toString()),
        decimals
      ),
      user: event.args?.user,
      tx: event.transactionHash,
    };
  });
}

function getTranchesFromHistory(
  createEvents,
  addEvents,
  removeEvents,
  decimals
) {
  return createEvents.map((event) => {
    const tranche_id = event.args?.tranche_id;
    const balanceAddedEvents = addEvents.filter(
      (e) =>
        e.event === 'Tranche_Balance_Added' && e.args?.tranche_id === tranche_id
    );
    const balanceRemovedEvents = removeEvents.filter(
      (e) =>
        e.event === 'Tranche_Balance_Removed' &&
        e.args?.tranche_id === tranche_id
    );

    //get tranche start and end dates
    const tranche_duration = event.args?.duration;
    const cliff_start = event.args?.cliff_start;
    const tranche_start = new Date(cliff_start.mul(1000).toNumber());
    const tranche_end = new Date(
      cliff_start.add(tranche_duration).mul(1000).toNumber()
    );

    // get added and removed values
    const total_added = sumFromEvents(balanceAddedEvents, decimals);
    const total_removed = sumFromEvents(balanceRemovedEvents, decimals);
    // get locked amount
    const locked_amount = getLockedAmount(
      total_added,
      cliff_start,
      tranche_duration
    );

    // get all deposits and withdrawals
    const deposits = createTransactions(balanceAddedEvents, decimals);
    const withdrawals = createTransactions(balanceRemovedEvents, decimals);

    // get all users
    const uniqueAddresses = uniq(
      balanceAddedEvents.map((event) => event.args?.user)
    );
    const users = getUsersInTranche(
      balanceAddedEvents,
      balanceRemovedEvents,
      uniqueAddresses,
      decimals
    );

    return {
      tranche_id: parseInt(tranche_id),
      tranche_start,
      tranche_end,
      total_added,
      total_removed,
      locked_amount,
      deposits,
      withdrawals,
      users,
    };
  });
}

const getBatched = async (filter, contract, provider, startblock) => {
  const batchSize = 100000;
  const currentBlockNumber = await provider.getBlockNumber();
  const batches = Math.ceil(currentBlockNumber / batchSize);
  const startIndex = Math.floor(startblock / batchSize);

  console.log(
    `Processing batches of size ${batches} starting at batch ${startIndex}`
  );
  let res = [];
  for (let index = startIndex; index < batches; index++) {
    let events = [];
    let attempts = 1;
    // Sometimes the queries fail for timeout even on small batch sizes. Some basic retry logic.
    while (attempts < MAX_ATTEMPTS) {
      try {
        events = await contract.queryFilter(
          filter,
          index * batchSize,
          (index + 1) * batchSize - 1
        );
        break;
      } catch {
        console.log('retry batch', index);
        ++attempts;
        if (attempts >= MAX_ATTEMPTS) {
          throw new Error('Could not get in 4 attempts');
        }
      }
    }
    res = [...events, ...res];

    console.log(
      `Processed blocks ${index * batchSize} to ${
        (index + 1) * batchSize - 1
      } as part of batch ${index}`
    );
  }
  return res;
};

const run = async (c) => {
  const provider = new ethers.providers.JsonRpcProvider({
    url: c.provider,
    timeout: 1000 * 60 * 1000,
  });

  const contract = new ethers.Contract(c.contract, vestingAbi, provider);
  const [created, added, removed] = await Promise.all([
    getBatched(
      contract.filters.Tranche_Created(),
      contract,
      provider,
      c.startBlock
    ),
    getBatched(
      contract.filters.Tranche_Balance_Added(),
      contract,
      provider,
      c.startBlock
    ),
    getBatched(
      contract.filters.Tranche_Balance_Removed(),
      contract,
      provider,
      c.startBlock
    ),
  ]);
  fs.writeFileSync(
    `./apps/static/src/assets/${c.env}-tranches.json`,
    JSON.stringify(
      getTranchesFromHistory(created, added, removed, 18),
      null,
      2
    ),
    { encoding: 'UTF-8' }
  );
};

CONFIG.forEach((c) => run(c));
