import stringify from 'safe-stable-stringify';
import { SortKeyCacheResult } from '../cache/SortKeyCache';
import { ContractCallRecord, InteractionCall } from '../core/ContractCallRecord';
import { ExecutionContext } from '../core/ExecutionContext';
import {
  ContractInteraction,
  HandlerApi,
  InteractionData,
  InteractionResult,
  InteractionType
} from '../core/modules/impl/HandlerExecutorFactory';
import { LexicographicalInteractionsSorter } from '../core/modules/impl/LexicographicalInteractionsSorter';
import { InteractionsSorter } from '../core/modules/InteractionsSorter';
import { DefaultEvaluationOptions, EvalStateResult, EvaluationOptions } from '../core/modules/StateEvaluator';
import { WARP_TAGS } from '../core/KnownTags';
import { Warp } from '../core/Warp';
import { createDummyTx, createInteractionTx } from '../legacy/create-interaction-tx';
import { GQLNodeInterface } from '../legacy/gqlResult';
import { Benchmark } from '../logging/Benchmark';
import { LoggerFactory } from '../logging/LoggerFactory';
import { Evolve } from '../plugins/Evolve';
import { ArweaveWrapper } from '../utils/ArweaveWrapper';
import { getJsonResponse, sleep, stripTrailingSlash } from '../utils/utils';
import {
  BenchmarkStats,
  Contract,
  DREContractStatusResponse,
  InnerCallData,
  WriteInteractionOptions,
  WriteInteractionResponse
} from './Contract';
import { ArTransfer, ArWallet, emptyTransfer, Tags } from './deploy/CreateContract';
import { InnerWritesEvaluator } from './InnerWritesEvaluator';
import { CustomSignature, Signature } from './Signature';
import { EvaluationOptionsEvaluator } from './EvaluationOptionsEvaluator';
import { WarpFetchWrapper } from '../core/WarpFetchWrapper';
import { Mutex } from 'async-mutex';
import { TransactionStatusResponse } from '../utils/types/arweave-types';
import { InteractionState } from './states/InteractionState';
import { ContractInteractionState } from './states/ContractInteractionState';
import { Crypto } from 'warp-isomorphic';
import { VrfPluginFunctions } from '../core/WarpPlugin';
import Arweave from 'arweave';

/**
 * An implementation of {@link Contract} that is backwards compatible with current style
 * of writing SW contracts (ie. using the "handle" function).
 *
 * It requires {@link ExecutorFactory} that is using {@link HandlerApi} generic type.
 */
export class HandlerBasedContract<State> implements Contract<State> {
  private readonly logger = LoggerFactory.INST.create('HandlerBasedContract');

  // TODO: refactor: extract execution context logic to a separate class
  private readonly ecLogger = LoggerFactory.INST.create('ExecutionContext');

  private readonly _innerWritesEvaluator = new InnerWritesEvaluator();
  private readonly _callDepth: number;
  private readonly _arweaveWrapper: ArweaveWrapper;
  private readonly _mutex = new Mutex();

  private _callStack: ContractCallRecord;
  private _evaluationOptions: EvaluationOptions;
  private _eoEvaluator: EvaluationOptionsEvaluator; // this is set after loading Contract Definition for the root contract
  private _benchmarkStats: BenchmarkStats = null;

  private _sorter: InteractionsSorter;
  private _rootSortKey: string;
  private _signature: Signature;
  private _warpFetchWrapper: WarpFetchWrapper;
  private _children: HandlerBasedContract<unknown>[] = [];
  private _interactionState;
  private _dreStates = new Map<string, SortKeyCacheResult<EvalStateResult<State>>>();

