/* External Imports */
import { injectL2Context } from '@eth-optimism/core-utils'
import { Logger, Metrics } from '@eth-optimism/common-ts'
import { exit } from 'process'
import { Signer, Wallet } from 'ethers'
import { JsonRpcProvider, TransactionReceipt } from '@ethersproject/providers'
import { config } from 'dotenv'
config()

/* Internal Imports */
import {
  TransactionBatchSubmitter,
  AutoFixBatchOptions,
  StateBatchSubmitter,
  STATE_BATCH_SUBMITTER_LOG_TAG,
  TX_BATCH_SUBMITTER_LOG_TAG,
} from '..'

const environment = process.env.NODE_ENV
const network = process.env.ETH_NETWORK_NAME
const release = `batch-submitter@${process.env.npm_package_version}`

/* Logger */
const name = 'oe:batch_submitter:init'
let logger

if (network) {
  // Initialize Sentry for Batch Submitter deployed to a network
  logger = new Logger({
    name,
    sentryOptions: {
      release,
      dsn: process.env.SENTRY_DSN,
      tracesSampleRate: parseInt(process.env.SENTRY_TRACE_RATE, 10) || 0.05,
      environment: network, // separate our Sentry errors by network instead of node environment
    },
  })
} else {
  // Skip initializing Sentry
  logger = new Logger({ name })
}

/* Metrics */
const metrics = new Metrics({
  prefix: name,
  labels: { environment, release, network },
})

interface RequiredEnvVars {
  // The HTTP provider URL for L1.
  L1_NODE_WEB3_URL: 'L1_NODE_WEB3_URL'
  // The HTTP provider URL for L2.
  L2_NODE_WEB3_URL: 'L2_NODE_WEB3_URL'
  // The layer one address manager address
  ADDRESS_MANAGER_ADDRESS: 'ADDRESS_MANAGER_ADDRESS'
  // The minimum size in bytes of any L1 transactions generated by the batch submitter.
  MIN_L1_TX_SIZE: 'MIN_L1_TX_SIZE'
  // The maximum size in bytes of any L1 transactions generated by the batch submitter.
  MAX_L1_TX_SIZE: 'MAX_L1_TX_SIZE'
  // The maximum number of L2 transactions that can ever be in a batch.
  MAX_TX_BATCH_COUNT: 'MAX_TX_BATCH_COUNT'
  // The maximum number of L2 state roots that can ever be in a batch.
  MAX_STATE_BATCH_COUNT: 'MAX_STATE_BATCH_COUNT'
  // The maximum amount of time (seconds) that we will wait before submitting an under-sized batch.
  MAX_BATCH_SUBMISSION_TIME: 'MAX_BATCH_SUBMISSION_TIME'
  // The delay in milliseconds between querying L2 for more transactions / to create a new batch.
  POLL_INTERVAL: 'POLL_INTERVAL'
  // The number of confirmations which we will wait after appending new batches.
  NUM_CONFIRMATIONS: 'NUM_CONFIRMATIONS'
  // The number of seconds to wait before resubmitting a transaction.
  RESUBMISSION_TIMEOUT: 'RESUBMISSION_TIMEOUT'
  // The number of confirmations that we should wait before submitting state roots for CTC elements.
  FINALITY_CONFIRMATIONS: 'FINALITY_CONFIRMATIONS'
  // Whether or not to run the tx batch submitter.
  RUN_TX_BATCH_SUBMITTER: 'true' | 'false' | 'RUN_TX_BATCH_SUBMITTER'
  // Whether or not to run the state batch submitter.
  RUN_STATE_BATCH_SUBMITTER: 'true' | 'false' | 'RUN_STATE_BATCH_SUBMITTER'
  // The safe minimum amount of ether the batch submitter key should
  // hold before it starts to log errors.
  SAFE_MINIMUM_ETHER_BALANCE: 'SAFE_MINIMUM_ETHER_BALANCE'
  // A boolean to clear the pending transactions in the mempool
  // on start up.
  CLEAR_PENDING_TXS: 'true' | 'false' | 'CLEAR_PENDING_TXS'
}
const requiredEnvVars: RequiredEnvVars = {
  L1_NODE_WEB3_URL: 'L1_NODE_WEB3_URL',
  L2_NODE_WEB3_URL: 'L2_NODE_WEB3_URL',
  ADDRESS_MANAGER_ADDRESS: 'ADDRESS_MANAGER_ADDRESS',
  MIN_L1_TX_SIZE: 'MIN_L1_TX_SIZE',
  MAX_L1_TX_SIZE: 'MAX_L1_TX_SIZE',
  MAX_TX_BATCH_COUNT: 'MAX_TX_BATCH_COUNT',
  MAX_STATE_BATCH_COUNT: 'MAX_STATE_BATCH_COUNT',
  MAX_BATCH_SUBMISSION_TIME: 'MAX_BATCH_SUBMISSION_TIME',
  POLL_INTERVAL: 'POLL_INTERVAL',
  NUM_CONFIRMATIONS: 'NUM_CONFIRMATIONS',
  RESUBMISSION_TIMEOUT: 'RESUBMISSION_TIMEOUT',
  FINALITY_CONFIRMATIONS: 'FINALITY_CONFIRMATIONS',
  RUN_TX_BATCH_SUBMITTER: 'RUN_TX_BATCH_SUBMITTER',
  RUN_STATE_BATCH_SUBMITTER: 'RUN_STATE_BATCH_SUBMITTER',
  SAFE_MINIMUM_ETHER_BALANCE: 'SAFE_MINIMUM_ETHER_BALANCE',
  CLEAR_PENDING_TXS: 'CLEAR_PENDING_TXS',
}

