/*
This file is part of web3.js.

web3.js is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

web3.js is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with web3.js.  If not, see <http://www.gnu.org/licenses/>.
*/
import {
	ETH_DATA_FORMAT,
	FormatType,
	DataFormat,
	EthExecutionAPI,
	TransactionWithSenderAPI,
	Web3BaseWalletAccount,
	HexString,
	TransactionReceipt,
	Transaction,
	TransactionCall,
	TransactionWithFromLocalWalletIndex,
	TransactionWithToLocalWalletIndex,
	TransactionWithFromAndToLocalWalletIndex,
	LogsInput,
	TransactionHash,
	ContractAbiWithSignature,
} from 'web3-types';
import { Web3Context, Web3EventEmitter, Web3PromiEvent } from 'web3-core';
import { isNullish } from 'web3-validator';
import {
	ContractExecutionError,
	InvalidResponseError,
	TransactionPollingTimeoutError,
	TransactionRevertedWithoutReasonError,
	TransactionRevertInstructionError,
	TransactionRevertWithCustomError,
} from 'web3-errors';
import { ethRpcMethods } from 'web3-rpc-methods';

import {
	SendSignedTransactionEvents,
	SendTransactionEvents,
	SendTransactionOptions,
} from '../types.js';
// eslint-disable-next-line import/no-cycle
import { getTransactionGasPricing } from './get_transaction_gas_pricing.js';
// eslint-disable-next-line import/no-cycle
import { trySendTransaction } from './try_send_transaction.js';
// eslint-disable-next-line import/no-cycle
import { watchTransactionForConfirmations } from './watch_transaction_for_confirmations.js';
import { ALL_EVENTS_ABI } from '../constants.js';
// eslint-disable-next-line import/no-cycle
import { getTransactionError } from './get_transaction_error.js';
// eslint-disable-next-line import/no-cycle
import { getRevertReason } from './get_revert_reason.js';
import { decodeEventABI } from './decoding.js';

export class SendTxHelper<
	ReturnFormat extends DataFormat,
	ResolveType = FormatType<TransactionReceipt, ReturnFormat>,
	TxType =
		| Transaction
		| TransactionWithFromLocalWalletIndex
		| TransactionWithToLocalWalletIndex
		| TransactionWithFromAndToLocalWalletIndex,