  constructor(
    private readonly _contractTxId: string,
    protected readonly warp: Warp,
    private readonly _parentContract: Contract = null,
    private readonly _innerCallData: InnerCallData = null
  ) {
    this.waitForConfirmation = this.waitForConfirmation.bind(this);
    this._arweaveWrapper = new ArweaveWrapper(warp);
    this._sorter = new LexicographicalInteractionsSorter(warp.arweave);
    if (_parentContract != null) {
      this._evaluationOptions = this.getRoot().evaluationOptions();
      this._callDepth = _parentContract.callDepth() + 1;
      const callingInteraction: InteractionCall = _parentContract
        .getCallStack()
        .getInteraction(_innerCallData.callingInteraction.id);

      if (this._callDepth > this._evaluationOptions.maxCallDepth) {
        throw new Error(
          `Max call depth of ${this._evaluationOptions.maxCallDepth} has been exceeded for interaction ${JSON.stringify(
            callingInteraction.interactionInput
          )}`
        );
      }
      this.logger.debug('Calling interaction', {
        id: _innerCallData.callingInteraction.id,
        sortKey: _innerCallData.callingInteraction.sortKey,
        type: _innerCallData.callType
      });

      // if you're reading a state of the contract, on which you've just made a write - you're doing it wrong.
      // the current state of the callee contract is always in the result of an internal write.
      // following is a protection against naughty developers who might be doing such crazy things ;-)
      if (
        callingInteraction.interactionInput?.foreignContractCalls[_contractTxId]?.innerCallType === 'write' &&
        _innerCallData.callType === 'read'
      ) {
        throw new Error(
          'Calling a readContractState after performing an inner write is wrong - instead use a state from the result of an internal write.'
        );
      }

      const callStack = new ContractCallRecord(_contractTxId, this._callDepth, _innerCallData?.callType);
      callingInteraction.interactionInput.foreignContractCalls[_contractTxId] = callStack;
      this._callStack = callStack;
      this._rootSortKey = _parentContract.rootSortKey;
      (_parentContract as HandlerBasedContract<unknown>)._children.push(this);
    } else {
      this._callDepth = 0;
      this._callStack = new ContractCallRecord(_contractTxId, 0);
      this._rootSortKey = null;
      this._evaluationOptions = new DefaultEvaluationOptions();
      this._children = [];
      this._interactionState = new ContractInteractionState(warp);
    }

    this.getCallStack = this.getCallStack.bind(this);
    this._warpFetchWrapper = new WarpFetchWrapper(this.warp);
  }

  async readState(
    sortKeyOrBlockHeight?: string | number,
    caller?: string,
    interactions?: GQLNodeInterface[]
  ): Promise<SortKeyCacheResult<EvalStateResult<State>>> {
    this.logger.info('Read state for', {
      contractTxId: this._contractTxId,
      sortKeyOrBlockHeight
    });
    if (!this.isRoot() && sortKeyOrBlockHeight == null) {
      throw new Error('SortKey MUST be always set for non-root contract calls');
    }
    const { stateEvaluator } = this.warp;
    const sortKey =
      typeof sortKeyOrBlockHeight == 'number'
        ? this._sorter.generateLastSortKey(sortKeyOrBlockHeight)
        : sortKeyOrBlockHeight;

    if (sortKey && !this.isRoot() && this.interactionState().has(this.txId())) {
      const result = this.interactionState().get(this.txId());
      return new SortKeyCacheResult<EvalStateResult<State>>(sortKey, result as EvalStateResult<State>);
    }

    // TODO: not sure if we should synchronize on a contract instance or contractTxId
    // in the latter case, the warp instance should keep a map contractTxId -> mutex
    const releaseMutex = await this._mutex.acquire();
    try {
      const initBenchmark = Benchmark.measure();
      this.maybeResetRootContract();

      const executionContext = await this.createExecutionContext(this._contractTxId, sortKey, false, interactions);
      this.logger.info('Execution Context', {
        srcTxId: executionContext.contractDefinition?.srcTxId,
        missingInteractions: executionContext.sortedInteractions?.length,
        cachedSortKey: executionContext.cachedState?.sortKey
      });
      initBenchmark.stop();

      const stateBenchmark = Benchmark.measure();
      const result = await stateEvaluator.eval(executionContext);
      stateBenchmark.stop();

      const total = (initBenchmark.elapsed(true) as number) + (stateBenchmark.elapsed(true) as number);

      this._benchmarkStats = {
        gatewayCommunication: initBenchmark.elapsed(true) as number,
        stateEvaluation: stateBenchmark.elapsed(true) as number,
        total
      };

      this.logger.info('Benchmark', {
        'Gateway communication  ': initBenchmark.elapsed(),
        'Contract evaluation    ': stateBenchmark.elapsed(),
        'Total:                 ': `${total.toFixed(0)}ms`
      });

      if (sortKey && !this.isRoot()) {
        this.interactionState().update(this.txId(), result.cachedValue);
      }

      return result;
    } finally {
      releaseMutex();
    }
  }

