/* eslint-disable @typescript-eslint/no-unused-vars */
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, isNumber } from '@polkadot/util';
import {
  blake2AsU8a,
  decodeAddress,
  isEthereumAddress
} from '@polkadot/util-crypto';

import { Provider } from './Provider';
import { SigningKey } from './SigningKey';
import { dataToString, handleTxResponse, toBN } from './utils';

const logger = new Logger('evm-provider');

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

  constructor(provider: Provider, 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: Provider): 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 = 'Reef 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(({ message, result }) => {
              if (message === 'evmAccounts.AccountIdHasMapped') {
                resolve();
              }
              reject(message);
            });
        })
        .catch((error) => {
          reject(error && error.message);
        });
    });
  }

  /**
   * 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(({ message, result }) => {
              if (message === 'evmAccounts.AccountIdHasMapped') {
                resolve();
              }
              reject(message);
            });
        })
        .catch((error) => {
          reject(error && error.message);
        });
    });
  }

  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);

    // Multiply by 3.1
    const gasLimit: BigNumber = resources.gas.mul(31).div(10);
    let storageLimit: BigNumber;

    // If the storage limit is supplied, override it from the estimateResources
    if (transaction.customData) {
      if ('storageLimit' in transaction.customData) {
        storageLimit = transaction.customData.storageLimit;
        if (isNumber(storageLimit)) {
          storageLimit = BigNumber.from(storageLimit);
        }
      }
    } else {
      storageLimit = resources.storage.mul(31).div(10);
    }

    let totalLimit = await transaction.gasLimit;

    if (totalLimit === null || totalLimit === undefined) {
      totalLimit = gasLimit.add(storageLimit);
    }

    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)
      );
    } else {
      extrinsic = this.provider.api.tx.evm.call(
        tx.to,
        tx.data,
        toBN(tx.value),
        toBN(gasLimit),
        toBN(storageLimit.isNegative() ? 0 : storageLimit)
      );
    }

    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(0),
                data: dataToString(data),
                value: BigNumber.from(tx.value || '0'),
                chainId: 13939,
                wait: (confirmations?: number): Promise<TransactionReceipt> => {
                  return this.provider._resolveTransactionReceipt(
                    extrinsic.hash.toHex(),
                    result.status.asInBlock.toHex(),
                    from
                  );
                }
              });
            })
            .catch(({ message, result }) => {
              reject(message);
            });
        })
        .catch((error) => {
          reject(error && error.message);
        });
    });
  }

  /**
   *
   * @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'
      }
    );
  }
}