/* Optional Env Vars
 * FRAUD_SUBMISSION_ADDRESS
 * DISABLE_QUEUE_BATCH_APPEND
 * SEQUENCER_PRIVATE_KEY
 * PROPOSER_PRIVATE_KEY
 * MNEMONIC
 * SEQUENCER_MNEMONIC
 * PROPOSER_MNEMONIC
 * SEQUENCER_HD_PATH
 * PROPOSER_HD_PATH
 */
const env = process.env
const FRAUD_SUBMISSION_ADDRESS = env.FRAUD_SUBMISSION_ADDRESS || 'no fraud'
const DISABLE_QUEUE_BATCH_APPEND = !!env.DISABLE_QUEUE_BATCH_APPEND
const MIN_GAS_PRICE_IN_GWEI = parseInt(env.MIN_GAS_PRICE_IN_GWEI, 10) || 0
const MAX_GAS_PRICE_IN_GWEI = parseInt(env.MAX_GAS_PRICE_IN_GWEI, 10) || 70
const GAS_RETRY_INCREMENT = parseInt(env.GAS_RETRY_INCREMENT, 10) || 5
const GAS_THRESHOLD_IN_GWEI = parseInt(env.GAS_THRESHOLD_IN_GWEI, 10) || 100

// Private keys & mnemonics
const SEQUENCER_PRIVATE_KEY = env.SEQUENCER_PRIVATE_KEY
const PROPOSER_PRIVATE_KEY =
  env.PROPOSER_PRIVATE_KEY || env.SEQUENCER_PRIVATE_KEY // Kept for backwards compatibility