  async readStateFor(
    sortKey: string,
    interactions: GQLNodeInterface[]
  ): Promise<SortKeyCacheResult<EvalStateResult<State>>> {
    return this.readState(sortKey, undefined, interactions);
  }

  async viewState<Input, View>(
    input: Input,
    tags: Tags = [],
    transfer: ArTransfer = emptyTransfer
  ): Promise<InteractionResult<State, View>> {
    this.logger.info('View state for', this._contractTxId);
    return await this.callContract<Input, View>(input, 'view', undefined, undefined, tags, transfer);
  }

  async viewStateForTx<Input, View>(
    input: Input,
    interactionTx: GQLNodeInterface
  ): Promise<InteractionResult<State, View>> {
    this.logger.info(`View state for ${this._contractTxId}`);
    return await this.doApplyInputOnTx<Input, View>(input, interactionTx, 'view');
  }

  async dryWrite<Input>(
    input: Input,
    caller?: string,
    tags?: Tags,
    transfer?: ArTransfer,
    vrf?: boolean
  ): Promise<InteractionResult<State, unknown>> {
    this.logger.info('Dry-write for', this._contractTxId);
    return await this.callContract<Input>(input, 'write', caller, undefined, tags, transfer, undefined, vrf);
  }

  async applyInput<Input>(input: Input, transaction: GQLNodeInterface): Promise<InteractionResult<State, unknown>> {
    this.logger.info(`Apply-input from transaction ${transaction.id} for ${this._contractTxId}`);
    return await this.doApplyInputOnTx<Input>(input, transaction, 'write');
  }

  async writeInteraction<Input>(
    input: Input,
    options?: WriteInteractionOptions
  ): Promise<WriteInteractionResponse | null> {
    this.logger.info('Write interaction', { input, options });
    if (!this._signature) {
      throw new Error("Wallet not connected. Use 'connect' method first.");
    }
    const { arweave, interactionsLoader, environment } = this.warp;

    // we're calling this to verify whether proper env is used for this contract
    // (e.g. test env for test contract)
    await this.warp.definitionLoader.load(this._contractTxId);

    const effectiveTags = options?.tags || [];
    const effectiveTransfer = options?.transfer || emptyTransfer;
    const effectiveStrict = options?.strict === true;
    const effectiveVrf = options?.vrf === true;
    const effectiveDisableBundling = options?.disableBundling === true;
    const effectiveReward = options?.reward;

    const bundleInteraction = interactionsLoader.type() == 'warp' && !effectiveDisableBundling;

    this._signature.checkNonArweaveSigningAvailability(bundleInteraction);

    if (
      bundleInteraction &&
      effectiveTransfer.target != emptyTransfer.target &&
      effectiveTransfer.winstonQty != emptyTransfer.winstonQty
    ) {
      throw new Error('Ar Transfers are not allowed for bundled interactions');
    }

    if (effectiveVrf && !bundleInteraction && environment === 'mainnet') {
      throw new Error('Vrf generation is only available for bundle interaction');
    }

    if (bundleInteraction) {
      return await this.bundleInteraction(input, {
        tags: effectiveTags,
        strict: effectiveStrict,
        vrf: effectiveVrf
      });
    } else {
      const interactionTx = await this.createInteraction(
        input,
        effectiveTags,
        effectiveTransfer,
        effectiveStrict,
        false,
        effectiveVrf && environment !== 'mainnet',
        effectiveReward
      );
      const response = await arweave.transactions.post(interactionTx);

      if (response.status !== 200) {
        this.logger.error('Error while posting transaction', response);
        return null;
      }

      if (this._evaluationOptions.waitForConfirmation) {
        this.logger.info('Waiting for confirmation of', interactionTx.id);
        const benchmark = Benchmark.measure();
        await this.waitForConfirmation(interactionTx.id);
        this.logger.info('Transaction confirmed after', benchmark.elapsed());
      }

      if (this.warp.environment == 'local' && this._evaluationOptions.mineArLocalBlocks) {
        await this.warp.testing.mineBlock();
      }

      return { originalTxId: interactionTx.id };
    }
  }

