import {ethers, SigningKey} from 'ethers';
import {EIP712_TX_TYPE} from './utils';
import {TransactionLike, TransactionBuilder, PayloadSigner} from './types';

/**
 * Signs the `payload` using an ECDSA private key.
 *
 * @param payload The payload that needs to be signed.
 * @param secret The ECDSA private key.
 *
 * @example Sign EIP712 transaction hash.
 *
 * import { EIP712Signer, types, utils } from "zksync-ethers";
 *
 * const PRIVATE_KEY = "<PRIVATE_KEY>";
 *
 * const tx: types.TransactionRequest = {
 *   chainId: 270,
 *   from: ADDRESS,
 *   to: "<RECEIVER>",
 *   value: 7_000_000_000,
 * };
 *
 * const txHash = EIP712Signer.getSignedDigest(tx);
 * const result = await utils.signPayloadWithECDSA(txHash, PRIVATE_KEY);
 *
 * @example Sign message hash.
 *
 * import { utils } from "zksync-ethers";
 * import { hashMessage } from "ethers";
 *
 * const PRIVATE_KEY = "<PRIVATE_KEY>";
 *
 * const message = 'Hello World!';
 * const messageHash = hashMessage(message);
 *
 * const result = await utils.signPayloadWithECDSA(messageHash, PRIVATE_KEY);
 *
 * @example Sign typed data hash.
 *
 * import { utils } from "zksync-ethers";
 * import { TypedDataEncoder } from "ethers";
 *
 * const PRIVATE_KEY = "<PRIVATE_KEY>";
 *
 * const typedDataHash = TypedDataEncoder.hash(
 *   {name: 'Example', version: '1', chainId: 270},
 *   {
 *     Person: [
 *       {name: 'name', type: 'string'},
 *       {name: 'age', type: 'uint8'},
 *     ],
 *   },
 *   {name: 'John', age: 30}
 * );
 * const result = await utils.signPayloadWithECDSA(typedDataHash, PRIVATE_KEY);
 */
export const signPayloadWithECDSA: PayloadSigner = async (
  payload,
  secret: string | SigningKey
) => {
  return new ethers.Wallet(secret).signingKey.sign(payload).serialized;
};

/**
 * Signs the `payload` using multiple ECDSA private keys.
 * The signature is generated by concatenating signatures created by signing with each key individually.
 * The length of the resulting signature should be `secrets.length * 65 + 2`.
 *
 * @param payload The payload that needs to be signed.
 * @param secret The list of the ECDSA private keys.
 *
 * @throws {Error} If the `secret` is not an array of at least two elements.
 *
 * @example Sign EIP712 transaction hash.
 *
 * import { EIP712Signer, types, utils } from "zksync-ethers";
 *
 * const PRIVATE_KEY1 = "<PRIVATE_KEY1>";
 * const PRIVATE_KEY2 = "<PRIVATE_KEY2>";
 *
 * const tx: types.TransactionRequest = {
 *   chainId: 270,
 *   from: ADDRESS,
 *   to: "<RECEIVER>",
 *   value: 7_000_000_000,
 * };
 *
 * const txHash = EIP712Signer.getSignedDigest(tx);
 * const result = await utils.signPayloadWithMultipleECDSA(typedDataHash, [PRIVATE_KEY1, PRIVATE_KEY2]);
 *
 * @example Sign message hash.
 *
 * import { utils } from "zksync-ethers";
 * import { hashMessage } from "ethers";
 *
 * const PRIVATE_KEY1 = "<PRIVATE_KEY1>";
 * const PRIVATE_KEY2 = "<PRIVATE_KEY2>";
 *
 * const message = 'Hello World!';
 * const messageHash = hashMessage(message);
 *
 * const result = await utils.signPayloadWithMultipleECDSA(typedDataHash, [PRIVATE_KEY1, PRIVATE_KEY2]);
 *
 * @example Sign typed data hash.
 *
 * import { utils } from "zksync-ethers";
 * import { TypedDataEncoder } from "ethers";
 *
 * const PRIVATE_KEY1 = "<PRIVATE_KEY1>";
 * const PRIVATE_KEY2 = "<PRIVATE_KEY2>";
 *
 * const typedDataHash = TypedDataEncoder.hash(
 *   {name: 'Example', version: '1', chainId: 270},
 *   {
 *     Person: [
 *       {name: 'name', type: 'string'},
 *       {name: 'age', type: 'uint8'},
 *     ],
 *   },
 *   {name: 'John', age: 30}
 * );
 * const result = await utils.signPayloadWithMultipleECDSA(typedDataHash, [PRIVATE_KEY1, PRIVATE_KEY2]);
 */
export const signPayloadWithMultipleECDSA: PayloadSigner = async (
  payload,
  secret: string[] | SigningKey[]
) => {
  if (!Array.isArray(secret) || secret.length < 2) {
    throw new Error('Multiple keys are required for multisig signing!');
  }

  const signatures = secret.map(
    key =>
      // Note, that `signMessage` wouldn't work here, since we don't want
      // the signed hash to be prefixed with `\x19Ethereum Signed Message:\n`
      ethers.Signature.from(new ethers.Wallet(key).signingKey.sign(payload))
        .serialized
  );

  return ethers.concat(signatures);
};

