import { TronWeb } from '../../tronweb.js';
import utils from '../../utils/index.js';
import { Method, AbiFragmentNoErrConstructor } from './method.js';
import { buildReadNamespace, buildWriteNamespace } from './readWrite.js';
import type { ContractReadNamespace, ContractWriteNamespace } from './readWrite.js';
import type { ContractAbiInterface, GetMethodsTypeFromAbi, GetOnMethodTypeFromAbi, AnyOnMethodType } from '../../types/ABI.js';
import type { Address } from '../../types/Trx.js';
import type { CreateSmartContractOptions } from '../../types/TransactionBuilder.js';

// When the ABI itself defines a function named `read`/`write`, the legacy flat
// call surface must stay reachable as `contract.read(...)`/`contract.write(...)`.
// Wrap the legacy call in a Proxy whose property access and enumeration are
// served entirely by the namespace, so both surfaces share the one getter.
function wrapReservedNamespace<T extends object>(namespace: T, legacyCall: unknown): T {
    if (typeof legacyCall !== 'function') return namespace;
    return new Proxy(legacyCall, {
        get: (_target, prop) => namespace[prop as keyof T],
        has: (_target, prop) => prop in namespace,
        ownKeys: () => Reflect.ownKeys(namespace),
        getOwnPropertyDescriptor: (_target, prop) => Object.getOwnPropertyDescriptor(namespace, prop),
    }) as T;
}

export class Contract<Abi extends ContractAbiInterface = ContractAbiInterface> {
    tronWeb: TronWeb;
    abi: Abi;
    address: false | string;
    eventListener: any;
    bytecode?: false | string;
    deployed?: boolean;
    lastBlock?: false | number;
    methods: GetOnMethodTypeFromAbi<Abi> & AnyOnMethodType;
    methodInstances: GetMethodsTypeFromAbi<Abi>;
    props: any[];
    private _readNamespace?: { abi: Abi; namespace: ContractReadNamespace<Abi> };
    private _writeNamespace?: { abi: Abi; namespace: ContractWriteNamespace<Abi> };
    [key: string | number | symbol]: any;

    constructor(tronWeb: TronWeb, abi: Abi = [] as any, address: Address) {
        if (!tronWeb || !(tronWeb instanceof TronWeb)) throw new Error('Expected instance of TronWeb');

        this.tronWeb = tronWeb;

        this.address = address;
        this.abi = abi;

        this.eventListener = false;
        this.bytecode = false;
        this.deployed = false;
        this.lastBlock = false;

        this.methods = Object.create(null) as GetOnMethodTypeFromAbi<Abi> & AnyOnMethodType;
        this.methodInstances = Object.create(null) as GetMethodsTypeFromAbi<Abi>;
        this.props = [];

        if (utils.address.isAddress(address)) {
            this.deployed = true;
        } else {
            this.address = false;
        }

        this.loadAbi(abi);
    }

    /**
     * Type-safe read namespace:
     * every `view`/`pure` ABI function is callable as
     * `contract.read.fn([args], { from, value })` and resolves to the decoded
     * result directly. Rebuilt lazily whenever the contract ABI is replaced.
     */
    get read(): ContractReadNamespace<Abi> {
        if (!this._readNamespace || this._readNamespace.abi !== this.abi) {
            this._readNamespace = {
                abi: this.abi,
                namespace: wrapReservedNamespace(
                    buildReadNamespace(this) as object,
                    this.methods['read']
                ) as ContractReadNamespace<Abi>,
            };
        }
        return this._readNamespace.namespace;
    }

    /**
     * Type-safe write namespace:
     * every state-changing ABI function is callable as
     * `contract.write.fn([args], { account, value, feeLimit, tokenId, tokenValue, permissionId })`,
     * signed with the instance's default private key, broadcast, and resolved to
     * the transaction ID. Rebuilt lazily whenever the contract ABI is replaced.
     */
    get write(): ContractWriteNamespace<Abi> {
        if (!this._writeNamespace || this._writeNamespace.abi !== this.abi) {
            this._writeNamespace = {
                abi: this.abi,
                namespace: wrapReservedNamespace(
                    buildWriteNamespace(this) as object,
                    this.methods['write']
                ) as ContractWriteNamespace<Abi>,
            };
        }
        return this._writeNamespace.namespace;
    }

    hasProperty(property: number | string | symbol) {
        // eslint-disable-next-line no-prototype-builtins
        return this.hasOwnProperty(property) || (this as any).__proto__.hasOwnProperty(property);
    }

