import { NETWORK_CONTRACTS, NETWORK_RPCS_BY_CHAIN } from '../consts';
import { WHTransaction, VaasByBlock, WHTransferRedeemed } from '../databases/types';
import { makeBlockKey, makeVaaKey, makeWHTransaction } from '../databases/utils';
import { EventObjectsTypes, RawLogEvents } from './TerraExplorerWatcher';
import BaseWatcher from './BaseWatcher';
import { makeSerializedVAA } from './utils';

export class InjectiveExplorerWatcher extends BaseWatcher {
  // Arbitrarily large since the code here is capable of pulling all logs from all via indexer pagination
  override maximumBatchSize: number = 1_000_000;

  latestBlockTag: string;
  getBlockTag: string;
  hashTag: string;
  contractTag: string;
  rpc: string | undefined;
  latestBlockHeight: number;

  constructor() {
    super('injective');
    this.rpc = NETWORK_RPCS_BY_CHAIN[this.chain];
    if (!this.rpc) {
      throw new Error(`${this.chain} RPC is not defined!`);
    }
    this.latestBlockHeight = 0;
    this.latestBlockTag = 'api/explorer/v1/blocks'; // This returns a page of the latest blocks
    this.getBlockTag = 'api/explorer/v1/blocks/';
    this.hashTag = 'api/explorer/v1/txs/';
    this.contractTag = 'api/explorer/v1/contractTxs/';
  }

  override async getFinalizedBlockNumber(): Promise<number> {
    const result: ExplorerBlocks = (await this.http.get(`${this.rpc}/${this.latestBlockTag}`)).data;
    if (result && result.paging.total) {
      const blockHeight: number = result.paging.total;
      if (blockHeight !== this.latestBlockHeight) {
        this.latestBlockHeight = blockHeight;
        this.logger.debug('blockHeight = ' + blockHeight);
      }
      return blockHeight;
    }
    throw new Error(`Unable to parse result of ${this.latestBlockTag} on ${this.rpc}`);
  }