  private async bundleInteraction<Input>(
    input: Input,
    options: {
      tags: Tags;
      strict: boolean;
      vrf: boolean;
    }
  ): Promise<WriteInteractionResponse | null> {
    this.logger.info('Bundle interaction input', input);

    const interactionTx = await this.createInteraction(
      input,
      options.tags,
      emptyTransfer,
      options.strict,
      true,
      options.vrf
    );

    const response = this._warpFetchWrapper.fetch(
      `${stripTrailingSlash(this._evaluationOptions.sequencerUrl)}/gateway/sequencer/register`,
      {
        method: 'POST',
        body: JSON.stringify(interactionTx),
        headers: {
          'Accept-Encoding': 'gzip, deflate, br',
          'Content-Type': 'application/json',
          Accept: 'application/json'
        }
      }
    );

    return {
      bundlrResponse: await getJsonResponse(response),
      originalTxId: interactionTx.id
    };
  }

  private async createInteraction<Input>(
    input: Input,
    tags: Tags,
    transfer: ArTransfer,
    strict: boolean,
    bundle = false,
    vrf = false,
    reward?: string
  ) {
    if (this._evaluationOptions.internalWrites) {
      // it modifies tags
      await this.discoverInternalWrites<Input>(input, tags, transfer, strict, vrf);
    }

    if (vrf) {
      tags.push({
        name: WARP_TAGS.REQUEST_VRF,
        value: 'true'
      });
    }

    const interactionTx = await createInteractionTx(
      this.warp.arweave,
      this._signature.signer,
      this._contractTxId,
      input,
      tags,
      transfer.target,
      transfer.winstonQty,
      bundle,
      this.warp.environment === 'testnet',
      reward
    );

    if (!this._evaluationOptions.internalWrites && strict) {
      const { arweave } = this.warp;
      const caller =
        this._signature.type == 'arweave'
          ? await arweave.wallets.ownerToAddress(interactionTx.owner)
          : interactionTx.owner;
      const handlerResult = await this.callContract(input, 'write', caller, undefined, tags, transfer, strict, vrf);
      if (handlerResult.type !== 'ok') {
        throw Error('Cannot create interaction: ' + JSON.stringify(handlerResult.error || handlerResult.errorMessage));
      }
    }

    return interactionTx;
  }

  txId(): string {
    return this._contractTxId;
  }

  getCallStack(): ContractCallRecord {
    return this._callStack;
  }

  connect(signature: ArWallet | CustomSignature): Contract<State> {
    this._signature = new Signature(this.warp, signature);
    return this;
  }

  setEvaluationOptions(options: Partial<EvaluationOptions>): Contract<State> {
    if (!this.isRoot()) {
      throw new Error('Evaluation options can be set only for the root contract');
    }
    this._evaluationOptions = {
      ...this._evaluationOptions,
      ...options
    };
    return this;
  }

  private async waitForConfirmation(transactionId: string): Promise<TransactionStatusResponse> {
    const { arweave } = this.warp;

    const status = await arweave.transactions.getStatus(transactionId);

    if (status.confirmed === null) {
      this.logger.info(`Transaction ${transactionId} not yet confirmed. Waiting another 20 seconds before next check.`);
      await sleep(20000);
      await this.waitForConfirmation(transactionId);
    } else {
      this.logger.info(`Transaction ${transactionId} confirmed`, status);
      return status;
    }
  }