    loadAbi(abi: Abi) {
        this.abi = abi;
        this.methods = Object.create(null) as GetOnMethodTypeFromAbi<Abi> & AnyOnMethodType;
        this.methodInstances = Object.create(null) as GetMethodsTypeFromAbi<Abi>;

        this.props.forEach((prop: string) => delete (this as any)[prop]);

        abi.forEach((func) => {
            // Don't build a method for constructor function. That's handled through contract create.
            // Don't build a method for error function.
            if (!func.type || ['constructor', 'error'].includes(func.type.toLowerCase())) return;
            const method = new Method<any>(this, func as AbiFragmentNoErrConstructor) as GetMethodsTypeFromAbi<Abi>[keyof GetMethodsTypeFromAbi<Abi>];
            const methodCall = method.onMethod.bind(method);

            const { name, functionSelector, signature } = method;
            const internalName = name as keyof GetMethodsTypeFromAbi<Abi>;
            const internalFunctionSelector = functionSelector as keyof GetMethodsTypeFromAbi<Abi>;
            const internalSignature = signature as keyof GetMethodsTypeFromAbi<Abi>;

            (this.methods as AnyOnMethodType)[name] = methodCall;
            (this.methods as AnyOnMethodType)[functionSelector!] = methodCall;
            (this.methods as AnyOnMethodType)[signature] = methodCall;

            this.methodInstances[internalName] = method;
            this.methodInstances[internalFunctionSelector!] = method;
            this.methodInstances[internalSignature] = method;

            if (!this.hasProperty(name)) {
                (this as any)[name] = methodCall;
                this.props.push(name);
            }

            if (!this.hasProperty(functionSelector!)) {
                (this as any)[functionSelector!] = methodCall;
                this.props.push(functionSelector);
            }

            if (!this.hasProperty(signature)) {
                (this as any)[signature] = methodCall;
                this.props.push(signature);
            }
        });
    }

    decodeInput(data: string) {
        const methodName = data.substring(0, 8);
        const inputData = data.substring(8);

        if (!this.methodInstances[methodName]) throw new Error('Contract method ' + methodName + ' not found');

        const methodInstance = this.methodInstances[methodName];

        return {
            name: methodInstance.name,
            params: this.methodInstances[methodName].decodeInput(inputData),
        };
    }

    async new(options: CreateSmartContractOptions, privateKey = this.tronWeb.defaultPrivateKey) {
        const address = this.tronWeb.address.fromPrivateKey(privateKey as string);
        const transaction = await this.tronWeb.transactionBuilder.createSmartContract(options, address as Address);
        const signedTransaction = await this.tronWeb.trx.sign(transaction, privateKey);
        const contract = await this.tronWeb.trx.sendRawTransaction(signedTransaction);

        if (contract.code) {
            throw {
                error: contract.code,
                message: this.tronWeb.toUtf8(contract.message),
            };
        }

        await utils.sleep(3000);
        const abi = signedTransaction.raw_data.contract[0].parameter.value.new_contract.abi.entrys || [];
        return this.tronWeb.contract(abi, signedTransaction.contract_address);
    }

    async at(contractAddress: Address) {
        try {
            const contract = await this.tronWeb.trx.getContract(contractAddress);

            if (!contract.contract_address) {
                throw new Error('Unknown error: ' + JSON.stringify(contract, null, 2));
            }

            this.address = contract.contract_address;
            this.bytecode = contract.bytecode;
            this.deployed = true;

            this.loadAbi(contract.abi ? (contract.abi.entrys ? contract.abi.entrys : []) : []);

            return this;
        } catch (ex: any) {
            if (ex.toString().includes('does not exist')) {
                throw new Error('Contract has not been deployed on the network');
            }

            throw new Error(ex);
        }
    }
}

export type { CallOptions, SendOptions, AbiFragmentNoErrConstructor } from './method.js';
export { Method } from './method.js';
export type {
    ContractReadNamespace,
    ContractWriteNamespace,
    ReadContractFunctionName,
    WriteContractFunctionName,
    ReadContractParameters,
    ReadContractReturnType,
    WriteContractParameters,
    WriteContractReturnType,
    ReadOptions,
    WriteOptions,
    CollapseSingleItemTuple,
} from './readWrite.js';
export { buildReadNamespace, buildWriteNamespace } from './readWrite.js';
