import {
  BaseContract,
  BaseContractMethod,
  ContractRunner,
  ContractTransaction,
  ContractTransactionReceipt,
  ContractTransactionResponse,
  defineProperties,
  FunctionFragment,
  Interface,
} from "ethers";

import { Adapter } from "./Adapter";
import { MinimalContract } from "../MinimalContract";

import { UNKNOWN_TRANSACTION_NAME } from "../../constants";

import { bytecodeToString, fillParameters, getMethodString } from "../../utils";

import { Instance } from "../../types/adapter";
import { OverridesAndLibs, OverridesAndName } from "../../types/deployer";
import { KeyTransactionFields, MigrationMetadata, TransactionFieldsToSave } from "../../types/tools";

import { Stats } from "../../tools/Stats";
import { Reporter } from "../../tools/reporters/Reporter";
import { networkManager } from "../../tools/network/NetworkManager";
import { TransactionRunner } from "../../tools/runners/TransactionRunner";
import { TransactionProcessor } from "../../tools/storage/TransactionProcessor";

export abstract class BaseAdapter extends Adapter {
  public getRawBytecode<A, I>(instance: Instance<A, I>): string {
    return bytecodeToString(instance.bytecode);
  }

  public async fromInstance<A, I>(instance: Instance<A, I>, parameters: OverridesAndName): Promise<MinimalContract> {
    return new MinimalContract(
      this._hre,
      this.getRawBytecode(instance),
      this.getInterface(instance),
      this.getContractName(instance, parameters),
    );
  }

  public async toInstance<A, I>(instance: Instance<A, I>, address: string, parameters: OverridesAndLibs): Promise<I> {
    const signer = await networkManager!.getSigner(parameters.from);
    const contractName = this.getContractName(instance, parameters);

    let contract = new BaseContract(address, this.getInterface(instance), signer);

    contract = this._insertAddressGetter(contract, address);
    contract = await this._overrideConnectMethod(contract, this.getInterface(instance), contractName);

    return this._insertHandlers(contract, contractName) as unknown as I;
  }

  protected _insertHandlers(contract: BaseContract, contractName: string): BaseContract {
    const methodSet = new Set<string>();

    for (const methodFragments of this._getContractFunctionFragments(contract.interface)) {
      if (methodFragments.stateMutability === "view" || methodFragments.stateMutability === "pure") {
        continue;
      }

      const methodName = methodFragments.format();
      const shortName = methodName.split("(")[0];

      const oldMethod: BaseContractMethod = (contract as any)[methodName];

      const newMethod = this._wrapOldMethod(contractName, methodName, methodFragments, oldMethod, contract.runner!);

      defineProperties<any>(newMethod, {
        name: oldMethod.name,
        getFragment: oldMethod.getFragment,
        estimateGas: oldMethod.estimateGas,
        populateTransaction: oldMethod.populateTransaction,
        send: oldMethod.send,
        staticCall: oldMethod.staticCall,
        staticCallResult: oldMethod.staticCallResult,
      });
      Object.defineProperty(newMethod, "fragment", oldMethod.fragment);

      (contract as any)[methodName] = newMethod;

      if (!methodSet.has(shortName)) {
        (contract as any)[shortName] = newMethod;

        methodSet.add(shortName);
      } else {
        (contract as any)[shortName] = undefined;
      }
    }

    return contract;
  }

  protected async _overrideConnectMethod(
    contract: BaseContract,
    contractInterface: Interface,
    contractName: string,
  ): Promise<BaseContract> {
    const defaultRunner = await networkManager!.getSigner();

    contract.connect = (runner: ContractRunner | null): BaseContract => {
      const newContract = new BaseContract(contract.target, contractInterface, runner ?? defaultRunner);

      return this._insertHandlers(newContract, contractName) as any;
    };

    return contract;
  }

  private _insertAddressGetter(contract: BaseContract, contractAddress: string): BaseContract {
    (contract as any).address = contractAddress;

    return contract;
  }

  private _getContractFunctionFragments(contractInterface: Interface): FunctionFragment[] {
    const result: FunctionFragment[] = [];

    contractInterface.forEachFunction((fragment) => {
      result.push(fragment);
    });

    return result;
  }

  private _wrapOldMethod(
    contractName: string,
    methodName: string,
    methodFragments: FunctionFragment,
    oldMethod: BaseContractMethod,
    runner: ContractRunner,
  ): (...args: any[]) => Promise<ContractTransactionResponse> {
    return async (...args: any[]): Promise<ContractTransactionResponse> => {
      const tx = await oldMethod.populateTransaction(...args);
      tx.from = (runner as any).address || (await (runner as any).getAddress());

      await fillParameters(tx);

      const methodString = getMethodString(contractName, methodName, methodFragments, args);

      const keyFields = this._getKeyFieldsFromTransaction(tx);

      if (this._hre.config.migrate.execution.continue) {
        return this._recoverTransaction(methodString, keyFields, oldMethod, args);
      }

      return this._sendTransaction(methodString, keyFields, oldMethod, args);
    };
  }

  private async _recoverTransaction(
    methodString: string,
    tx: KeyTransactionFields,
    oldMethod: BaseContractMethod,
    args: any[],
  ) {
    try {
      const savedTransaction = TransactionProcessor?.tryRestoreSavedTransaction(tx);

      Reporter!.notifyTransactionRecovery(methodString, savedTransaction!);

      return this._wrapTransactionFieldsToSave(savedTransaction!);
    } catch {
      Reporter!.notifyTransactionSendingInsteadOfRecovery(methodString);

      return this._sendTransaction(methodString, tx, oldMethod, args);
    }
  }

  private async _sendTransaction(
    methodString: string,
    tx: KeyTransactionFields,
    oldMethod: BaseContractMethod,
    args: any[],
  ) {
    const txResponse: ContractTransactionResponse = (await oldMethod(...args)) as ContractTransactionResponse;

    const saveMetadata: MigrationMetadata = {
      migrationNumber: Stats.currentMigration,
      methodName: methodString,
    };

    await TransactionRunner!.reportTransactionResponse(txResponse, methodString);

    TransactionProcessor?.saveTransaction(tx, (await txResponse.wait())!, saveMetadata);

    return txResponse;
  }

  private _wrapTransactionFieldsToSave(data: TransactionFieldsToSave): ContractTransactionResponse {
    return {
      wait(_confirms?: number): Promise<ContractTransactionReceipt | null> {
        return data.receipt as unknown as Promise<ContractTransactionReceipt | null>;
      },
    } as unknown as ContractTransactionResponse;
  }

  private _getKeyFieldsFromTransaction(tx: ContractTransaction): KeyTransactionFields {
    return {
      name: this._getTransactionName(tx),
      data: tx.data,
      from: tx.from!,
      chainId: tx.chainId!,
      value: tx.value!,
      to: tx.to,
    };
  }

  private _getTransactionName(tx: ContractTransaction): string {
    if (tx.customData === undefined || tx.customData.txName === undefined) {
      return UNKNOWN_TRANSACTION_NAME;
    }

    return tx.customData.txName;
  }
}