  private async createExecutionContext(
    contractTxId: string,
    upToSortKey?: string,
    forceDefinitionLoad = false,
    interactions?: GQLNodeInterface[]
  ): Promise<ExecutionContext<State, HandlerApi<State>>> {
    const { definitionLoader, interactionsLoader, stateEvaluator } = this.warp;

    const benchmark = Benchmark.measure();
    let cachedState = await stateEvaluator.latestAvailableState<State>(contractTxId, upToSortKey);

    this.logger.debug('cache lookup', benchmark.elapsed());
    benchmark.reset();

    const evolvedSrcTxId = Evolve.evolvedSrcTxId(cachedState?.cachedValue?.state);
    let handler, contractDefinition, contractEvaluationOptions, remoteState;
    let sortedInteractions = interactions || [];

    this.logger.debug('Cached state', cachedState, upToSortKey);

    if (cachedState && cachedState.sortKey == upToSortKey) {
      this.logger.debug('State fully cached, not loading interactions.');
      if (forceDefinitionLoad || evolvedSrcTxId || interactions?.length) {
        contractDefinition = await definitionLoader.load<State>(contractTxId, evolvedSrcTxId);
        if (interactions?.length) {
          sortedInteractions = (await this._sorter.sort(interactions.map((i) => ({ node: i, cursor: null })))).map(
            (i) => i.node
          );
        }
      }
    } else {
      // if we want to apply some 'external' interactions on top of the state cached at given sort key
      // AND we don't have the state cached at the exact requested sort key - throw.
      // NOTE: this feature is used by the D.R.E. nodes.
      if (interactions?.length) {
        throw new Error(`Cannot apply requested interactions at ${upToSortKey}`);
      }

      contractDefinition = await definitionLoader.load<State>(contractTxId, evolvedSrcTxId);
      contractEvaluationOptions = this.resolveEvaluationOptions(contractDefinition.manifest?.evaluationOptions);

      if (contractEvaluationOptions.remoteStateSyncEnabled && !contractEvaluationOptions.useKVStorage) {
        remoteState = await this.getRemoteContractState(contractTxId);
        cachedState = await this.maybeSyncStateWithRemoteSource(remoteState, upToSortKey, cachedState);
        const maybeEvolvedSrcTxId = Evolve.evolvedSrcTxId(cachedState?.cachedValue?.state);
        if (maybeEvolvedSrcTxId && maybeEvolvedSrcTxId !== contractDefinition.srcTxId) {
          // even though the state will be synced, the CacheableStateEvaluator will
          // still try to init it in the WASM module (https://github.com/warp-contracts/warp/issues/372)
          // if the state struct definition has changed via evolve - there is a risk of panic in Rust.
          // that's why the contract definition has to be updated.
          contractDefinition = await definitionLoader.load<State>(contractTxId, maybeEvolvedSrcTxId);
        }
      }

      if (!remoteState && sortedInteractions.length == 0) {
        sortedInteractions = await interactionsLoader.load(
          contractTxId,
          cachedState?.sortKey,
          this.getToSortKey(upToSortKey),
          contractEvaluationOptions
        );
      }

      // we still need to return only interactions up to original "upToSortKey"
      if (cachedState?.sortKey) {
        sortedInteractions = sortedInteractions.filter((i) => i.sortKey.localeCompare(cachedState?.sortKey) > 0);
      }

      if (upToSortKey) {
        sortedInteractions = sortedInteractions.filter((i) => i.sortKey.localeCompare(upToSortKey) <= 0);
      }

      this.logger.debug('contract and interactions load', benchmark.elapsed());
      if (this.isRoot() && sortedInteractions.length) {
        // note: if the root contract has zero interactions, it still should be safe
        // - as no other contracts will be called.
        this._rootSortKey = sortedInteractions[sortedInteractions.length - 1].sortKey;
      }
    }

    if (contractDefinition) {
      if (!contractEvaluationOptions) {
        contractEvaluationOptions = this.resolveEvaluationOptions(contractDefinition.manifest?.evaluationOptions);
      }

      this.ecLogger.debug(`Evaluation options ${contractTxId}:`, contractEvaluationOptions);

      handler = (await this.warp.executorFactory.create(
        contractDefinition,
        contractEvaluationOptions,
        this.warp,
        this.interactionState()
      )) as HandlerApi<State>;
    }

    return {
      warp: this.warp,
      contract: this,
      contractDefinition,
      sortedInteractions,
      evaluationOptions: contractEvaluationOptions || this.evaluationOptions(),
      handler,
      cachedState,
      requestedSortKey: upToSortKey
    };
  }

  private resolveEvaluationOptions(rootManifestEvalOptions: EvaluationOptions) {
    if (this.isRoot()) {
      this._eoEvaluator = new EvaluationOptionsEvaluator(this.evaluationOptions(), rootManifestEvalOptions);
      return this._eoEvaluator.rootOptions;
    }
    return this.getRootEoEvaluator().forForeignContract(rootManifestEvalOptions);
  }

  private async getRemoteContractState(contractId: string): Promise<SortKeyCacheResult<EvalStateResult<State>>> {
    if (this.hasDreState(contractId)) {
      return this.getDreState(contractId);
    } else {
      const dreResponse = await this.fetchRemoteContractState(contractId);
      if (dreResponse != null) {
        return this.setDREState(contractId, dreResponse);
      }
      return null;
    }
  }