> {
	private readonly web3Context: Web3Context<EthExecutionAPI>;
	private readonly promiEvent: Web3PromiEvent<
		ResolveType,
		SendSignedTransactionEvents<ReturnFormat> | SendTransactionEvents<ReturnFormat>
	>;
	private readonly options: SendTransactionOptions<ResolveType> = {
		checkRevertBeforeSending: true,
	};
	private readonly returnFormat: ReturnFormat;
	public constructor({
		options,
		web3Context,
		promiEvent,
		returnFormat,
	}: {
		web3Context: Web3Context<EthExecutionAPI>;
		options: SendTransactionOptions<ResolveType>;
		promiEvent: Web3PromiEvent<
			ResolveType,
			SendSignedTransactionEvents<ReturnFormat> | SendTransactionEvents<ReturnFormat>
		>;
		returnFormat: ReturnFormat;
	}) {
		this.options = options;
		this.web3Context = web3Context;
		this.promiEvent = promiEvent;
		this.returnFormat = returnFormat;
	}

	public getReceiptWithEvents(data: TransactionReceipt): ResolveType {
		const result = { ...(data ?? {}) };
		if (this.options?.contractAbi && result.logs && result.logs.length > 0) {
			result.events = {};
			for (const log of result.logs) {
				const event = decodeEventABI(
					ALL_EVENTS_ABI,
					log as LogsInput,
					this.options?.contractAbi as ContractAbiWithSignature,
					this.returnFormat,
				);
				if (event.event) {
					result.events[event.event] = event;
				}
			}
		}

		return result as unknown as ResolveType;
	}

	public async checkRevertBeforeSending(tx: TransactionCall) {
		if (this.options.checkRevertBeforeSending !== false) {
			const reason = await getRevertReason(this.web3Context, tx, this.options.contractAbi);
			if (reason !== undefined) {
				throw await getTransactionError<ReturnFormat>(
					this.web3Context,
					tx,
					undefined,
					undefined,
					this.options.contractAbi,
					reason,
				);
			}
		}
	}

	public emitSending(tx: TxType | HexString) {
		if (this.promiEvent.listenerCount('sending') > 0) {
			this.promiEvent.emit('sending', tx);
		}
	}

	public async populateGasPrice({
		transactionFormatted,
		transaction,
	}: {
		transactionFormatted: TxType;
		transaction: TxType;
	}): Promise<TxType> {
		let result = transactionFormatted;
		if (
			!this.options?.ignoreGasPricing &&
			isNullish((transactionFormatted as Transaction).gasPrice) &&
			(isNullish((transaction as Transaction).maxPriorityFeePerGas) ||
				isNullish((transaction as Transaction).maxFeePerGas))
		) {
			result = {
				...transactionFormatted,
				// @TODO gasPrice, maxPriorityFeePerGas, maxFeePerGas
				// should not be included if undefined, but currently are
				...(await getTransactionGasPricing(
					transactionFormatted,
					this.web3Context,
					ETH_DATA_FORMAT,
				)),
			};
		}

		return result;
	}

	public async signAndSend({
		wallet,
		tx,
	}: {
		wallet: Web3BaseWalletAccount | undefined;
		tx: TxType;
	}) {
		if (wallet) {
			const signedTransaction = await wallet.signTransaction(tx);

			return trySendTransaction(
				this.web3Context,
				async (): Promise<string> =>
					ethRpcMethods.sendRawTransaction(
						this.web3Context.requestManager,
						signedTransaction.rawTransaction,
					),
				signedTransaction.transactionHash,
			);
		}
		return trySendTransaction(
			this.web3Context,
			async (): Promise<string> =>
				ethRpcMethods.sendTransaction(
					this.web3Context.requestManager,
					tx as Partial<TransactionWithSenderAPI>,
				),
		);
	}

	public emitSent(tx: TxType | HexString) {
		if (this.promiEvent.listenerCount('sent') > 0) {
			this.promiEvent.emit('sent', tx);
		}
	}
	public emitTransactionHash(hash: string & Uint8Array) {
		if (this.promiEvent.listenerCount('transactionHash') > 0) {
			this.promiEvent.emit('transactionHash', hash);
		}
	}

	public emitReceipt(receipt: ResolveType) {
		if (this.promiEvent.listenerCount('receipt') > 0) {
			(
				this.promiEvent as Web3EventEmitter<
					SendTransactionEvents<ReturnFormat> | SendSignedTransactionEvents<ReturnFormat>
				>
			).emit(
				'receipt',
				// @ts-expect-error unknown type fix
				receipt,
			);
		}
	}

	public async handleError({ error, tx }: { error: unknown; tx: TransactionCall }) {
		let _error = error;

		if (_error instanceof ContractExecutionError && this.web3Context.handleRevert) {
			_error = await getTransactionError(
				this.web3Context,
				tx,
				undefined,
				undefined,
				this.options?.contractAbi,
			);
		}

		if (
			(_error instanceof InvalidResponseError ||
				_error instanceof ContractExecutionError ||
				_error instanceof TransactionRevertWithCustomError ||
				_error instanceof TransactionRevertedWithoutReasonError ||
				_error instanceof TransactionRevertInstructionError ||
				_error instanceof TransactionPollingTimeoutError) &&
			this.promiEvent.listenerCount('error') > 0
		) {
			this.promiEvent.emit('error', _error);
		}

		return _error;
	}

	public emitConfirmation({
		receipt,
		transactionHash,
	}: {
		receipt: ResolveType;
		transactionHash: TransactionHash;
	}) {
		if (this.promiEvent.listenerCount('confirmation') > 0) {
			watchTransactionForConfirmations<
				ReturnFormat,
				SendSignedTransactionEvents<ReturnFormat> | SendTransactionEvents<ReturnFormat>,
				ResolveType
			>(
				this.web3Context,
				this.promiEvent,
				receipt as unknown as TransactionReceipt,
				transactionHash,
				this.returnFormat,
			);
		}
	}

	public async handleResolve({ receipt, tx }: { receipt: ResolveType; tx: TransactionCall }) {
		if (this.options?.transactionResolver) {
			return this.options?.transactionResolver(receipt as unknown as TransactionReceipt);
		}
		if ((receipt as unknown as TransactionReceipt).status === BigInt(0)) {
			const error = await getTransactionError<ReturnFormat>(
				this.web3Context,
				tx,
				// @ts-expect-error unknown type fix
				receipt,
				undefined,
				this.options?.contractAbi,
			);
			if (this.promiEvent.listenerCount('error') > 0) {
				this.promiEvent.emit('error', error);
			}

			throw error;
		} else {
			return receipt;
		}
	}
}
