// SPDX-License-Identifier: Apache-2.0

import '@polkadot/api-augment';
import fs, { PathLike } from 'fs';
import { ApiPromise, WsProvider, Keyring } from '@polkadot/api';
import { convertWeight } from '@polkadot/api-contract/base/util';
import { CodePromise, ContractPromise } from '@polkadot/api-contract';
import { SubmittableExtrinsic } from '@polkadot/api/types';
import { AnyNumber, Codec, ISubmittableResult } from '@polkadot/types/types';
import { KeyringPair } from '@polkadot/keyring/types';
import expect from 'expect';
import { ContractExecResultResult, WeightV2 } from '@polkadot/types/interfaces';

const default_url = "ws://127.0.0.1:9944";

export function aliceKeypair(): KeyringPair {
  const keyring = new Keyring({ type: 'sr25519' });
  return keyring.addFromUri('//Alice');
}

export function daveKeypair(): KeyringPair {
  const keyring = new Keyring({ type: 'sr25519' });
  return keyring.addFromUri('//Dave');
}

export function createConnection(): Promise<ApiPromise> {
  const url = process.env.RPC_URL || default_url;

  return ApiPromise.create({ provider: new WsProvider(url) });
}

export function deploy(api: ApiPromise, pair: KeyringPair, file: PathLike, value: bigint, ...params: unknown[]): Promise<any> {
  const contractJson = fs.readFileSync(file, { encoding: 'utf-8' });

  const code = new CodePromise(api, contractJson, null);

  const gasLimit = api.registry.createType('WeightV2', { refTime: 100000n * 1000000n, proofSize: 100000n });
  const tx = code.tx.new({ gasLimit, value }, ...params);

  return new Promise(async (resolve, reject) => {
    const unsub = await tx.signAndSend(pair, (result: any) => {
      if (result.status.isInBlock || result.status.isFinalized) {
        resolve(result.contract);
        unsub();
      }

      if (result.isError) {
        if (result.dispatchError) {
          console.log(result.dispatchError.toHuman());
        } else {
          console.log(result.asError.toHuman());
        }

        reject(result);
        unsub();
      }
    });
  });
}

export function transaction(tx: SubmittableExtrinsic<"promise", ISubmittableResult>, pair: KeyringPair): Promise<ISubmittableResult> {
  return new Promise(async (resolve, reject) => {
    const unsub = await tx.signAndSend(pair, (result: any) => {
      if (result.dispatchError) {
        console.log(`dispatchError:${JSON.stringify(result)}`)
        reject(result);
        unsub();
      }

      if (result.isError) {
        console.log(`isError:${JSON.stringify(result)}`)
        reject(result);
        unsub();
      }

      if (result.status.isInBlock || result.status.isFinalized) {
        resolve(result);
        unsub();
      }
    });
  });
}

// Returns the required gas estimated from a dry run
export async function weight(api: ApiPromise, contract: ContractPromise, message: string, args?: unknown[], value?: AnyNumber) {
  let res = await dry_run(api, contract, message, args, value);
  return res.gasRequired
}

// Returns the debug buffer from the dry run result
export async function debug_buffer(api: ApiPromise, contract: ContractPromise, message: string, args?: unknown[], value?: AnyNumber) {
  let res = await dry_run(api, contract, message, args, value);
  return res.debugMessage.toHuman()
}

// Return dry run result
export async function dry_run(api: ApiPromise, contract: ContractPromise, message: string, args?: unknown[], value?: AnyNumber) {
  const ALICE = new Keyring({ type: 'sr25519' }).addFromUri('//Alice').address;
  const msg = contract.abi.findMessage(message);
  const dry = await api.call.contractsApi.call(ALICE, contract.address, value ? value : 0, null, null, msg.toU8a(args ? args : []));
  return dry;
}

// FIXME: The old contract.query API does not support WeightV2 yet
export async function query(
  api: ApiPromise,
  account: KeyringPair,
  contract: ContractPromise,
  message: string,
  args?: unknown[],
  value?: number,
  gasLimit?: WeightV2 | { refTime?: any; proofSize?: any; }
): Promise<{ output: Codec | null, result: ContractExecResultResult }> {
  const msg = contract.abi.findMessage(message);
  const callResult = await api.call.contractsApi.call(account.address, contract.address, value ? value : 0, gasLimit ? gasLimit : null, null, msg.toU8a(args ? args : []));
  // Same logic as contracts UI, so should be fine.
  // Reference implementation: https://github.com/paritytech/contracts-ui/blob/e343221a0d5c1ae67122fe99028246e5bdf38c46/src/ui/hooks/useDecodedOutput.ts
  const output = callResult.result.isOk && msg.returnType && callResult.result.asOk.flags.isEmpty
    ? contract.abi.registry.createTypeUnsafe(
      msg.returnType.lookupName || msg.returnType.type,
      [callResult.result.asOk.data.toU8a(true)],
      { isPedantic: true }
    )
    : null;
  expect(output !== null && typeof output === 'object' && 'Err' in output).toBeFalsy();
  return { output, result: callResult.result };
}