  // retrieve blocks for token bridge contract.
  // should be core, but the explorer doesn't support it yet
  // use "to": as the pagination key
  // compare block height ("block_number":) with what is passed in.
  override async getMessagesForBlocks(fromBlock: number, toBlock: number): Promise<VaasByBlock> {
    const coreAddress = NETWORK_CONTRACTS[this.chain].core;
    const address = NETWORK_CONTRACTS[this.chain].token_bridge;
    if (!address) {
      throw new Error(`Token Bridge contract not defined for ${this.chain}`);
    }
    this.logger.debug(`Token Bridge contract for ${this.chain} is ${address}`);
    const vaasByBlock: VaasByBlock = {};
    this.logger.debug(`fetching info for blocks ${fromBlock} to ${toBlock}`);

    const limit: number = 50;
    let done: boolean = false;
    let skip: number = 0;
    let lastBlockInserted: number = 0;
    while (!done) {
      // This URL gets the paginated list of transactions for the token bridge contract
      const url: string = `${this.rpc}/${this.contractTag}${address}?skip=${skip}&limit=${limit}`;
      // this.logger.debug(`Query string = ${url}`);
      const bulkTxnResult = (
        await this.http.get<ContractTxnResult>(url, {
          headers: {
            'User-Agent': 'Mozilla/5.0',
          },
        })
      ).data;
      if (!bulkTxnResult) {
        throw new Error('bad bulkTxnResult');
      }
      skip = bulkTxnResult.paging.to;
      const bulkTxns: ContractTxnData[] = bulkTxnResult.data;
      if (!bulkTxns) {
        throw new Error('No transactions');
      }
      for (let i: number = 0; i < bulkTxns.length; ++i) {
        // Walk the transactions
        const txn: ContractTxnData = bulkTxns[i];
        const height: number = txn.block_number;
        if (height >= fromBlock && height <= toBlock) {
          // We only care about the transactions in the given block range
          this.logger.debug(`Found one: ${fromBlock}, ${height}, ${toBlock}`);
          const blockKey = makeBlockKey(
            txn.block_number.toString(),
            new Date(txn.block_unix_timestamp).toISOString(),
          );
          vaasByBlock[blockKey] = [];
          lastBlockInserted = height;
          this.logger.debug(`lastBlockInserted = ${lastBlockInserted}`);
          let vaaKey: string = '';
          // Each txn has an array of raw_logs
          if (txn.logs) {
            const rawLogs: RawLogEvents[] = txn.logs;
            for (let j: number = 0; j < rawLogs.length; ++j) {
              const rawLog: RawLogEvents = rawLogs[j];
              const events: EventObjectsTypes[] = rawLog.events;
              if (!events) {
                this.logger.debug(
                  `No events in rawLog${j} for block ${height}, hash = ${txn.hash}`,
                );
                continue;
              }
              for (let k: number = 0; k < events.length; k++) {
                const event: EventObjectsTypes = events[k];
                if (event.type === 'wasm') {
                  if (event.attributes) {
                    const attrs = event.attributes;
                    let emitter: string = '';
                    let sequence: string = '';
                    let coreContract: boolean = false;
                    // only care about _contract_address, message.sender and message.sequence
                    const numAttrs = attrs.length;
                    for (let l = 0; l < numAttrs; l++) {
                      const key = attrs[l].key;
                      if (key === 'message.sender') {
                        emitter = attrs[l].value;
                      } else if (key === 'message.sequence') {
                        sequence = attrs[l].value;
                      } else if (key === '_contract_address' || key === 'contract_address') {
                        const addr = attrs[l].value;
                        if (addr === coreAddress) {
                          coreContract = true;
                        }
                      }
                    }
                    if (coreContract && emitter !== '' && sequence !== '') {
                      vaaKey = makeVaaKey(txn.hash, this.chain, emitter, sequence);
                      this.logger.debug('blockKey: ' + blockKey);
                      this.logger.debug('Making vaaKey: ' + vaaKey);
                      vaasByBlock[blockKey] = [...(vaasByBlock[blockKey] || []), vaaKey];
                    }
                  }
                }
              }
            }
          }
        }
        if (height < fromBlock) {
          this.logger.debug('Breaking out due to height < fromBlock');
          done = true;
          break;
        }
      }
      if (bulkTxns.length < limit) {
        this.logger.debug('Breaking out due to ran out of txns.');
        done = true;
      }
    }
    if (lastBlockInserted < toBlock) {
      // Need to create something for the last requested block because it will
      // become the new starting point for subsequent calls.
      this.logger.debug(`Adding filler for block ${toBlock}`);
      const blkUrl = `${this.rpc}/${this.getBlockTag}${toBlock}`;
      this.logger.debug(`Query string for block = ${blkUrl}`);
      const result = (await this.http.get<ExplorerBlock>(blkUrl)).data;
      if (!result) {
        throw new Error(`Unable to get block information for block ${toBlock}`);
      }
      const blockKey = makeBlockKey(
        result.data.height.toString(),
        new Date(result.data.timestamp).toISOString(),
      );
      vaasByBlock[blockKey] = [];
    }
    return vaasByBlock;
  }

