/**
 * This is a truffle script that syncs the on-chain contract with
 * the reference implementation and state in this repo. Please execute
 * this script using `deploy.sh` as described in `Deploying.md` file.
 *
 * This script is a statefull and fully automated. It will invoke the multisig
 * cli in the `../third_party/pyth/multisig-wh-message-builder`
 * to create governed instructions to change on-chain contracts.
 * As multisig instructions require multiple people approval, this script
 * will create some cache files to store the last step and continues from
 * the previous step in the next run.
 */

const governance = require("@pythnetwork/xc-governance-sdk");
const wormhole = require("@certusone/wormhole-sdk");
const assertVaaPayloadEquals = require("./assertVaaPayloadEquals");
const { assert } = require("chai");
const util = require("node:util");
const exec = util.promisify(require("node:child_process").exec);
const fs = require("fs");
const lodash = require("lodash");

const loadEnv = require("./loadEnv");
loadEnv("../");

const network = process.env.MIGRATIONS_NETWORK;
const chainName = process.env.WORMHOLE_CHAIN_NAME;
const cluster = process.env.CLUSTER;
const PythUpgradable = artifacts.require("PythUpgradable");

/**
 *
 * @param {string} cmd
 * @returns {Promise<string>} output of the multisig command
 */
async function execMultisigCommand(cmd) {
  const multisigCluster = cluster === "mainnet" ? "mainnet" : "devnet";
  const fullCmd = `npm start -- ${cmd} -c ${multisigCluster}`;
  console.log(`Executing "${fullCmd}"`);

  const { stdout, stderr } = await exec(fullCmd, {
    cwd: "../third_party/pyth/multisig-wh-message-builder",
  });

  console.log("stdout:");
  console.log(stdout);
  console.log("stderr");
  console.log(stderr);

  return stdout;
}

/**
 *
 * @param {string} payload Payload in hex string without leading 0x
 * @returns {Promise<string>}
 */
async function createMultisigTx(payload) {
  console.log("Creating a multisig transaction for this transaction");
  const stdout = await execMultisigCommand(`create -p ${payload}`);

  const txKey = stdout.match(/Tx key: (.*)\n/)[1];
  assert(txKey !== undefined && txKey.length > 10);
  console.log(`Created a multisig tx with key: ${txKey}`);

  return txKey;
}

/**
 *
 * @param {string} txKey
 * @param {string} payload
 * @returns {Promise<string>} VAA for the tx as hex (without leading 0x).
 */
async function executeMultisigTxAndGetVaa(txKey) {
  console.log("Executing a multisig transaction for this transaction");
  const stdout = await execMultisigCommand(`execute -t ${txKey}`);

  let /** @type {string} */ vaa;
  try {
    vaa = stdout.match(/VAA \(Hex\): (.*)\n/)[1];
    assert(vaa !== undefined && vaa.length > 10);
  } catch (err) {
    throw new Error("Couldn't find VAA from the logs.");
  }

  console.log(`Executed multisig tx and got VAA: ${vaa}`);

  return vaa;
}

/**
 *
 * @param {Buffer} payload
 */
function cleanUpVaaCache(payload) {
  fs.rmSync(`.${network}.ms_vaa_${payload.toString("hex")}`);
}

/**
 *
 * @param {Buffer} payload
 * @returns {Promise<string>} VAA for the tx as hex (without leading 0x).
 */
async function createVaaFromPayloadThroughMultiSig(payload) {
  const payloadHex = payload.toString("hex");

  const msVaaCachePath = `.${network}.ms_vaa_${payloadHex}`;
  let vaa;
  if (fs.existsSync(msVaaCachePath)) {
    vaa = fs.readFileSync(msVaaCachePath).toString().trim();
    console.log(`VAA already exists: ${vaa}`);
    return vaa;
  } else {
    const msTxCachePath = `.${network}.ms_tx_${payloadHex}`;

    let txKey;
    if (fs.existsSync(msTxCachePath)) {
      txKey = fs.readFileSync(msTxCachePath).toString();
    } else {
      console.log(
        `Creating multisig to send VAA with this payload: ${payloadHex} ...`
      );
      txKey = await createMultisigTx(payloadHex);
      fs.writeFileSync(msTxCachePath, txKey);
      throw new Error(
        "Contract not sync yet. Run the script again once the multisig transaction is ready to be executed."
      );
    }

    try {
      vaa = await executeMultisigTxAndGetVaa(txKey, payloadHex);
    } catch (e) {
      console.error(e);
      throw new Error(
        "Could not execute multisig tx. If the transaction is executed please get the VAA manually " +
          `and put it on .${network}.ms_vaa_${payloadHex}. Then execute the script again.`
      );
    }

    fs.writeFileSync(msVaaCachePath, vaa);
    fs.rmSync(`.${network}.ms_tx_${payloadHex}`);
  }

  return vaa;
}

