import { HttpProvider, providers } from './lib/providers/index.js';
import type { Providers } from './lib/providers/index.js';
import utils from './utils/index.js';
import { BigNumber } from 'bignumber.js';
import EventEmitter from 'eventemitter3';
import semver from 'semver';

import { TransactionBuilder } from './lib/TransactionBuilder/TransactionBuilder.js';
import { Trx } from './lib/trx.js';
import { Contract } from './lib/contract/index.js';
import { Plugin } from './lib/plugin.js';
import { Event } from './lib/event.js';
import { keccak256 } from './utils/ethersUtils.js';
import { fromHex, fromPrivateKey, isAddress, toHex, toChecksumAddress, isChecksumAddress } from './utils/address.js';
import { HeadersType } from './types/Providers.js';
import { isString } from './utils/validations.js';
import { DefaultAddress, NodeProvider, TronWebOptions, IBigNumber } from './types/TronWeb.js';
import { ContractAbiInterface } from './types/ABI.js';
import { Address } from './types/Trx.js';

const DEFAULT_VERSION = '4.7.1';

const FEE_LIMIT = 150000000;

const version = '6.0.3';

function isValidOptions(options: unknown): options is TronWebOptions {
    return (
        !!options &&
        typeof options === 'object' &&
        (!!(options as TronWebOptions).fullNode || !!(options as TronWebOptions).fullHost)
    );
}

export class TronWeb extends EventEmitter {
    providers: Providers;
    BigNumber: typeof BigNumber;
    transactionBuilder: TransactionBuilder;
    trx: Trx;
    plugin: Plugin;
    event: Event;
    version: typeof TronWeb.version;
    static version = version;
    utils: typeof utils;

    defaultBlock: number | false | 'earliest' | 'latest';
    defaultPrivateKey: string | false;
    defaultAddress: DefaultAddress;
    fullnodeVersion: string;
    feeLimit: number;

    fullNode!: HttpProvider;
    solidityNode!: HttpProvider;
    eventServer?: HttpProvider;

    constructor(options: TronWebOptions);
    constructor(fullNode: NodeProvider, solidityNode: NodeProvider, eventServer?: NodeProvider, privateKey?: string);
    /* prettier-ignore */
    constructor(fullNode: NodeProvider, solidityNode: NodeProvider, eventServer: NodeProvider, privateKey?: string);
    constructor(
        options: TronWebOptions | NodeProvider,
        solidityNode: NodeProvider = '',
        eventServer?: NodeProvider,
        privateKey = ''
    ) {
        super();

        let fullNode;
        let headers: HeadersType | false = false;
        let eventHeaders: HeadersType | false = false;

        if (isValidOptions(options)) {
            fullNode = options.fullNode || options.fullHost;
            solidityNode = (options.solidityNode || options.fullHost)!;
            eventServer = (options.eventServer || options.fullHost)!;
            headers = options.headers || false;
            eventHeaders = options.eventHeaders || headers;
            privateKey = options.privateKey!;
        } else {
            fullNode = options;
        }
        if (utils.isString(fullNode)) fullNode = new providers.HttpProvider(fullNode);

        if (utils.isString(solidityNode)) solidityNode = new providers.HttpProvider(solidityNode);

        if (utils.isString(eventServer)) eventServer = new providers.HttpProvider(eventServer);

        this.event = new Event(this);
        this.transactionBuilder = new TransactionBuilder(this);
        this.trx = new Trx(this);
        this.plugin = new Plugin(this, {
            disablePlugins: isValidOptions(options) ? options.disablePlugins : false,
        });
        this.utils = utils;

        this.setFullNode(fullNode as HttpProvider);
        this.setSolidityNode(solidityNode as HttpProvider);
        this.setEventServer(eventServer!);

        this.providers = providers;
        this.BigNumber = BigNumber;

        this.defaultBlock = false;
        this.defaultPrivateKey = false;
        this.defaultAddress = {
            hex: false,
            base58: false,
        };

        this.version = TronWeb.version;
        this.sha3 = TronWeb.sha3;
        this.fromUtf8 = TronWeb.fromUtf8;
        this.address = TronWeb.address;
        this.toAscii = TronWeb.toAscii;
        this.toUtf8 = TronWeb.toUtf8;
        this.isAddress = TronWeb.isAddress;
        this.fromAscii = TronWeb.fromAscii;
        this.toHex = TronWeb.toHex;
        this.toBigNumber = TronWeb.toBigNumber;
        this.toDecimal = TronWeb.toDecimal;
        this.fromDecimal = TronWeb.fromDecimal;
        this.toSun = TronWeb.toSun;
        this.fromSun = TronWeb.fromSun;
        this.createAccount = TronWeb.createAccount;
        this.createRandom = TronWeb.createRandom;
        this.fromMnemonic = TronWeb.fromMnemonic;

        if (privateKey) this.setPrivateKey(privateKey);
        this.fullnodeVersion = DEFAULT_VERSION;
        this.feeLimit = FEE_LIMIT;

        if (headers) {
            this.setFullNodeHeader(headers);
        }

        if (eventHeaders) {
            this.setEventHeader(eventHeaders);
        }
    }

