import Web3 from "web3";
import PythInterfaceAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json";
import EntropyAbi from "@pythnetwork/entropy-sdk-solidity/abis/IEntropy.json";
import { PriceFeedContract, PrivateKey, Storable } from "../base";
import { Chain, EvmChain } from "../chains";
import { DataSource, EvmExecute } from "xc_admin_common";
import { WormholeContract } from "./wormhole";
import { TokenQty } from "../token";

// Just to make sure tx gas limit is enough
const EXTENDED_ENTROPY_ABI = [
  {
    inputs: [],
    name: "acceptOwnership",
    outputs: [],
    stateMutability: "nonpayable",
    type: "function",
  },
  {
    inputs: [],
    name: "acceptAdmin",
    outputs: [],
    stateMutability: "nonpayable",
    type: "function",
  },
  {
    inputs: [],
    name: "owner",
    outputs: [
      {
        internalType: "address",
        name: "",
        type: "address",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [],
    name: "pendingOwner",
    outputs: [
      {
        internalType: "address",
        name: "",
        type: "address",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [],
    name: "version",
    outputs: [
      {
        internalType: "string",
        name: "",
        type: "string",
      },
    ],
    stateMutability: "pure",
    type: "function",
  },
  ...EntropyAbi,
] as any; // eslint-disable-line  @typescript-eslint/no-explicit-any
const EXTENDED_PYTH_ABI = [
  {
    inputs: [],
    name: "wormhole",
    outputs: [
      {
        internalType: "contract IWormhole",
        name: "",
        type: "address",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [],
    name: "governanceDataSource",
    outputs: [
      {
        components: [
          {
            internalType: "uint16",
            name: "chainId",
            type: "uint16",
          },
          {
            internalType: "bytes32",
            name: "emitterAddress",
            type: "bytes32",
          },
        ],
        internalType: "struct PythInternalStructs.DataSource",
        name: "",
        type: "tuple",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [
      {
        internalType: "bytes",
        name: "encodedVM",
        type: "bytes",
      },
    ],
    name: "executeGovernanceInstruction",
    outputs: [],
    stateMutability: "nonpayable",
    type: "function",
  },
  {
    inputs: [],
    name: "version",
    outputs: [{ internalType: "string", name: "", type: "string" }],
    stateMutability: "pure",
    type: "function",
  },
  {
    inputs: [],
    name: "singleUpdateFeeInWei",
    outputs: [
      {
        internalType: "uint256",
        name: "",
        type: "uint256",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [],
    name: "validDataSources",
    outputs: [
      {
        components: [
          {
            internalType: "uint16",
            name: "chainId",
            type: "uint16",
          },
          {
            internalType: "bytes32",
            name: "emitterAddress",
            type: "bytes32",
          },
        ],
        internalType: "struct PythInternalStructs.DataSource[]",
        name: "",
        type: "tuple[]",
      },
    ],
    stateMutability: "view",
    type: "function",
    constant: true,
  },
  {
    inputs: [],
    name: "lastExecutedGovernanceSequence",
    outputs: [
      {
        internalType: "uint64",
        name: "",
        type: "uint64",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [
      {
        internalType: "bytes32",
        name: "id",
        type: "bytes32",
      },
    ],
    name: "priceFeedExists",
    outputs: [
      {
        internalType: "bool",
        name: "",
        type: "bool",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
  ...PythInterfaceAbi,
] as any; // eslint-disable-line  @typescript-eslint/no-explicit-any
const WORMHOLE_ABI = [
  {
    inputs: [],
    name: "getCurrentGuardianSetIndex",
    outputs: [
      {
        internalType: "uint32",
        name: "",
        type: "uint32",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [],
    name: "chainId",
    outputs: [
      {
        internalType: "uint16",
        name: "",
        type: "uint16",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [
      {
        internalType: "uint32",
        name: "index",
        type: "uint32",
      },
    ],
    name: "getGuardianSet",
    outputs: [
      {
        components: [
          {
            internalType: "address[]",
            name: "keys",
            type: "address[]",
          },
          {
            internalType: "uint32",
            name: "expirationTime",
            type: "uint32",
          },
        ],
        internalType: "struct Structs.GuardianSet",
        name: "",
        type: "tuple",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [
      {
        internalType: "bytes",
        name: "_vm",
        type: "bytes",
      },
    ],
    name: "submitNewGuardianSet",
    outputs: [],
    stateMutability: "nonpayable",
    type: "function",
  },
  {
    inputs: [],
    name: "messageFee",
    outputs: [
      {
        internalType: "uint256",
        name: "",
        type: "uint256",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
] as any; // eslint-disable-line  @typescript-eslint/no-explicit-any
const EXECUTOR_ABI = [
  {
    inputs: [
      {
        internalType: "bytes",
        name: "encodedVm",
        type: "bytes",
      },
    ],
    name: "execute",
    outputs: [
      {
        internalType: "bytes",
        name: "response",
        type: "bytes",
      },
    ],
    stateMutability: "nonpayable",
    type: "function",
  },
  {
    inputs: [],
    name: "getOwnerChainId",
    outputs: [
      {
        internalType: "uint64",
        name: "",
        type: "uint64",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [],
    name: "getOwnerEmitterAddress",
    outputs: [
      {
        internalType: "bytes32",
        name: "",
        type: "bytes32",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [],
    name: "getLastExecutedSequence",
    outputs: [
      {
        internalType: "uint64",
        name: "",
        type: "uint64",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [],
    name: "owner",
    outputs: [
      {
        internalType: "address",
        name: "",
        type: "address",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
] as any; // eslint-disable-line  @typescript-eslint/no-explicit-any
export class WormholeEvmContract extends WormholeContract {
  constructor(public chain: EvmChain, public address: string) {
    super();
  }
  getContract() {
    const web3 = new Web3(this.chain.getRpcUrl());
    return new web3.eth.Contract(WORMHOLE_ABI, this.address);
  }

  async getCurrentGuardianSetIndex(): Promise<number> {
    const wormholeContract = this.getContract();
    return Number(
      await wormholeContract.methods.getCurrentGuardianSetIndex().call()
    );
  }

  async getChainId(): Promise<number> {
    const wormholeContract = this.getContract();
    return Number(await wormholeContract.methods.chainId().call());
  }

  /**
   * Returns an array of guardian addresses used for VAA verification in this contract
   */
  async getGuardianSet(): Promise<string[]> {
    const wormholeContract = this.getContract();
    const currentIndex = await this.getCurrentGuardianSetIndex();
    const [currentSet] = await wormholeContract.methods
      .getGuardianSet(currentIndex)
      .call();
    return currentSet;
  }

  async upgradeGuardianSets(senderPrivateKey: PrivateKey, vaa: Buffer) {
    const web3 = new Web3(this.chain.getRpcUrl());
    const { address } = web3.eth.accounts.wallet.add(senderPrivateKey);
    const wormholeContract = new web3.eth.Contract(WORMHOLE_ABI, this.address);
    const transactionObject = wormholeContract.methods.submitNewGuardianSet(
      "0x" + vaa.toString("hex")
    );
    const result = await this.chain.estiamteAndSendTransaction(
      transactionObject,
      { from: address }
    );
    return { id: result.transactionHash, info: result };
  }
}

interface EntropyProviderInfo {
  feeInWei: string;
  accruedFeesInWei: string;
  originalCommitment: string;
  originalCommitmentSequenceNumber: string;
  commitmentMetadata: string;
  uri: string;
  endSequenceNumber: string;
  sequenceNumber: string;
  currentCommitment: string;
  currentCommitmentSequenceNumber: string;
}

export class EvmEntropyContract extends Storable {
  static type = "EvmEntropyContract";

  constructor(public chain: EvmChain, public address: string) {
    super();
  }

  getId(): string {
    return `${this.chain.getId()}_${this.address}`;
  }

  getChain(): EvmChain {
    return this.chain;
  }

  getType(): string {
    return EvmEntropyContract.type;
  }

  async getVersion(): Promise<string> {
    const contract = this.getContract();
    return contract.methods.version().call();
  }

  static fromJson(
    chain: Chain,
    parsed: { type: string; address: string }
  ): EvmEntropyContract {
    if (parsed.type !== EvmEntropyContract.type)
      throw new Error("Invalid type");
    if (!(chain instanceof EvmChain))
      throw new Error(`Wrong chain type ${chain}`);
    return new EvmEntropyContract(chain, parsed.address);
  }

  // Generate a payload for the given executor address and calldata.
  // `executor` and `calldata` should be hex strings.
  generateExecutorPayload(
    executor: string,
    callAddress: string,
    calldata: string
  ) {
    return new EvmExecute(
      this.chain.wormholeChainName,
      executor.replace("0x", ""),
      callAddress.replace("0x", ""),
      0n,
      Buffer.from(calldata.replace("0x", ""), "hex")
    ).encode();
  }

  // Generates a payload for the newAdmin to call acceptAdmin on the entropy contracts
  generateAcceptAdminPayload(newAdmin: string): Buffer {
    const contract = this.getContract();
    const data = contract.methods.acceptAdmin().encodeABI();
    return this.generateExecutorPayload(newAdmin, this.address, data);
  }

  // Generates a payload for newOwner to call acceptOwnership on the entropy contracts
  generateAcceptOwnershipPayload(newOwner: string): Buffer {
    const contract = this.getContract();
    const data = contract.methods.acceptOwnership().encodeABI();
    return this.generateExecutorPayload(newOwner, this.address, data);
  }

  // Generates a payload to upgrade the executor contract, the owner of entropy contracts
  async generateUpgradeExecutorContractsPayload(
    newImplementation: string
  ): Promise<Buffer> {
    // Executor contract is the owner of entropy contract
    const executorAddr = await this.getOwner();
    const web3 = new Web3(this.chain.getRpcUrl());
    const executor = new web3.eth.Contract(EXECUTOR_ABI, executorAddr);
    const data = executor.methods.upgradeTo(newImplementation).encodeABI();
    return this.generateExecutorPayload(executorAddr, executorAddr, data);
  }

  async getOwner(): Promise<string> {
    const contract = this.getContract();
    return contract.methods.owner().call();
  }

  async getExecutorContract(): Promise<EvmExecutorContract> {
    const owner = await this.getOwner();
    return new EvmExecutorContract(this.chain, owner);
  }

  async getPendingOwner(): Promise<string> {
    const contract = this.getContract();
    return contract.methods.pendingOwner().call();
  }

  toJson() {
    return {
      chain: this.chain.getId(),
      address: this.address,
      type: EvmEntropyContract.type,
    };
  }

  getContract() {
    const web3 = new Web3(this.chain.getRpcUrl());
    return new web3.eth.Contract(EXTENDED_ENTROPY_ABI, this.address);
  }

  async getDefaultProvider(): Promise<string> {
    const contract = this.getContract();
    return await contract.methods.getDefaultProvider().call();
  }

  async getProviderInfo(address: string): Promise<EntropyProviderInfo> {
    const contract = this.getContract();
    const info: EntropyProviderInfo = await contract.methods
      .getProviderInfo(address)
      .call();
    return {
      ...info,
      uri: Web3.utils.toAscii(info.uri),
    };
  }

  generateUserRandomNumber() {
    const web3 = new Web3(this.chain.getRpcUrl());
    return web3.utils.randomHex(32);
  }

  async requestRandomness(
    userRandomNumber: string,
    provider: string,
    senderPrivateKey: PrivateKey
  ) {
    const web3 = new Web3(this.chain.getRpcUrl());
    const userCommitment = web3.utils.keccak256(userRandomNumber);
    const contract = new web3.eth.Contract(EXTENDED_ENTROPY_ABI, this.address);
    const fee = await contract.methods.getFee(provider).call();
    const { address } = web3.eth.accounts.wallet.add(senderPrivateKey);
    const useBlockHash = false;
    const transactionObject = contract.methods.request(
      provider,
      userCommitment,
      useBlockHash
    );
    return this.chain.estiamteAndSendTransaction(transactionObject, {
      from: address,
      value: fee,
    });
  }

  async revealRandomness(
    userRevelation: string,
    providerRevelation: string,
    provider: string,
    sequenceNumber: string,
    senderPrivateKey: PrivateKey
  ) {
    const web3 = new Web3(this.chain.getRpcUrl());
    const contract = new web3.eth.Contract(EXTENDED_ENTROPY_ABI, this.address);
    const { address } = web3.eth.accounts.wallet.add(senderPrivateKey);
    const transactionObject = contract.methods.reveal(
      provider,
      sequenceNumber,
      userRevelation,
      providerRevelation
    );
    return this.chain.estiamteAndSendTransaction(transactionObject, {
      from: address,
    });
  }
}

export class EvmExecutorContract {
  constructor(public chain: EvmChain, public address: string) {}

  getId(): string {
    return `${this.chain.getId()}_${this.address}`;
  }

  async getWormholeContract(): Promise<WormholeEvmContract> {
    const web3 = new Web3(this.chain.getRpcUrl());
    //Unfortunately, there is no public method to get the wormhole address
    //Found 251 by using `forge build --extra-output storageLayout` and finding the slot for the wormhole variable.
    let address = await web3.eth.getStorageAt(this.address, 251);
    address = "0x" + address.slice(26);
    return new WormholeEvmContract(this.chain, address);
  }

  getContract() {
    const web3 = new Web3(this.chain.getRpcUrl());
    return new web3.eth.Contract(EXECUTOR_ABI, this.address);
  }

  async getLastExecutedGovernanceSequence() {
    return await this.getContract().methods.getLastExecutedSequence().call();
  }

  async getGovernanceDataSource(): Promise<DataSource> {
    const executorContract = this.getContract();
    const ownerEmitterAddress = await executorContract.methods
      .getOwnerEmitterAddress()
      .call();
    const ownerEmitterChainid = await executorContract.methods
      .getOwnerChainId()
      .call();
    return {
      emitterChain: Number(ownerEmitterChainid),
      emitterAddress: ownerEmitterAddress.replace("0x", ""),
    };
  }

  /**
   * Returns the owner of the executor contract, this should always be the contract address itself
   */
  async getOwner(): Promise<string> {
    const contract = this.getContract();
    return contract.methods.owner().call();
  }

  async executeGovernanceInstruction(
    senderPrivateKey: PrivateKey,
    vaa: Buffer
  ) {
    const web3 = new Web3(this.chain.getRpcUrl());
    const { address } = web3.eth.accounts.wallet.add(senderPrivateKey);
    const executorContract = new web3.eth.Contract(EXECUTOR_ABI, this.address);
    const transactionObject = executorContract.methods.execute(
      "0x" + vaa.toString("hex")
    );
    const result = await this.chain.estiamteAndSendTransaction(
      transactionObject,
      { from: address }
    );
    return { id: result.transactionHash, info: result };
  }
}

export class EvmPriceFeedContract extends PriceFeedContract {
  static type = "EvmPriceFeedContract";

  constructor(public chain: EvmChain, public address: string) {
    super();
  }

  static fromJson(
    chain: Chain,
    parsed: { type: string; address: string }
  ): EvmPriceFeedContract {
    if (parsed.type !== EvmPriceFeedContract.type)
      throw new Error("Invalid type");
    if (!(chain instanceof EvmChain))
      throw new Error(`Wrong chain type ${chain}`);
    return new EvmPriceFeedContract(chain, parsed.address);
  }

  getId(): string {
    return `${this.chain.getId()}_${this.address}`;
  }

  getType(): string {
    return EvmPriceFeedContract.type;
  }

  async getVersion(): Promise<string> {
    const pythContract = this.getContract();
    const result = await pythContract.methods.version().call();
    return result;
  }

  getContract() {
    const web3 = new Web3(this.chain.getRpcUrl());
    const pythContract = new web3.eth.Contract(EXTENDED_PYTH_ABI, this.address);
    return pythContract;
  }

  /**
   * Returns the bytecode of the contract in hex format
   */
  async getCode(): Promise<string> {
    // TODO: handle proxy contracts
    const web3 = new Web3(this.chain.getRpcUrl());
    return web3.eth.getCode(this.address);
  }

  async getImplementationAddress(): Promise<string> {
    const web3 = new Web3(this.chain.getRpcUrl());
    // bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) according to EIP-1967
    const storagePosition =
      "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";
    let address = await web3.eth.getStorageAt(this.address, storagePosition);
    address = "0x" + address.slice(26);
    return address;
  }

  /**
   * Returns the keccak256 digest of the contract bytecode after replacing any occurrences of the contract addr in
   * the bytecode with 0.The bytecode stores the deployment address as an immutable variable.
   * This behavior is inherited from OpenZeppelin's implementation of UUPSUpgradeable contract.
   * You can read more about verification with immutable variables here:
   * https://docs.sourcify.dev/docs/immutables/
   * This function can be used to verify that the contract code is the same on all chains and matches
   * with the deployedCode property generated by truffle builds
   */
  async getCodeDigestWithoutAddress(): Promise<string> {
    const code = await this.getCode();
    const strippedCode = code.replaceAll(
      this.address.toLowerCase().replace("0x", ""),
      "0000000000000000000000000000000000000000"
    );
    return Web3.utils.keccak256(strippedCode);
  }

  async getTotalFee(): Promise<TokenQty> {
    const web3 = new Web3(this.chain.getRpcUrl());
    const amount = BigInt(await web3.eth.getBalance(this.address));
    return {
      amount,
      denom: this.chain.getNativeToken(),
    };
  }

  async getLastExecutedGovernanceSequence() {
    const pythContract = await this.getContract();
    return Number(
      await pythContract.methods.lastExecutedGovernanceSequence().call()
    );
  }

  async getPriceFeed(feedId: string) {
    const pythContract = this.getContract();
    const feed = "0x" + feedId;
    const exists = await pythContract.methods.priceFeedExists(feed).call();
    if (!exists) {
      return undefined;
    }
    const [price, conf, expo, publishTime] = await pythContract.methods
      .getPriceUnsafe(feed)
      .call();

    const [emaPrice, emaConf, emaExpo, emaPublishTime] =
      await pythContract.methods.getEmaPriceUnsafe(feed).call();
    return {
      price: { price, conf, expo, publishTime },
      emaPrice: {
        price: emaPrice,
        conf: emaConf,
        expo: emaExpo,
        publishTime: emaPublishTime,
      },
    };
  }

  async getValidTimePeriod() {
    const pythContract = this.getContract();
    const result = await pythContract.methods.getValidTimePeriod().call();
    return Number(result);
  }

  /**
   * Returns the wormhole contract which is being used for VAA verification
   */
  async getWormholeContract(): Promise<WormholeEvmContract> {
    const pythContract = this.getContract();
    const address = await pythContract.methods.wormhole().call();
    return new WormholeEvmContract(this.chain, address);
  }

  async getBaseUpdateFee() {
    const pythContract = this.getContract();
    const result = await pythContract.methods.singleUpdateFeeInWei().call();
    return { amount: result };
  }

  async getDataSources(): Promise<DataSource[]> {
    const pythContract = this.getContract();
    const result = await pythContract.methods.validDataSources().call();
    return result.map(
      ({
        chainId,
        emitterAddress,
      }: {
        chainId: string;
        emitterAddress: string;
      }) => {
        return {
          emitterChain: Number(chainId),
          emitterAddress: emitterAddress.replace("0x", ""),
        };
      }
    );
  }

  async getGovernanceDataSource(): Promise<DataSource> {
    const pythContract = this.getContract();
    const [chainId, emitterAddress] = await pythContract.methods
      .governanceDataSource()
      .call();
    return {
      emitterChain: Number(chainId),
      emitterAddress: emitterAddress.replace("0x", ""),
    };
  }

  async executeUpdatePriceFeed(senderPrivateKey: PrivateKey, vaas: Buffer[]) {
    const web3 = new Web3(this.chain.getRpcUrl());
    const { address } = web3.eth.accounts.wallet.add(senderPrivateKey);
    const pythContract = new web3.eth.Contract(EXTENDED_PYTH_ABI, this.address);
    const priceFeedUpdateData = vaas.map((vaa) => "0x" + vaa.toString("hex"));
    const updateFee = await pythContract.methods
      .getUpdateFee(priceFeedUpdateData)
      .call();
    const transactionObject =
      pythContract.methods.updatePriceFeeds(priceFeedUpdateData);
    const result = await this.chain.estiamteAndSendTransaction(
      transactionObject,
      { from: address, value: updateFee }
    );
    return { id: result.transactionHash, info: result };
  }

  async executeGovernanceInstruction(
    senderPrivateKey: PrivateKey,
    vaa: Buffer
  ) {
    const web3 = new Web3(this.chain.getRpcUrl());
    const { address } = web3.eth.accounts.wallet.add(senderPrivateKey);
    const pythContract = new web3.eth.Contract(EXTENDED_PYTH_ABI, this.address);
    const transactionObject = pythContract.methods.executeGovernanceInstruction(
      "0x" + vaa.toString("hex")
    );
    const result = await this.chain.estiamteAndSendTransaction(
      transactionObject,
      { from: address }
    );
    return { id: result.transactionHash, info: result };
  }

  getChain(): EvmChain {
    return this.chain;
  }

  toJson() {
    return {
      chain: this.chain.getId(),
      address: this.address,
      type: EvmPriceFeedContract.type,
    };
  }
}