/**
 * Create a VAA from Payload through multisig.
 *
 * @param {} proxy
 * @param {Buffer} payload
 * @param {boolean|undefined} keepVaaCache
 * @returns {Promise<void>}
 */
async function createAndExecuteVaaFromPayloadThroughMultiSig(
  proxy,
  payload,
  keepVaaCache
) {
  const vaa = await createVaaFromPayloadThroughMultiSig(payload);

  assertVaaPayloadEquals(vaa, payload);

  console.log(`Executing the VAA...`);
  await proxy.executeGovernanceInstruction("0x" + vaa);

  if (keepVaaCache !== true) {
    cleanUpVaaCache(payload);
  }
}

async function ensureWormholeAddrAndChainIdIsCorrect(proxy) {
  let desiredWormholeAddr;
  if (governance.RECEIVER_CHAINS[chainName] !== undefined) {
    const WormholeReceiver = artifacts.require("WormholeReceiver");
    desiredWormholeAddr = (await WormholeReceiver.deployed()).address;
  } else {
    desiredWormholeAddr =
      wormhole.CONTRACTS[cluster.toUpperCase()][chainName].core;
  }

  assert(desiredWormholeAddr !== undefined);

  const onchainWormholeAddr = await proxy.wormhole();
  assert(desiredWormholeAddr == onchainWormholeAddr);

  const desiredChainId = governance.CHAINS[chainName];
  const onchainChainId = await proxy.chainId();
  assert(desiredChainId == onchainChainId);

  console.log(
    `✅ Wormhole address and chain id is correct: ${desiredWormholeAddr} chainId: ${desiredChainId}`
  );
}

async function ensureThereIsNoOwner(proxy) {
  const onchainOwner = await proxy.owner();
  assert(onchainOwner == "0x0000000000000000000000000000000000000000");
  console.log("✅ There is no owner");
}

/**
 *
 * @param {} proxy
 * @param {string} desiredVersion
 */
async function upgradeContract(proxy, desiredVersion) {
  const implCachePath = `.${network}.new_impl`;
  let newImplementationAddress;
  if (fs.existsSync(implCachePath)) {
    newImplementationAddress = fs.readFileSync(implCachePath).toString();
    console.log(
      `A new implementation has already been deployed at address ${newImplementationAddress}`
    );
  } else {
    console.log("Deploying a new implementation...");
    const newImplementation = await PythUpgradable.new();
    console.log(`Tx hash:  ${newImplementation.transactionHash}`);
    console.log(`New implementation address: ${newImplementation.address}`);
    fs.writeFileSync(implCachePath, newImplementation.address);
    newImplementationAddress = newImplementation.address;
  }

  const upgradePayload = new governance.EthereumUpgradeContractInstruction(
    governance.CHAINS[chainName],
    new governance.HexString20Bytes(newImplementationAddress)
  ).serialize();

  const upgradePayloadHex = upgradePayload.toString("hex");

  await createAndExecuteVaaFromPayloadThroughMultiSig(proxy, upgradePayload);

  fs.rmSync(implCachePath);
  cleanUpVaaCache(upgradePayloadHex);

  const newVersion = await proxy.version();
  assert(desiredVersion == newVersion, "New contract version is not a match");

  console.log(`✅ Upgraded the contract successfully.`);
}

async function syncContractCode(proxy) {
  const onchainVersion = await proxy.version();
  const { version: desiredVersion } = require("../package.json");

  if (onchainVersion === desiredVersion) {
    console.log(`✅ Contract version is up to date: ${desiredVersion}`);
  } else {
    console.log(
      `❌ On-chain contract is outdated. Deployed version: ${onchainVersion}, desired version: ${desiredVersion}. Upgrading...`
    );
    await upgradeContract(proxy, desiredVersion);
  }
}

async function syncUpdateFee(proxy) {
  const desiredUpdateFee = process.env.SINGLE_UPDATE_FEE_IN_WEI;
  const onchainUpdateFee = (await proxy.singleUpdateFeeInWei()).toString();

  if (onchainUpdateFee == desiredUpdateFee) {
    console.log(`✅ Contract update fee is in sync: ${desiredUpdateFee}`);
  } else {
    console.log(
      `❌ Update fee is not in sync. on-chain update fee: ${onchainUpdateFee}, ` +
        `desired update fee: ${desiredUpdateFee}. Updating...`
    );

    const setFeePayload = new governance.SetFeeInstruction(
      governance.CHAINS[chainName],
      BigInt(desiredUpdateFee),
      BigInt(0)
    ).serialize();

    await createAndExecuteVaaFromPayloadThroughMultiSig(proxy, setFeePayload);

    const newOnchainUpdateFee = (await proxy.singleUpdateFeeInWei()).toString();
    assert(newOnchainUpdateFee == desiredUpdateFee);

    console.log(`✅ Set the new update fee successfully.`);
  }
}