    async getFullnodeVersion() {
        try {
            const nodeInfo = await this.trx.getNodeInfo();
            this.fullnodeVersion = nodeInfo.configNodeInfo.codeVersion;
            if (this.fullnodeVersion.split('.').length === 2) {
                this.fullnodeVersion += '.0';
            }
        } catch (err) {
            this.fullnodeVersion = DEFAULT_VERSION;
        }
    }

    setDefaultBlock(blockID: false | 'latest' | 'earliest' | number = false) {
        if ([false, 'latest', 'earliest', 0].includes(blockID)) {
            return (this.defaultBlock = blockID);
        }

        if (!utils.isInteger(blockID) || !blockID) throw new Error('Invalid block ID provided');

        return (this.defaultBlock = Math.abs(blockID));
    }

    setPrivateKey(privateKey: string) {
        try {
            this.setAddress(TronWeb.address.fromPrivateKey(privateKey) as string);
        } catch {
            throw new Error('Invalid private key provided');
        }

        this.defaultPrivateKey = privateKey;
        this.emit('privateKeyChanged', privateKey);
    }

    setAddress(address: string) {
        if (!TronWeb.isAddress(address)) throw new Error('Invalid address provided');

        const hex = TronWeb.address.toHex(address);
        const base58 = TronWeb.address.fromHex(address);

        if (this.defaultPrivateKey && TronWeb.address.fromPrivateKey(this.defaultPrivateKey) !== base58)
            this.defaultPrivateKey = false;

        this.defaultAddress = {
            hex,
            base58,
        };

        this.emit('addressChanged', { hex, base58 });
    }

    fullnodeSatisfies(version: string) {
        return semver.satisfies(this.fullnodeVersion, version);
    }

    isValidProvider(provider: unknown) {
        return Object.values(providers).some((knownProvider) => provider instanceof knownProvider);
    }

    setFullNode(fullNode: HttpProvider | string) {
        if (isString(fullNode)) fullNode = new providers.HttpProvider(fullNode);

        if (!this.isValidProvider(fullNode)) throw new Error('Invalid full node provided');

        this.fullNode = fullNode;
        this.fullNode.setStatusPage('wallet/getnowblock');
    }

    setSolidityNode(solidityNode: HttpProvider | string) {
        if (utils.isString(solidityNode)) solidityNode = new providers.HttpProvider(solidityNode);

        if (!this.isValidProvider(solidityNode)) throw new Error('Invalid solidity node provided');

        this.solidityNode = solidityNode;
        this.solidityNode.setStatusPage('walletsolidity/getnowblock');
    }

    setEventServer(eventServer: NodeProvider, healthcheck?: string) {
        this.event.setServer(eventServer, healthcheck);
    }

    setHeader(headers = {}) {
        const fullNode = new providers.HttpProvider(this.fullNode.host, 30000, '', '', headers);
        const solidityNode = new providers.HttpProvider(this.solidityNode.host, 30000, '', '', headers);
        const eventServer = new providers.HttpProvider(this.eventServer!.host, 30000, '', '', headers);

        this.setFullNode(fullNode);
        this.setSolidityNode(solidityNode);
        this.setEventServer(eventServer);
    }

    setFullNodeHeader(headers = {}) {
        const fullNode = new providers.HttpProvider(this.fullNode.host, 30000, '', '', headers);
        const solidityNode = new providers.HttpProvider(this.solidityNode.host, 30000, '', '', headers);

        this.setFullNode(fullNode);
        this.setSolidityNode(solidityNode);
    }

    setEventHeader(headers = {}) {
        const eventServer = new providers.HttpProvider(this.eventServer!.host, 30000, '', '', headers);
        this.setEventServer(eventServer);
    }

    currentProviders() {
        return {
            fullNode: this.fullNode,
            solidityNode: this.solidityNode,
            eventServer: this.eventServer,
        };
    }

    currentProvider() {
        return this.currentProviders();
    }

    getEventResult(...params: Parameters<Event['getEventsByContractAddress']>): ReturnType<Event['getEventsByContractAddress']> {
        return this.event.getEventsByContractAddress(...params);
    }

    getEventByTransactionID(
        ...params: Parameters<Event['getEventsByTransactionID']>
    ): ReturnType<Event['getEventsByTransactionID']> {
        return this.event.getEventsByTransactionID(...params);
    }

    contract(abi: ContractAbiInterface = [], address?: Address) {
        return new Contract(this, abi, address!);
    }

    address: typeof TronWeb.address;
    static get address() {
        return {
            fromHex(address: string) {
                return fromHex(address);
            },
            toHex(address: string) {
                return toHex(address);
            },
            toChecksumAddress(address: string) {
                return toChecksumAddress(address);
            },
            isChecksumAddress(address: string) {
                return isChecksumAddress(address);
            },
            fromPrivateKey(privateKey: string, strict = false) {
                return fromPrivateKey(privateKey, strict);
            },
        };
    }

