/* eslint-disable @typescript-eslint/no-unused-vars */
import { SignerProvider } from '@acala-network/eth-providers';
import { handleTxResponse } from '@acala-network/eth-providers/lib';
import type { TransactionReceipt } from '@ethersproject/abstract-provider';
import { TransactionRequest, TransactionResponse } from '@ethersproject/abstract-provider';
import {
  Signer as Abstractsigner,
  TypedDataDomain,
  TypedDataField,
  TypedDataSigner
} from '@ethersproject/abstract-signer';
import { getAddress } from '@ethersproject/address';
import { BigNumber } from '@ethersproject/bignumber';
import { Bytes, concat, joinSignature } from '@ethersproject/bytes';
import { Logger } from '@ethersproject/logger';
import { Deferrable, defineReadOnly } from '@ethersproject/properties';
import { toUtf8Bytes } from '@ethersproject/strings';
import { SubmittableResult } from '@polkadot/api';
import { SubmittableExtrinsic } from '@polkadot/api/types';
import { u8aConcat, u8aEq, u8aToHex } from '@polkadot/util';
import { blake2AsU8a, decodeAddress, isEthereumAddress } from '@polkadot/util-crypto';
import { SigningKey } from './SigningKey';
import { dataToString, toBN } from './utils';
import { version } from './_version';

export const logger = new Logger(version);

export class Signer extends Abstractsigner implements TypedDataSigner {
  // @ts-ignore
  readonly provider: SignerProvider;
  // @ts-ignore
  readonly signingKey: SigningKey;
  // @ts-ignore
  readonly _substrateAddress: string;

  constructor(provider: SignerProvider, address: string, signingKey: SigningKey) {
    super();

    defineReadOnly(this, 'provider', provider);
    defineReadOnly(this, 'signingKey', signingKey);

    // @ts-ignore
    this.provider.api.setSigner(signingKey);

    if (typeof address === 'string' && isEthereumAddress(address)) {
      logger.throwError('expect substrate address');
    } else {
      try {
        decodeAddress(address);
        defineReadOnly(this, '_substrateAddress', address);
      } catch {
        logger.throwArgumentError('invalid address', 'address', address);
      }
    }
  }

  connect(provider: SignerProvider): Signer {
    return logger.throwError('cannot alter JSON-RPC Signer connection', Logger.errors.UNSUPPORTED_OPERATION, {
      operation: 'connect'
    });
  }

  /**
   *
   * @param evmAddress The EVM address to check
   * @returns A promise that resolves to true if the EVM address is claimed
   * or false if the address is not claimed
   */
  async isClaimed(evmAddress?: string): Promise<boolean> {
    const rpcEvmAddress = await this.queryEvmAddress();

    if (!rpcEvmAddress) return false;
    if (!evmAddress) return true;
    if (rpcEvmAddress === evmAddress) {
      return true;
    }

    return logger.throwError('An evm account already exists to bind to this account');
  }

  /**
   * Get the signer's EVM address, and claim an EVM address if it has not claimed one.
   * @returns A promise resolving to the EVM address of the signer's substrate
   * address
   */
  async getAddress(): Promise<string> {
    const address = await this.queryEvmAddress();
    if (address) {
      return address;
    } else {
      // default address
      return this.computeDefaultEvmAddress();
    }
  }

  /**
   * Get the signers EVM address if it has claimed one.
   * @returns A promise resolving to the EVM address of the signer's substrate
   * address or an empty string if the EVM address isn't claimed
   */
  async queryEvmAddress(): Promise<string> {
    const address = await this.provider.api.query.evmAccounts.evmAddresses(this._substrateAddress);

    if (!address.isEmpty) {
      const evmAddress = getAddress(address.toString());
      return evmAddress;
    }

    return '';
  }

  /**
   *
   * @returns The default EVM address generated for the signer's substrate address
   */
  computeDefaultEvmAddress(): string {
    const address = this._substrateAddress;
    const publicKey = decodeAddress(address);

    const isStartWithEvm = u8aEq('evm:', publicKey.slice(0, 4));

    if (isStartWithEvm) {
      return getAddress(u8aToHex(publicKey.slice(4, 24)));
    }

    return getAddress(u8aToHex(blake2AsU8a(u8aConcat('evm:', publicKey), 256).slice(0, 20)));
  }

  /**
   *
   * @returns The substrate account stored in this Signer
   */
  async getSubstrateAddress(): Promise<string> {
    return this._substrateAddress;
  }

  async claimEvmAccount(evmAddress: string): Promise<void> {
    const isConnented = await this.isClaimed(evmAddress);

    if (isConnented) return;

    const publicKey = decodeAddress(this._substrateAddress);
    const data = 'acala evm:' + Buffer.from(publicKey).toString('hex');
    const signature = await this._signMessage(evmAddress, data);
    const extrinsic = this.provider.api.tx.evmAccounts.claimAccount(evmAddress, signature);

    await extrinsic.signAsync(this._substrateAddress);

    await new Promise<void>((resolve, reject) => {
      extrinsic
        .send((result: SubmittableResult) => {
          handleTxResponse(result, this.provider.api)
            .then(() => {
              resolve();
            })
            .catch((err) => {
              if (err.message === 'evmAccounts.AccountIdHasMapped') {
                resolve();
              }
              reject(err);
            });
        })
        .catch(reject);
    });
  }