const SEQUENCER_MNEMONIC = env.SEQUENCER_MNEMONIC || env.MNEMONIC
const PROPOSER_MNEMONIC = env.PROPOSER_MNEMONIC || env.MNEMONIC
const SEQUENCER_HD_PATH = env.SEQUENCER_HD_PATH || env.HD_PATH
const PROPOSER_HD_PATH = env.PROPOSER_HD_PATH || env.HD_PATH
// Auto fix batch options -- TODO: Remove this very hacky config
const AUTO_FIX_BATCH_OPTIONS_CONF = env.AUTO_FIX_BATCH_OPTIONS_CONF
const autoFixBatchOptions: AutoFixBatchOptions = {
  fixDoublePlayedDeposits: AUTO_FIX_BATCH_OPTIONS_CONF
    ? AUTO_FIX_BATCH_OPTIONS_CONF.includes('fixDoublePlayedDeposits')
    : false,
  fixMonotonicity: AUTO_FIX_BATCH_OPTIONS_CONF
    ? AUTO_FIX_BATCH_OPTIONS_CONF.includes('fixMonotonicity')
    : false,
  fixSkippedDeposits: AUTO_FIX_BATCH_OPTIONS_CONF
    ? AUTO_FIX_BATCH_OPTIONS_CONF.includes('fixSkippedDeposits')
    : false,
}

