import * as nt from "nekoton-wasm";
import { BlockchainConfig } from "nekoton-wasm";
import { Address, LT_COLLATOR } from "everscale-inpage-provider";
import { Heap } from "heap-js";
import _ from "lodash";
import { GIVER_ADDRESS_EVER_WALLET, GIVER_BOC_EVER_WALLET, ZERO_ADDRESS } from "./constants";
import { AccountFetcherCallback } from "../types";
import { TychoExecutor } from "@tychosdk/emulator";
import { beginCell, Cell, Dictionary, loadShardAccount, storeShardAccount } from "@ton/core";
import type { ExecutorEmulationResult } from "@ton/sandbox";
import { bocFromShardAccount, parseBlocks, shardAccountFromBoc } from "./utils";

const messageComparator = (a: nt.JsRawMessage, b: nt.JsRawMessage) => LT_COLLATOR.compare(a.lt || "0", b.lt || "0");
const emptyShardAccount = beginCell()
  .store(
    storeShardAccount({
      account: null,
      lastTransactionHash: 0n,
      lastTransactionLt: 0n,
    }),
  )
  .endCell()
  .toBoc()
  .toString("base64");
type ExecutorState = {
  // accounts: { [id: string]: nt.FullContractState };
  accounts: { [id: string]: string };
  // txId -> tx
  transactions: { [id: string]: nt.JsRawTransaction };
  // txId -> trace
  traces: { [id: string]: { parsed: nt.EngineTraceInfo[]; raw: string } };
  // msgHash -> tx_id
  msgToTransaction: { [msgHash: string]: string };
  // address -> tx_ids
  addrToTransactions: { [addr: string]: string[] };
  messageQueue: Heap<nt.JsRawMessage>;
  libs: Dictionary<bigint, Cell>;
};

interface LockliftTransport {
  getBlockchainConfig(): Promise<BlockchainConfig>;
  setExecutor(executor: LockliftExecutor): void;
}

export class LockliftExecutor {
  private traceEnabled = false;
  private state: ExecutorState = {} as ExecutorState;
  private snapshots: { [id: string]: ExecutorState } = {};
  private nonce = 0;
  private blockchainConfig!: string;
  private globalId: number | undefined;
  private clock: nt.ClockWithOffset | undefined;
  private tychoExecutor!: TychoExecutor;
  blockchainLt = 1000n;

  constructor(
    private readonly transport: LockliftTransport,
    private readonly accountFetcherCallback?: AccountFetcherCallback,
  ) {
    this.createInitialBlockchainState();
    transport.setExecutor(this);
  }

  private createInitialBlockchainState() {
    this.state = {
      accounts: {},
      transactions: {},
      msgToTransaction: {},
      addrToTransactions: {},
      traces: {},
      messageQueue: new Heap<nt.JsRawMessage>(messageComparator),
      libs: Dictionary.empty(Dictionary.Keys.BigInt(256), Dictionary.Values.Cell()),
    };
    // set this in order to pass standalone-client checks

    this.state.accounts[ZERO_ADDRESS.toString()] = bocFromShardAccount({
      account: null,
      lastTransactionHash: 0n,
      lastTransactionLt: 0n,
    });

    this.state.accounts[GIVER_ADDRESS_EVER_WALLET] = bocFromShardAccount(
      shardAccountFromBoc(nt.makeFullAccountBoc(GIVER_BOC_EVER_WALLET), 0n, 999999999999999999999999999999n),
    );
  }

  async initialize() {
    const config = await this.transport.getBlockchainConfig();

    this.blockchainConfig = Cell.fromBase64(config.boc).asSlice().loadRef().toBoc().toString("base64");
    this.globalId = Number(config.globalId);
    this.tychoExecutor = await TychoExecutor.create();
  }

  setClock(clock: nt.ClockWithOffset) {
    if (this.clock !== undefined) throw new Error("Clock already set");
    this.clock = clock;
  }

  _setAccount1(address: Address | string, boc: string) {
    this.state.accounts[address.toString()] = boc;
  }

  _setLibrary(key: bigint, cell: Cell) {
    this.state.libs.set(key, cell);
  }

  setAccount(address: Address | string, boc: string, type: "accountStuffBoc" | "fullAccountBoc") {
    const newBoc = type === "accountStuffBoc" ? nt.makeFullAccountBoc(boc) : boc;
    this.state.accounts[address.toString()] = bocFromShardAccount(shardAccountFromBoc(newBoc, 0n));
  }

  async _getAccount(address: Address | string): Promise<string | undefined> {
    return this.state.accounts[address.toString()];
    // ||
    // this.accountFetcherCallback?.(address instanceof Address ? address : new Address(address))
    //   .then(({ boc, type }) => {
    //     if (!boc) throw new Error("Account not found");
    //     this.setAccount(address, boc, type);
    //     return this.state.accounts[address.toString()];
    //   })
    //   .catch(e => {
    //     console.error(`Failed to fetch account ${address.toString()}: ${e.trace}`);
    //     return undefined;
    //   })
  }

  enableTraces() {
    this.traceEnabled = true;
    console.log("VM traces enabled, note that performance is downgraded!");
  }
  disableTraces() {
    this.traceEnabled = false;
  }