/**
 * Populates missing properties meant for signing using an ECDSA private key:
 *
 * - Populates `from` using the address derived from the ECDSA private key.
 * - Populates `nonce` via `provider.getTransactionCount(tx.from, "pending")`.
 * - Populates `gasLimit` via `provider.estimateGas(tx)`. If `tx.from` is not EOA, the estimation is done with address
 * derived from the ECDSA private key.
 * - Populates `chainId` via `provider.getNetwork()`.
 * - Populates `type` with `utils.EIP712_TX_TYPE`.
 * - Populates `value` by converting to `bigint` if set, otherwise to `0n`.
 * - Populates `data` with `0x`.
 * - Populates `customData` with `{factoryDeps=[], gasPerPubdata=utils.DEFAULT_GAS_PER_PUBDATA_LIMIT}`.
 *
 * @param tx The transaction that needs to be populated.
 * @param [secret] The ECDSA private key used for populating the transaction.
 * @param [provider] The provider is used to fetch data from the network if it is required for signing.
 *
 * @throws {Error} Requires `provider` to be set.
 *
 * @example
 *
 * import { Provider, types, utils } from "zksync-ethers";
 *
 * const PRIVATE_KEY = "<PRIVATE_KEY>";
 *
 * const provider = Provider.getDefaultProvider(types.Network.Sepolia);
 *
 * const populatedTx = await utils.populateTransactionECDSA(
 *   {
 *     chainId: 270,
 *     to: "<RECEIVER>",
 *     value: 7_000_000_000,
 *   },
 *   PRIVATE_KEY,
 *   provider
 * );
 */
export const populateTransactionECDSA: TransactionBuilder = async (
  tx,
  secret: string | SigningKey,
  provider
) => {
  if (!provider) {
    throw new Error('Provider is required but is not provided!');
  }

  const populatedTx = {...tx};

  populatedTx.type = EIP712_TX_TYPE;
  populatedTx.chainId ??= (await provider.getNetwork()).chainId;
  populatedTx.value = populatedTx.value ? BigInt(populatedTx.value) : 0n;
  populatedTx.data ??= '0x';
  populatedTx.customData = tx.customData ?? {};
  populatedTx.customData.factoryDeps ??= [];

  populatedTx.from ??= new ethers.Wallet(secret).address;

  if (
    populatedTx.gasPrice &&
    (populatedTx.maxFeePerGas || populatedTx.maxPriorityFeePerGas)
  ) {
    throw new Error(
      'Provide combination of maxFeePerGas and maxPriorityFeePerGas or provide gasPrice. Not both!'
    );
  }
  if (
    !populatedTx.gasLimit ||
    (!populatedTx.gasPrice &&
      (!populatedTx.maxFeePerGas ||
        populatedTx.maxPriorityFeePerGas === null ||
        populatedTx.maxPriorityFeePerGas === undefined))
  ) {
    let fromToUse = populatedTx.from;
    const isContractAccount =
      ethers.getBytes(await provider.getCode(populatedTx.from)).length !== 0;
    if (isContractAccount) {
      // Gas estimation does not work when initiator is contract account (works only with EOA).
      // In order to estimation gas, the transaction's from value is replaced with signer's address.
      fromToUse = new ethers.Wallet(secret).address;
    }
    const fee = await provider.estimateFee({
      ...populatedTx,
      from: fromToUse,
    });

    populatedTx.gasLimit ??= fee.gasLimit;
    populatedTx.customData.gasPerPubdata ??= fee.gasPerPubdataLimit;
    if (!populatedTx.gasPrice) {
      populatedTx.maxFeePerGas ??= fee.maxFeePerGas;
      populatedTx.maxPriorityFeePerGas ??= fee.maxPriorityFeePerGas;
    }
  }

  populatedTx.nonce ??= await provider.getTransactionCount(
    populatedTx.from,
    'pending'
  );
  return populatedTx as TransactionLike;
};

/**
 * Populates missing properties meant for signing using multiple ECDSA private keys.
 * It uses {@link populateTransactionECDSA}, where the address of the first ECDSA key is set as the `secret` argument.
 *
 * @param tx The transaction that needs to be populated.
 * @param [secret] The list of the ECDSA private keys used for populating the transaction.
 * @param [provider] The provider is used to fetch data from the network if it is required for signing.
 *
 * @throws {Error} The `secret` must be an array of at least two elements.
 *
 * @example
 *
 * import { Provider, types, utils } from "zksync-ethers";
 *
 * const PRIVATE_KEY1 = "<PRIVATE_KEY1>";
 * const PRIVATE_KEY2 = "<PRIVATE_KEY2>";
 *
 * const provider = Provider.getDefaultProvider(types.Network.Sepolia);
 *
 * const populatedTx = await utils.populateTransactionMultisigECDSA(
 *   {
 *     chainId: 270,
 *     to: "<RECEIVER>",
 *     value: 7_000_000_000,
 *   },
 *   [PRIVATE_KEY1, PRIVATE_KEY2],
 *   provider
 * );
 */
export const populateTransactionMultisigECDSA: TransactionBuilder = async (
  tx,
  secret: string[] | SigningKey[],
  provider
) => {
  if (!Array.isArray(secret) || secret.length < 2) {
    throw new Error('Multiple keys are required to build the transaction!');
  }
  // estimates gas accepts only one address, so the first signer is chosen.
  return populateTransactionECDSA(tx, secret[0], provider);
};