export const run = async () => {
  logger.info('Starting batch submitter...')

  for (const [i, val] of Object.entries(requiredEnvVars)) {
    if (!process.env[val]) {
      logger.warn('Missing environment variable', {
        varName: val,
      })
      exit(1)
    }
    requiredEnvVars[val] = process.env[val]
  }

  const clearPendingTxs = requiredEnvVars.CLEAR_PENDING_TXS === 'true'

  const l1Provider = new JsonRpcProvider(requiredEnvVars.L1_NODE_WEB3_URL)
  const l2Provider = injectL2Context(
    new JsonRpcProvider(requiredEnvVars.L2_NODE_WEB3_URL)
  )

  let sequencerSigner: Signer
  let proposerSigner: Signer
  if (SEQUENCER_PRIVATE_KEY) {
    sequencerSigner = new Wallet(SEQUENCER_PRIVATE_KEY, l1Provider)
  } else if (SEQUENCER_MNEMONIC) {
    sequencerSigner = Wallet.fromMnemonic(
      SEQUENCER_MNEMONIC,
      SEQUENCER_HD_PATH
    ).connect(l1Provider)
  } else {
    throw new Error(
      'Must pass one of SEQUENCER_PRIVATE_KEY, MNEMONIC, or SEQUENCER_MNEMONIC'
    )
  }
  if (PROPOSER_PRIVATE_KEY) {
    proposerSigner = new Wallet(PROPOSER_PRIVATE_KEY, l1Provider)
  } else if (PROPOSER_MNEMONIC) {
    proposerSigner = Wallet.fromMnemonic(
      PROPOSER_MNEMONIC,
      PROPOSER_HD_PATH
    ).connect(l1Provider)
  } else {
    throw new Error(
      'Must pass one of PROPOSER_PRIVATE_KEY, MNEMONIC, or PROPOSER_MNEMONIC'
    )
  }

  const sequencerAddress = await sequencerSigner.getAddress()
  const proposerAddress = await proposerSigner.getAddress()
  const address = await sequencerSigner.getAddress()
  logger.info('Configured batch submitter addresses', {
    sequencerAddress,
    proposerAddress,
    addressManagerAddress: requiredEnvVars.ADDRESS_MANAGER_ADDRESS,
  })

  // If the sequencer & proposer are the same, use a single wallet
  if (sequencerAddress === proposerAddress) {
    proposerSigner = sequencerSigner
  }

  const txBatchSubmitter = new TransactionBatchSubmitter(
    sequencerSigner,
    l2Provider,
    parseInt(requiredEnvVars.MIN_L1_TX_SIZE, 10),
    parseInt(requiredEnvVars.MAX_L1_TX_SIZE, 10),
    parseInt(requiredEnvVars.MAX_TX_BATCH_COUNT, 10),
    parseInt(requiredEnvVars.MAX_BATCH_SUBMISSION_TIME, 10) * 1_000,
    parseInt(requiredEnvVars.NUM_CONFIRMATIONS, 10),
    parseInt(requiredEnvVars.RESUBMISSION_TIMEOUT, 10) * 1_000,
    requiredEnvVars.ADDRESS_MANAGER_ADDRESS,
    parseFloat(requiredEnvVars.SAFE_MINIMUM_ETHER_BALANCE),
    MIN_GAS_PRICE_IN_GWEI,
    MAX_GAS_PRICE_IN_GWEI,
    GAS_RETRY_INCREMENT,
    GAS_THRESHOLD_IN_GWEI,
    logger.child({ name: TX_BATCH_SUBMITTER_LOG_TAG }),
    new Metrics({
      prefix: TX_BATCH_SUBMITTER_LOG_TAG,
      labels: { environment, release, network },
    }),
    DISABLE_QUEUE_BATCH_APPEND,
    autoFixBatchOptions
  )

  const stateBatchSubmitter = new StateBatchSubmitter(
    proposerSigner,
    l2Provider,
    parseInt(requiredEnvVars.MIN_L1_TX_SIZE, 10),
    parseInt(requiredEnvVars.MAX_L1_TX_SIZE, 10),
    parseInt(requiredEnvVars.MAX_STATE_BATCH_COUNT, 10),
    parseInt(requiredEnvVars.MAX_BATCH_SUBMISSION_TIME, 10) * 1_000,
    parseInt(requiredEnvVars.NUM_CONFIRMATIONS, 10),
    parseInt(requiredEnvVars.RESUBMISSION_TIMEOUT, 10) * 1_000,
    parseInt(requiredEnvVars.FINALITY_CONFIRMATIONS, 10),
    requiredEnvVars.ADDRESS_MANAGER_ADDRESS,
    parseFloat(requiredEnvVars.SAFE_MINIMUM_ETHER_BALANCE),
    MIN_GAS_PRICE_IN_GWEI,
    MAX_GAS_PRICE_IN_GWEI,
    GAS_RETRY_INCREMENT,
    GAS_THRESHOLD_IN_GWEI,
    logger.child({ name: STATE_BATCH_SUBMITTER_LOG_TAG }),
    new Metrics({
      prefix: STATE_BATCH_SUBMITTER_LOG_TAG,
      labels: { environment, release, network },
    }),
    FRAUD_SUBMISSION_ADDRESS
  )

  // Loops infinitely!
  const loop = async (
    func: () => Promise<TransactionReceipt>
  ): Promise<void> => {
    // Clear all pending transactions
    if (clearPendingTxs) {
      try {
        const pendingTxs = await sequencerSigner.getTransactionCount('pending')
        const latestTxs = await sequencerSigner.getTransactionCount('latest')
        if (pendingTxs > latestTxs) {
          logger.info(
            'Detected pending transactions. Clearing all transactions!'
          )
          for (let i = latestTxs; i < pendingTxs; i++) {
            const response = await sequencerSigner.sendTransaction({
              to: await sequencerSigner.getAddress(),
              value: 0,
              nonce: i,
            })
            logger.info('Submitted empty transaction', {
              nonce: i,
              txHash: response.hash,
              to: response.to,
              from: response.from,
            })
            logger.debug('empty transaction data', {
              data: response.data,
            })
            await sequencerSigner.provider.waitForTransaction(
              response.hash,
              parseInt(requiredEnvVars.NUM_CONFIRMATIONS, 10)
            )
          }
        }
      } catch (err) {
        logger.error('Cannot clear transactions', { err })
        process.exit(1)
      }
    }

    while (true) {
      try {
        await func()
      } catch (err) {
        logger.error('Error submitting batch', { err })
        logger.info('Retrying...')
      }
      // Sleep
      await new Promise((r) =>
        setTimeout(r, parseInt(requiredEnvVars.POLL_INTERVAL, 10))
      )
    }
  }

  // Run batch submitters in two seperate infinite loops!
  if (requiredEnvVars.RUN_TX_BATCH_SUBMITTER === 'true') {
    loop(() => txBatchSubmitter.submitNextBatch())
  }
  if (requiredEnvVars.RUN_STATE_BATCH_SUBMITTER === 'true') {
    loop(() => stateBatchSubmitter.submitNextBatch())
  }
}