    sha3: typeof TronWeb.sha3;
    static sha3(string: string, prefix = true) {
        return (prefix ? '0x' : '') + keccak256(Buffer.from(string, 'utf-8')).toString().substring(2);
    }

    toHex: typeof TronWeb.toHex;
    static toHex(val: string | number | boolean | Record<string | number | symbol, unknown> | unknown[] | IBigNumber) {
        if (utils.isBoolean(val)) return TronWeb.fromDecimal(+val);

        if (utils.isBigNumber(val)) return TronWeb.fromDecimal(val);

        if (typeof val === 'object') return TronWeb.fromUtf8(JSON.stringify(val));

        if (utils.isString(val)) {
            if (/^(-|)0x/.test(val)) return val;

            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-ignore
            if (!isFinite(val) || /^\s*$/.test(val)) return TronWeb.fromUtf8(val);
        }

        const result = TronWeb.fromDecimal(val as number);
        if (result === '0xNaN') {
            throw new Error('The passed value is not convertible to a hex string');
        } else {
            return result;
        }
    }

    toUtf8: typeof TronWeb.toUtf8;
    static toUtf8(hex: string) {
        if (utils.isHex(hex)) {
            hex = hex.replace(/^0x/, '');
            return Buffer.from(hex, 'hex').toString('utf8');
        } else {
            throw new Error('The passed value is not a valid hex string');
        }
    }

    fromUtf8: typeof TronWeb.fromUtf8;
    static fromUtf8(string: string) {
        if (!utils.isString(string)) {
            throw new Error('The passed value is not a valid utf-8 string');
        }
        return '0x' + Buffer.from(string, 'utf8').toString('hex');
    }

    toAscii: typeof TronWeb.toAscii;
    static toAscii(hex: string) {
        if (utils.isHex(hex)) {
            let str = '';
            let i = 0;
            const l = hex.length;
            if (hex.substring(0, 2) === '0x') {
                i = 2;
            }
            for (; i < l; i += 2) {
                const code = parseInt(hex.substr(i, 2), 16);
                str += String.fromCharCode(code);
            }
            return str;
        } else {
            throw new Error('The passed value is not a valid hex string');
        }
    }

    fromAscii: typeof TronWeb.fromAscii;
    static fromAscii(string: string, padding?: number) {
        if (!utils.isString(string)) {
            throw new Error('The passed value is not a valid utf-8 string');
        }
        return '0x' + Buffer.from(string, 'ascii').toString('hex').padEnd(padding!, '0');
    }

    toDecimal: typeof TronWeb.toDecimal;
    static toDecimal(value: string | number | IBigNumber) {
        return TronWeb.toBigNumber(value).toNumber();
    }

    fromDecimal: typeof TronWeb.fromDecimal;
    static fromDecimal(value: number | IBigNumber) {
        const number = TronWeb.toBigNumber(value);
        const result = number.toString(16);

        return number.isLessThan(0) ? '-0x' + result.substr(1) : '0x' + result;
    }

    fromSun: typeof TronWeb.fromSun;
    static fromSun(sun: number): string | IBigNumber {
        const trx = TronWeb.toBigNumber(sun).div(1_000_000);
        return utils.isBigNumber(sun) ? trx : trx.toString(10);
    }

    toSun: typeof TronWeb.toSun;
    static toSun(trx: number): string | IBigNumber {
        const sun = TronWeb.toBigNumber(trx).times(1_000_000);
        return utils.isBigNumber(trx) ? sun : sun.toString(10);
    }

    toBigNumber: typeof TronWeb.toBigNumber;
    static toBigNumber(amount: string | number | IBigNumber = 0): IBigNumber {
        if (utils.isBigNumber(amount)) return amount;

        if (utils.isString(amount) && /^(-|)0x/.test(amount)) return new BigNumber(amount.replace('0x', ''), 16);

        return new BigNumber(amount.toString(10), 10);
    }

    isAddress: typeof TronWeb.isAddress;
    static isAddress(address: unknown = ''): boolean {
        return isAddress(address);
    }

    createAccount: typeof TronWeb.createAccount;
    static async createAccount() {
        const account = utils.accounts.generateAccount();

        return account;
    }

    createRandom: typeof TronWeb.createRandom;
    static createRandom(
        ...params: Parameters<(typeof utils)['accounts']['generateRandom']>
    ): ReturnType<(typeof utils)['accounts']['generateRandom']> {
        const account = utils.accounts.generateRandom(...params);

        return account;
    }

    fromMnemonic: typeof TronWeb.fromMnemonic;
    static fromMnemonic(
        ...params: Parameters<(typeof utils)['accounts']['generateAccountWithMnemonic']>
    ): ReturnType<(typeof utils)['accounts']['generateAccountWithMnemonic']> {
        const account = utils.accounts.generateAccountWithMnemonic(...params);

        return account;
    }

    async isConnected() {
        return {
            fullNode: await this.fullNode.isConnected(),
            solidityNode: await this.solidityNode.isConnected(),
            eventServer: this.eventServer && (await this.eventServer.isConnected()),
        };
    }
}
export default TronWeb;