async function syncValidTimePeriod(proxy) {
  const desiredValidTimePeriod = process.env.VALID_TIME_PERIOD_SECONDS;
  const onchainValidTimePeriod = (
    await proxy.validTimePeriodSeconds()
  ).toString();

  if (onchainValidTimePeriod == desiredValidTimePeriod) {
    console.log(
      `✅ Contract valid time period is in sync: ${desiredValidTimePeriod}s`
    );
  } else {
    console.log(
      `❌ Valid time period is not in sync. on-chain valid time period: ${onchainValidTimePeriod}s, ` +
        `desired valid time period: ${desiredValidTimePeriod}s. Updating...`
    );

    const setValidPeriodPayload = new governance.SetValidPeriodInstruction(
      governance.CHAINS[chainName],
      BigInt(desiredValidTimePeriod)
    ).serialize();

    await createAndExecuteVaaFromPayloadThroughMultiSig(
      proxy,
      setValidPeriodPayload
    );

    const newOnchainValidTimePeriod = (
      await proxy.validTimePeriodSeconds()
    ).toString();
    assert(newOnchainValidTimePeriod == desiredValidTimePeriod);

    console.log(`✅ Set the new valid time period successfully.`);
  }
}

async function syncDataSources(proxy) {
  const desiredDataSources = new Set([
    [
      Number(process.env.SOLANA_CHAIN_ID).toString(),
      process.env.SOLANA_EMITTER,
    ],
    [
      Number(process.env.PYTHNET_CHAIN_ID).toString(),
      process.env.PYTHNET_EMITTER,
    ],
  ]);

  const onchainDataSources = new Set(await proxy.validDataSources());

  if (lodash.isEqual(desiredDataSources, onchainDataSources)) {
    console.log(
      `✅ Contract data sources are in sync:\n` +
        `${JSON.stringify([...desiredDataSources])}`
    );
  } else {
    console.log(
      `❌ Data sources are not in sync. on-chain data sources:\n` +
        `${JSON.stringify([...onchainDataSources])}\n` +
        `desired data sources:\n` +
        `${JSON.stringify([...desiredDataSources])}\n` +
        `Updating...`
    );

    // Usually this change is universal, so the Payload is generated for all
    // the chains.
    const setDataSourcesPayload = new governance.SetDataSourcesInstruction(
      governance.CHAINS[chainName],
      Array.from(desiredDataSources).map(
        (ds) =>
          new governance.DataSource(
            Number(ds[0]),
            new governance.HexString32Bytes(ds[1])
          )
      )
    ).serialize();
    await createAndExecuteVaaFromPayloadThroughMultiSig(
      proxy,
      setDataSourcesPayload
    );

    const newOnchainDataSources = new Set(await proxy.validDataSources());
    assert(lodash.isEqual(desiredDataSources, newOnchainDataSources));

    console.log(`✅ Set the new data sources successfully.`);
  }
}

async function syncGovernanceDataSource(proxy) {
  const desiredGovDataSource = [
    Number(process.env.GOVERNANCE_CHAIN_ID).toString(),
    process.env.GOVERNANCE_EMITTER,
  ];

  const onchainGovDataSource = Array.from(await proxy.governanceDataSource());

  if (lodash.isEqual(desiredGovDataSource, onchainGovDataSource)) {
    console.log(
      `✅ Contract data sources are in sync:\n` + `${desiredGovDataSource}`
    );
  } else {
    console.log(
      `❌ Governance data source is not in sync. on-chain governance data source:\n` +
        `${onchainGovDataSource}\n` +
        `desired governance data source:\n` +
        `${desiredGovDataSource}\n` +
        `Cannot upgrade governance data source automatically. Please upgrade it manually`
    );
    throw new Error("Governance data source is not in sync.");
  }
}

module.exports = async function (callback) {
  try {
    const proxy = await PythUpgradable.deployed();
    console.log(`Syncing Pyth contract deployed on ${proxy.address}...`);

    await ensureThereIsNoOwner(proxy);
    await ensureWormholeAddrAndChainIdIsCorrect(proxy);

    await syncContractCode(proxy);
    await syncUpdateFee(proxy);
    await syncValidTimePeriod(proxy);
    await syncDataSources(proxy);
    await syncGovernanceDataSource(proxy);

    callback();
  } catch (e) {
    callback(e);
  }
};