  getAccounts(): Record<string, nt.FullContractState> {
    return Object.entries(this.state.accounts).reduce((acc, next) => {
      const [address, account] = next;
      const fullContractState = nt.parseShardAccountBoc(account);
      if (!fullContractState) {
        return acc;
      }
      acc[address] = fullContractState;
      return acc;
    }, {} as Record<string, nt.FullContractState>);
  }

  getTxTrace(txId: string): { parsed: nt.EngineTraceInfo[]; raw: string } | undefined {
    return this.state.traces[txId];
  }

  private saveTransaction(tx: nt.JsRawTransaction, trace: { parsed: nt.EngineTraceInfo[]; raw: string }) {
    this.state.transactions[tx.hash] = tx;
    this.state.msgToTransaction[tx.inMessage.hash] = tx.hash;
    this.state.addrToTransactions[tx.inMessage.dst as string] = [tx.hash].concat(
      this.state.addrToTransactions[tx.inMessage.dst as string] || [],
    );
    this.state.traces[tx.hash] = trace;
  }

  getDstTransaction(msgHash: string): nt.JsRawTransaction | undefined {
    return this.state.transactions[this.state.msgToTransaction[msgHash]];
  }

  getTransaction(id: string): nt.JsRawTransaction | undefined {
    return this.state.transactions[id];
  }

  getTransactions(address: Address | string, fromLt: string, count: number): nt.JsRawTransaction[] {
    const result: nt.JsRawTransaction[] = [];
    for (const txId of this.state.addrToTransactions[address.toString()] || []) {
      const rawTx = this.state.transactions[txId];
      if (Number(rawTx.lt) > Number(fromLt)) continue;
      result.push(rawTx);
      if (result.length >= count) return result;
    }
    return result;
  }

  saveSnapshot(): number {
    this.snapshots[this.nonce] = _.cloneDeep(this.state);
    // postincrement!
    return this.nonce++;
  }

  loadSnapshot(id: number) {
    if (this.snapshots[id] === undefined) {
      throw new Error(`Snapshot ${id} not found`);
    }
    this.state = this.snapshots[id];
  }

  clearSnapshots() {
    this.snapshots = {};
  }

  resetBlockchainState() {
    this.createInitialBlockchainState();
  }

  // process all msgs in queue
  async processQueue() {
    while (this.state.messageQueue.size() > 0) {
      await this.processNextMsg();
    }
  }

  // process msg with lowest lt in queue
  async processNextMsg() {
    const message = this.state.messageQueue.pop() as nt.JsRawMessage;
    // everything is processed
    if (!message) return;
    const receiverAcc = (await this._getAccount(message.dst as string)) || emptyShardAccount;

    const messageCell = Cell.fromBase64(message.boc);

    const now = Math.floor(this.clock!.nowMs / 1000);
    let trace: Array<nt.EngineTraceInfo> = [];
    let res: ExecutorEmulationResult = await this.tychoExecutor.runTransaction({
      config: this.blockchainConfig,
      message: messageCell,
      lt: this.blockchainLt,
      shardAccount: receiverAcc,
      now,
      libs: this.state.libs.size > 0 ? beginCell().storeDictDirect(this.state.libs).endCell() : null,
      debugEnabled: true,
      randomSeed: null,
      verbosity: this.traceEnabled ? "full_location_stack_verbose" : "short",
      ignoreChksig: true,
    });
    if (!res.result.success) {
      console.log("Error in executor: ", res.result.error);
      return;
    }

    const decodedTx = nt.decodeRawTransaction(res.result.transaction);
    if (decodedTx.description.aborted) {
      // run 1 more time with trace on
      res = await this.tychoExecutor.runTransaction({
        config: this.blockchainConfig,
        message: messageCell,
        lt: this.blockchainLt,
        shardAccount: receiverAcc,
        now,
        libs: this.state.libs.size > 0 ? beginCell().storeDictDirect(this.state.libs).endCell() : null,
        debugEnabled: true,
        randomSeed: null,
        verbosity: "full_location_stack_verbose",
        ignoreChksig: true,
      });
    }

    if (res.result.success && res.result.vmLog) {
      trace = parseBlocks(res.result.vmLog);
    }

    if (res.logs || res.debugLogs) {
      console.log("Debug logs: ", res.debugLogs);
    }

    if (!res.result.success) {
      console.log("Error in executor: ", res.result.error);
      return;
    }
    this.blockchainLt += 1000n;
    if (res.result.shardAccount) {
      if (message.dst?.startsWith("-1")) {
        const loadedShardAccount = loadShardAccount(Cell.fromBase64(res.result.shardAccount).asSlice()).account;
        if (loadedShardAccount?.storage.state.type === "active") {
          const libraries = loadedShardAccount.storage.state.state.libraries;
          libraries?.keys().map(el => {
            const lib = libraries.get(el);
            if (lib && lib.public && lib.root) {
              this._setLibrary(el, lib.root);
            }
          });
        }
      }
      this._setAccount1(message.dst as string, res.result.shardAccount);

      this.saveTransaction(decodedTx, {
        parsed: trace,
        raw: res.result.vmLog || "",
      });
      decodedTx.outMessages.map((msg: nt.JsRawMessage) => {
        if (msg.msgType === "ExtOut") return; // event
        this.enqueueMsg(msg);
      });
    }
  }

  // push new message to queue
  enqueueMsg(message: nt.JsRawMessage) {
    this.state.messageQueue.push(message);
  }
}