  /**
   * Claims a default EVM address for this signer's substrate address
   */
  async claimDefaultAccount(): Promise<void> {
    const extrinsic = this.provider.api.tx.evmAccounts.claimDefaultAccount();

    await extrinsic.signAsync(this._substrateAddress);

    await new Promise<void>((resolve, reject) => {
      extrinsic
        .send((result: SubmittableResult) => {
          handleTxResponse(result, this.provider.api)
            .then(() => {
              resolve();
            })
            .catch((err) => {
              if (err.message === 'evmAccounts.AccountIdHasMapped') {
                resolve();
              }
              reject(err);
            });
        })
        .catch((error) => {
          reject(error);
        });
    });
  }

  signTransaction(transaction: Deferrable<TransactionRequest>): Promise<string> {
    return logger.throwError('signing transactions is unsupported', Logger.errors.UNSUPPORTED_OPERATION, {
      operation: 'signTransaction'
    });
  }

  /**
   *
   * @param transaction
   * @returns A promise that resolves to the transaction's response
   */
  async sendTransaction(_transaction: Deferrable<TransactionRequest>): Promise<TransactionResponse> {
    this._checkProvider('sendTransaction');

    const signerAddress = await this.getSubstrateAddress();
    const evmAddress = await this.getAddress();

    // estimateResources requires the from parameter.
    // However, when creating the contract, there is no from parameter in the tx
    const transaction = {
      from: evmAddress,
      ..._transaction
    };

    const resources = await this.provider.estimateResources(transaction);

    let gasLimit: BigNumber;
    let storageLimit: BigNumber;

    let totalLimit = await transaction.gasLimit;

    if (totalLimit === null || totalLimit === undefined) {
      gasLimit = resources.gas;
      storageLimit = resources.storage;
      totalLimit = resources.gas.add(resources.storage);
    } else {
      const estimateTotalLimit = resources.gas.add(resources.storage);
      gasLimit = BigNumber.from(totalLimit).mul(resources.gas).div(estimateTotalLimit).add(1);
      storageLimit = BigNumber.from(totalLimit).mul(resources.storage).div(estimateTotalLimit).add(1);
    }

    transaction.gasLimit = totalLimit;

    const tx = await this.populateTransaction(transaction);

    const data = tx.data;
    const from = tx.from;

    if (!data) {
      return logger.throwError('Request data not found');
    }

    if (!from) {
      return logger.throwError('Request from not found');
    }

    let extrinsic: SubmittableExtrinsic<'promise'>;

    // @TODO create contract
    if (!tx.to) {
      extrinsic = this.provider.api.tx.evm.create(
        tx.data,
        toBN(tx.value),
        toBN(gasLimit),
        toBN(storageLimit.isNegative() ? 0 : storageLimit),
        tx.accessList || []
      );
    } else {
      extrinsic = this.provider.api.tx.evm.call(
        tx.to,
        tx.data,
        toBN(tx.value),
        toBN(gasLimit),
        toBN(storageLimit.isNegative() ? 0 : storageLimit),
        tx.accessList || []
      );
    }

    await extrinsic.signAsync(signerAddress);

    return new Promise((resolve, reject) => {
      extrinsic
        .send((result: SubmittableResult) => {
          handleTxResponse(result, this.provider.api)
            .then(() => {
              resolve({
                hash: extrinsic.hash.toHex(),
                from: from || '',
                confirmations: 0,
                nonce: toBN(tx.nonce).toNumber(),
                gasLimit: BigNumber.from(tx.gasLimit || '0'),
                gasPrice: BigNumber.from(1),
                data: dataToString(data),
                value: BigNumber.from(tx.value || '0'),
                chainId: +this.provider.api.consts.evmAccounts.chainId.toString(),
                wait: (confirmations?: number): Promise<TransactionReceipt> => {
                  const hex = result.status.isInBlock
                    ? result.status.asInBlock.toHex()
                    : result.status.asFinalized.toHex();
                  return this.provider.getTransactionReceiptAtBlock(extrinsic.hash.toHex(), hex);
                }
              });
            })
            .catch(reject);
        })
        .catch(reject);
    });
  }

  /**
   *
   * @param message The message to sign
   * @returns A promise that resolves to the signed hash of the message
   */
  async signMessage(message: Bytes | string): Promise<string> {
    const evmAddress = await this.queryEvmAddress();
    return this._signMessage(evmAddress, message);
  }

  async _signMessage(evmAddress: string, message: Bytes | string): Promise<string> {
    if (!evmAddress) {
      return logger.throwError('No binding evm address');
    }
    const messagePrefix = '\x19Ethereum Signed Message:\n';
    if (typeof message === 'string') {
      message = toUtf8Bytes(message);
    }
    const msg = u8aToHex(concat([toUtf8Bytes(messagePrefix), toUtf8Bytes(String(message.length)), message]));

    if (!this.signingKey.signRaw) {
      return logger.throwError('Need to implement signRaw method');
    }

    const result = await this.signingKey.signRaw({
      address: evmAddress,
      data: msg,
      type: 'bytes'
    });

    return joinSignature(result.signature);
  }

  async _signTypedData(
    domain: TypedDataDomain,
    types: Record<string, Array<TypedDataField>>,
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    value: Record<string, any>
  ): Promise<string> {
    return logger.throwError('_signTypedData is unsupported', Logger.errors.UNSUPPORTED_OPERATION, {
      operation: '_signTypedData'
    });
  }
}