  private async fetchRemoteContractState(contractId: string): Promise<DREContractStatusResponse<State> | null> {
    return this._warpFetchWrapper
      .fetch(`${this._evaluationOptions.remoteStateSyncSource}?id=${contractId}&events=false`)
      .then((res) => {
        return res.ok ? res.json() : Promise.reject(res);
      })
      .catch((error) => {
        throw new Error(`Unable to read contract state from DRE. ${error.status}. ${error.body?.message}`);
      });
  }

  private getToSortKey(upToSortKey?: string) {
    if (this._parentContract?.rootSortKey) {
      if (!upToSortKey) {
        return this._parentContract.rootSortKey;
      }
      return this._parentContract.rootSortKey.localeCompare(upToSortKey) > 0
        ? this._parentContract.rootSortKey
        : upToSortKey;
    } else {
      return upToSortKey;
    }
  }

  private async createExecutionContextFromTx(
    contractTxId: string,
    transaction: GQLNodeInterface
  ): Promise<ExecutionContext<State, HandlerApi<State>>> {
    const caller = transaction.owner.address;
    const sortKey = transaction.sortKey;

    const baseContext = await this.createExecutionContext(contractTxId, sortKey, true);

    return {
      ...baseContext,
      caller
    };
  }

  private maybeResetRootContract() {
    if (this.isRoot()) {
      this.logger.debug('Clearing call stack for the root contract');
      this._callStack = new ContractCallRecord(this.txId(), 0);
      this._rootSortKey = null;
      this.warp.interactionsLoader.clearCache();
      this._children = [];
      this._interactionState = new ContractInteractionState(this.warp);
      this._dreStates = new Map();
    }
  }

  private async callContract<Input, View = unknown>(
    input: Input,
    interactionType: InteractionType,
    caller?: string,
    sortKey?: string,
    tags: Tags = [],
    transfer: ArTransfer = emptyTransfer,
    strict = false,
    vrf = false,
    sign = true
  ): Promise<InteractionResult<State, View>> {
    this.logger.info('Call contract input', input);
    this.maybeResetRootContract();
    if (!this._signature) {
      this.logger.warn('Wallet not set.');
    }
    const { arweave, stateEvaluator } = this.warp;
    // create execution context
    let executionContext = await this.createExecutionContext(this._contractTxId, sortKey, true);

    const currentBlockData =
      this.warp.environment == 'mainnet' ? await this._arweaveWrapper.warpGwBlock() : await arweave.blocks.getCurrent();

    // add caller info to execution context
    let effectiveCaller;
    if (caller) {
      effectiveCaller = caller;
    } else if (this._signature) {
      effectiveCaller = await this._signature.getAddress();
    } else {
      effectiveCaller = '';
    }

    this.logger.info('effectiveCaller', effectiveCaller);
    executionContext = {
      ...executionContext,
      caller: effectiveCaller
    };

    // eval current state
    const evalStateResult = await stateEvaluator.eval<State>(executionContext);
    this.logger.info('Current state', evalStateResult.cachedValue.state);

    // create interaction transaction
    const interaction: ContractInteraction<Input> = {
      input,
      caller: executionContext.caller,
      interactionType
    };

    this.logger.debug('interaction', interaction);
    const tx = await createInteractionTx(
      arweave,
      sign ? this._signature?.signer : undefined,
      this._contractTxId,
      input,
      tags,
      transfer.target,
      transfer.winstonQty,
      true,
      this.warp.environment === 'testnet'
    );
    const dummyTx = createDummyTx(tx, executionContext.caller, currentBlockData);

    this.logger.debug('Creating sortKey for', {
      blockId: dummyTx.block.id,
      id: dummyTx.id,
      height: dummyTx.block.height
    });

    dummyTx.sortKey = await this._sorter.createSortKey(dummyTx.block.id, dummyTx.id, dummyTx.block.height, true);
    dummyTx.strict = strict;
    if (vrf) {
      Arweave.utils;
      const vrfPlugin = this.warp.maybeLoadPlugin<void, VrfPluginFunctions>('vrf');
      if (vrfPlugin) {
        dummyTx.vrf = vrfPlugin.process().generateMockVrf(dummyTx.sortKey);
      } else {
        this.logger.warn('Cannot generate mock vrf for interaction - no "warp-contracts-plugin-vrf" attached!');
      }
    }

    const handleResult = await this.evalInteraction<Input, View>(
      {
        interaction,
        interactionTx: dummyTx
      },
      executionContext,
      evalStateResult.cachedValue
    );

    if (handleResult.type !== 'ok') {
      this.logger.fatal('Error while interacting with contract', {
        type: handleResult.type,
        error: handleResult.errorMessage
      });
    }

    return handleResult;
  }