  override async getWhTxs(fromBlock: number, toBlock: number): Promise<WHTransaction[]> {
    const whTxs: WHTransaction[] = [];
    const coreAddress = NETWORK_CONTRACTS[this.chain].core;
    const address = NETWORK_CONTRACTS[this.chain].token_bridge;

    if (!address) {
      throw new Error(`Token Bridge contract not defined for ${this.chain}`);
    }

    this.logger.debug(`Token Bridge contract for ${this.chain} is ${address}`);
    this.logger.debug(`fetching info for blocks ${fromBlock} to ${toBlock}`);

    const limit: number = 50;
    let done: boolean = false;
    let skip: number = 0;
    while (!done) {
      // This URL gets the paginated list of transactions for the token bridge contract
      const url: string = `${this.rpc}/${this.contractTag}${address}?skip=${skip}&limit=${limit}`;
      // this.logger.debug(`Query string = ${url}`);
      const bulkTxnResult = (
        await this.http.get<ContractTxnResult>(url, {
          headers: {
            'User-Agent': 'Mozilla/5.0',
          },
        })
      ).data;
      if (!bulkTxnResult) {
        throw new Error('bad bulkTxnResult');
      }
      skip = bulkTxnResult.paging.to;
      const bulkTxns: ContractTxnData[] = bulkTxnResult.data;
      if (!bulkTxns) {
        throw new Error('No transactions');
      }
      for (let i: number = 0; i < bulkTxns.length; ++i) {
        // Walk the transactions
        const txn: ContractTxnData = bulkTxns[i];
        const height: number = txn.block_number;
        if (height >= fromBlock && height <= toBlock) {
          // We only care about the transactions in the given block range
          this.logger.debug(`Found one: ${fromBlock}, ${height}, ${toBlock}`);
          const blockNumber = txn.block_number.toString();

          // Each txn has an array of raw_logs
          if (txn.logs) {
            const rawLogs: RawLogEvents[] = txn.logs;
            for (let j: number = 0; j < rawLogs.length; ++j) {
              const rawLog: RawLogEvents = rawLogs[j];
              const events: EventObjectsTypes[] = rawLog.events;
              if (!events) {
                this.logger.debug(
                  `No events in rawLog${j} for block ${height}, hash = ${txn.hash}`,
                );
                continue;
              }
              for (let k: number = 0; k < events.length; k++) {
                const event: EventObjectsTypes = events[k];
                if (event.type === 'wasm') {
                  if (event.attributes) {
                    const attrs = event.attributes;
                    let isCoreContract: boolean = false;
                    let emitter: string | null = null;
                    let sequence: number | null = null;
                    let nonce: number | null = null;
                    let payload: string | null = null;
                    let chainId: number | null = null;
                    let timestamp: Date | null = null;

                    // only care about _contract_address, message.sender and message.sequence
                    const numAttrs = attrs.length;
                    for (let l = 0; l < numAttrs; l++) {
                      const key = attrs[l].key.toLowerCase();
                      const value = attrs[l].value.toLowerCase();

                      if (key === '_contract_address' || key === 'contract_address') {
                        if (value === coreAddress) {
                          isCoreContract = true;
                        }
                      }

                      if (key === 'message.message') {
                        payload = value;
                      }

                      if (key === 'message.sender') {
                        emitter = value;
                      }

                      if (key === 'message.chain_id') {
                        chainId = Number(value);
                      }

                      if (key === 'message.nonce') {
                        nonce = Number(value);
                      }

                      if (key === 'message.sequence') {
                        sequence = Number(value);
                      }

                      if (key === 'message.block_time') {
                        timestamp = new Date(+value * 1000);
                      }
                    }
                    if (isCoreContract) {
                      this.logger.debug('blockNumber: ' + blockNumber);

                      // console.log({ attrs });
                      // console.log('------');

                      const txHash = txn.hash;
                      const vaaSerialized = await makeSerializedVAA({
                        timestamp: timestamp!,
                        nonce: nonce!,
                        emitterChain: chainId!,
                        emitterAddress: emitter!,
                        sequence: sequence!,
                        payloadAsHex: payload!,
                        consistencyLevel: 0, // https://docs.wormhole.com/wormhole/blockchain-environments/consistency
                      });
                      const unsignedVaaBuffer = Buffer.from(vaaSerialized, 'hex');

                      const whTx = await makeWHTransaction({
                        eventLog: {
                          emitterChain: chainId!,
                          emitterAddr: emitter!,
                          sequence: sequence!,
                          txHash,
                          blockNumber: blockNumber,
                          unsignedVaa: unsignedVaaBuffer,
                          sender: '', // sender is not coming from the event log
                          indexedAt: timestamp!,
                        },
                      });

                      whTxs.push(whTx);
                    }
                  }
                }
              }
            }
          }
        }
        if (height < fromBlock) {
          this.logger.debug('Breaking out due to height < fromBlock');
          done = true;
          break;
        }
      }
      if (bulkTxns.length < limit) {
        this.logger.debug('Breaking out due to ran out of txns.');
        done = true;
      }
    }

    return whTxs;
  }

  override async getRedeemedTxs(
    _fromBlock: number,
    _toBlock: number,
  ): Promise<WHTransferRedeemed[]> {
    return [];
  }
}

type ExplorerBlocks = {
  paging: { total: number; from: number; to: number };
  data: ExplorerBlocksData[];
};

type ExplorerBlock = {
  s: string;
  data: ExplorerBlocksData;
};

type ExplorerBlocksData = {
  height: number;
  proposer: string;
  moniker: string;
  block_hash: string;
  parent_hash: string;
  num_pre_commits: number;
  num_txs: number;
  timestamp: string;
};

type ContractTxnResult = {
  data: ContractTxnData[];
  paging: {
    from: number;
    to: number;
    total: number;
  };
};

type ContractTxnData = {
  block_number: number;
  block_timestamp: string;
  block_unix_timestamp: number;
  code: number;
  codespace: string;
  data: string;
  error_log: string;
  gas_fee: {
    amount: Coin[];
    gas_limit: number;
    granter: string;
    payer: string;
  };
  gas_used: number;
  gas_wanted: number;
  hash: string;
  id: string;
  info: string;
  logs?: RawLogEvents[];
  memo: string;
  // messages: [];
  // signatures: [];
  tx_number: number;
  tx_type: string;
};

type Coin = {
  denom: string;
  amount: string;
};
