import { BigNumber } from "@ethersproject/bignumber"
import { Block, WebSocketProvider } from "@ethersproject/providers"
import { ethers, Wallet } from "ethers"
import { Log } from "./Log"
import { parseUnits } from "@ethersproject/units"
import { ServerProfile } from "./ServerProfile"
import { Service } from "typedi"
import { sleep } from "./util"
import { TransactionReceipt, TransactionResponse } from "@ethersproject/abstract-provider"
import Big from "big.js"

@Service()
export class EthService {
    provider!: WebSocketProvider
    static readonly log = Log.getLogger(EthService.name)

    constructor(readonly serverProfile: ServerProfile) {
        this.provider = this.initProvider()
    }

    initProvider(): WebSocketProvider {
        const provider = new WebSocketProvider(this.serverProfile.web3Endpoint)
        provider._websocket.on("close", async (code: any) => {
            await EthService.log.warn(
                JSON.stringify({
                    event: "ReconnectWebSocket",
                    params: { code },
                }),
            )
            provider._websocket.terminate()
            await sleep(3000) // wait before reconnect
            this.provider = this.initProvider() // reconnect and replace the original provider
        })
        return provider
    }

    privateKeyToWallet(privateKey: string): Wallet {
        return new ethers.Wallet(privateKey, this.provider)
    }

    createContract<T>(address: string, abi: ethers.ContractInterface, signer?: ethers.Signer): T {
        return (new ethers.Contract(address, abi, signer ? signer : this.provider) as unknown) as T
    }

    async getBlock(blockNumber: number): Promise<Block> {
        return await this.provider.getBlock(blockNumber)
    }

    async getSafeGasPrice(): Promise<BigNumber> {
        for (let i = 0; i < 3; i++) {
            const gasPrice = Big((await this.provider.getGasPrice()).toString())
            if (gasPrice.gt(Big(0))) {
                return parseUnits(
                    gasPrice
                        .mul(1.0001) // add 20% markup so the tx is more likely to pass
                        .toFixed(0),
                    0,
                )
            }
        }
        throw new Error("GasPrice is 0")
    }

    async getBalance(addr: string): Promise<Big> {
        const balance = await this.provider.getBalance(addr)
        return new Big(ethers.utils.formatEther(balance))
    }

    static async supervise(
        signer: Wallet,
        tx: TransactionResponse,
        timeout: number,
        retry = 3,
    ): Promise<TransactionReceipt> {
        return new Promise((resolve, reject) => {
            // Set timeout for sending cancellation tx at double the gas price
            const timeoutId = setTimeout(async () => {
                const cancelTx = await signer.sendTransaction({
                    to: signer.address,
                    value: 0,
                    gasPrice: tx.gasPrice.mul(2), // TODO Make configurable?
                    nonce: tx.nonce,
                })

                await EthService.log.warn(
                    JSON.stringify({
                        event: "txCancelling",
                        params: {
                            tx: tx.hash,
                            txGasPrice: tx.gasPrice.toString(),
                            cancelTx: cancelTx.hash,
                            cancelTxGasPrice: cancelTx.gasPrice.toString(),
                            nonce: cancelTx.nonce,
                        },
                    }),
                )

                // Yo dawg I heard you like cancelling tx so
                // we put a cancel in your cancel tx so you can supervise while you supervise
                if (retry > 0) {
                    await EthService.supervise(signer, cancelTx, timeout, retry - 1)
                } else {
                    await cancelTx.wait()
                }
                reject({
                    reason: "timeout",
                    tx: tx.hash,
                    cancelTx: cancelTx.hash,
                })
            }, timeout)

            // Otherwise, resolve normally if the original tx is confirmed
            tx.wait().then(result => {
                clearTimeout(timeoutId)
                resolve(result)
            })
        })
    }
}