  private async doApplyInputOnTx<Input, View = unknown>(
    input: Input,
    interactionTx: GQLNodeInterface,
    interactionType: InteractionType
  ): Promise<InteractionResult<State, View>> {
    this.maybeResetRootContract();

    let evalStateResult: SortKeyCacheResult<EvalStateResult<State>>;

    const executionContext = await this.createExecutionContextFromTx(this._contractTxId, interactionTx);

    if (!this.isRoot() && this.interactionState().has(this.txId())) {
      evalStateResult = new SortKeyCacheResult<EvalStateResult<State>>(
        interactionTx.sortKey,
        this.interactionState().get(this.txId()) as EvalStateResult<State>
      );
    } else {
      evalStateResult = await this.warp.stateEvaluator.eval<State>(executionContext);
      this.interactionState().update(this.txId(), evalStateResult.cachedValue);
    }

    this.logger.debug('callContractForTx - evalStateResult', {
      result: evalStateResult.cachedValue.state,
      txId: this._contractTxId
    });

    const interaction: ContractInteraction<Input> = {
      input,
      caller: this._parentContract.txId(),
      interactionType
    };

    const interactionData: InteractionData<Input> = {
      interaction,
      interactionTx
    };

    const result = await this.evalInteraction<Input, View>(
      interactionData,
      executionContext,
      evalStateResult.cachedValue
    );
    result.originalValidity = evalStateResult.cachedValue.validity;
    result.originalErrorMessages = evalStateResult.cachedValue.errorMessages;

    return result;
  }

  private async evalInteraction<Input, View = unknown>(
    interactionData: InteractionData<Input>,
    executionContext: ExecutionContext<State, HandlerApi<State>>,
    evalStateResult: EvalStateResult<State>
  ) {
    const interactionCall: InteractionCall = this.getCallStack().addInteractionData(interactionData);
    const benchmark = Benchmark.measure();

    await executionContext.handler.initState(evalStateResult.state);

    const result = await executionContext.handler.handle<Input, View>(
      executionContext,
      evalStateResult,
      interactionData
    );

    interactionCall.update({
      cacheHit: false,
      outputState: this._evaluationOptions.stackTrace.saveState ? result.state : undefined,
      executionTime: benchmark.elapsed(true) as number,
      valid: result.type === 'ok',
      errorMessage: result.errorMessage,
      gasUsed: result.gasUsed
    });

    return result;
  }

  parent(): Contract | null {
    return this._parentContract;
  }

  callDepth(): number {
    return this._callDepth;
  }

  evaluationOptions(): EvaluationOptions {
    return this._evaluationOptions;
  }

  lastReadStateStats(): BenchmarkStats {
    return this._benchmarkStats;
  }

  async stateHash(state: State): Promise<string> {
    const jsonState = stringify(state);

    const hash = await Crypto.subtle.digest('SHA-256', Buffer.from(jsonState, 'utf-8'));

    return Buffer.from(hash).toString('hex');
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- params can be anything
  async syncState(externalUrl: string, params?: any): Promise<Contract> {
    const { stateEvaluator } = this.warp;
    const response = await this._warpFetchWrapper
      .fetch(
        `${externalUrl}?${new URLSearchParams({
          id: this._contractTxId,
          ...params
        })}`
      )
      .then((res) => {
        return res.ok ? res.json() : Promise.reject(res);
      })
      .catch((error) => {
        if (error.body?.message) {
          this.logger.error(error.body.message);
        }
        throw new Error(`Unable to retrieve state. ${error.status}: ${error.body?.message}`);
      });

    await stateEvaluator.syncState<State>(this._contractTxId, response.sortKey, response.state, response.validity);

    return this;
  }

  async evolve(newSrcTxId: string, options?: WriteInteractionOptions): Promise<WriteInteractionResponse | null> {
    return await this.writeInteraction({ function: 'evolve', value: newSrcTxId }, options);
  }

  get rootSortKey(): string {
    return this._rootSortKey;
  }

  getRootEoEvaluator(): EvaluationOptionsEvaluator {
    const root = this.getRoot() as HandlerBasedContract<unknown>;
    return root._eoEvaluator;
  }

  isRoot(): boolean {
    return this._parentContract == null;
  }

  async getStorageValues(keys: string[]): Promise<SortKeyCacheResult<Map<string, unknown>>> {
    const lastCached = await this.warp.stateEvaluator.getCache().getLast(this.txId());
    if (lastCached == null) {
      return new SortKeyCacheResult<Map<string, unknown>>(null, new Map());
    }

    const storage = this.warp.kvStorageFactory(this.txId());
    const result: Map<string, unknown> = new Map();
    try {
      await storage.open();
      for (const key of keys) {
        const lastValue = await storage.getLessOrEqual(key, lastCached.sortKey);
        result.set(key, lastValue == null ? null : lastValue.cachedValue);
      }
      return new SortKeyCacheResult<Map<string, unknown>>(lastCached.sortKey, result);
    } finally {
      await storage.close();
    }
  }

  interactionState(): InteractionState {
    return this.getRoot()._interactionState;
  }

  getRoot(): HandlerBasedContract<unknown> {
    let result: Contract = this;
    while (!result.isRoot()) {
      result = result.parent();
    }

    return result as HandlerBasedContract<unknown>;
  }

  private async maybeSyncStateWithRemoteSource(
    remoteState: SortKeyCacheResult<EvalStateResult<State>>,
    upToSortKey: string,
    cachedState: SortKeyCacheResult<EvalStateResult<State>>
  ): Promise<SortKeyCacheResult<EvalStateResult<State>>> {
    const { stateEvaluator } = this.warp;
    if (this.isStateHigherThanAndUpTo(remoteState, cachedState?.sortKey, upToSortKey)) {
      return await stateEvaluator.syncState<State>(
        this._contractTxId,
        remoteState.sortKey,
        remoteState.cachedValue.state,
        remoteState.cachedValue.validity
      );
    }
    return cachedState;
  }

  private isStateHigherThanAndUpTo(
    remoteState: SortKeyCacheResult<EvalStateResult<State>>,
    fromSortKey: string,
    upToSortKey: string
  ) {
    return (
      remoteState &&
      (!upToSortKey || upToSortKey >= remoteState.sortKey) &&
      (!fromSortKey || remoteState.sortKey > fromSortKey)
    );
  }

  setDREState(
    contractTxId: string,
    result: DREContractStatusResponse<State>
  ): SortKeyCacheResult<EvalStateResult<State>> {
    const dreCachedState = new SortKeyCacheResult(
      result.sortKey,
      new EvalStateResult(result.state, {}, result.errorMessages)
    );
    this.getRoot()._dreStates.set(contractTxId, dreCachedState);
    return dreCachedState;
  }

  getDreState(contractTxId: string): SortKeyCacheResult<EvalStateResult<State>> {
    return this.getRoot()._dreStates.get(contractTxId) as SortKeyCacheResult<EvalStateResult<State>>;
  }

  hasDreState(contractTxId: string): boolean {
    return this.getRoot()._dreStates.has(contractTxId);
  }

  // Call contract and verify if there are any internal writes:
  // 1. Evaluate current contract state
  // 2. Apply input as "dry-run" transaction
  // 3. Verify the callStack and search for any "internalWrites" transactions
  // 4. For each found "internalWrite" transaction - generate additional tag:
  // {name: 'InternalWrite', value: callingContractTxId}
  private async discoverInternalWrites<Input>(
    input: Input,
    tags: Tags,
    transfer: ArTransfer,
    strict: boolean,
    vrf: boolean
  ) {
    const handlerResult = await this.callContract(
      input,
      'write',
      undefined,
      undefined,
      tags,
      transfer,
      strict,
      vrf,
      false
    );

    if (strict && handlerResult.type !== 'ok') {
      throw Error('Cannot create interaction: ' + JSON.stringify(handlerResult.error || handlerResult.errorMessage));
    }
    const callStack: ContractCallRecord = this.getCallStack();
    const innerWrites = this._innerWritesEvaluator.eval(callStack);
    this.logger.debug('Input', input);
    this.logger.debug('Callstack', callStack.print());

    innerWrites.forEach((contractTxId) => {
      tags.push({
        name: WARP_TAGS.INTERACT_WRITE,
        value: contractTxId
      });
    });

    this.logger.debug('Tags with inner calls', tags);
  }
}
