import wasm from "../dist/wasm.js";
import { MethodName, WorkerAction } from "./constants.js";

const {
  Account,
  AccountHeader,
  AccountId,
  AccountStorageMode,
  AdviceMap,
  AuthSecretKey,
  ConsumableNoteRecord,
  Felt,
  FeltArray,
  FungibleAsset,
  InputNoteState,
  Note,
  NoteAssets,
  NoteConsumability,
  NoteExecutionHint,
  NoteExecutionMode,
  NoteFilter,
  NoteFilterTypes,
  NoteId,
  NoteIdAndArgs,
  NoteIdAndArgsArray,
  NoteInputs,
  NoteMetadata,
  NoteRecipient,
  NoteScript,
  NoteTag,
  NoteType,
  OutputNote,
  OutputNotesArray,
  Rpo256,
  TestUtils,
  TransactionFilter,
  TransactionProver,
  TransactionRequest,
  TransactionResult,
  TransactionRequestBuilder,
  TransactionScriptInputPair,
  TransactionScriptInputPairArray,
  Word,
  WebClient: WasmWebClient, // Alias the WASM-exported WebClient
} = wasm;

export {
  Account,
  AccountHeader,
  AccountId,
  AccountStorageMode,
  AdviceMap,
  AuthSecretKey,
  ConsumableNoteRecord,
  Felt,
  FeltArray,
  FungibleAsset,
  InputNoteState,
  Note,
  NoteAssets,
  NoteConsumability,
  NoteExecutionHint,
  NoteExecutionMode,
  NoteFilter,
  NoteFilterTypes,
  NoteId,
  NoteIdAndArgs,
  NoteIdAndArgsArray,
  NoteInputs,
  NoteMetadata,
  NoteRecipient,
  NoteScript,
  NoteTag,
  NoteType,
  OutputNote,
  OutputNotesArray,
  Rpo256,
  TestUtils,
  TransactionFilter,
  TransactionProver,
  TransactionRequest,
  TransactionResult,
  TransactionRequestBuilder,
  TransactionScriptInputPair,
  TransactionScriptInputPairArray,
  Word,
};

/**
 * WebClient is a wrapper around the underlying WASM WebClient object.
 *
 * This wrapper serves several purposes:
 *
 * 1. It creates a dedicated web worker to offload computationally heavy tasks
 *    (such as creating accounts, executing transactions, submitting transactions, etc.)
 *    from the main thread, helping to prevent UI freezes in the browser.
 *
 * 2. It defines methods that mirror the API of the underlying WASM WebClient,
 *    with the intention of executing these functions via the web worker. This allows us
 *    to maintain the same API and parameters while benefiting from asynchronous, worker-based computation.
 *
 * 3. It employs a Proxy to forward any calls not designated for web worker computation
 *    directly to the underlying WASM WebClient instance.
 *
 * Additionally, the wrapper provides a static create_client function. This static method
 * instantiates the WebClient object and ensures that the necessary create_client calls are
 * performed both in the main thread and within the worker thread. This dual initialization
 * correctly passes user parameters (RPC URL and seed) to both the main-thread
 * WASM WebClient and the worker-side instance.
 *
 * Because of this implementation, the only breaking change for end users is in the way the
 * web client is instantiated. Users should now use the WebClient.create_client static call.
 */
export class WebClient {
  constructor(rpcUrl, seed) {
    this.rpcUrl = rpcUrl;
    this.seed = seed;

    // Check if Web Workers are available.
    if (typeof Worker !== "undefined") {
      console.log("WebClient: Web Workers are available.");
      // Create the worker.
      this.worker = new Worker(
        new URL("./workers/web-client-methods-worker.js", import.meta.url),
        { type: "module" }
      );

      // Map to track pending worker requests.
      this.pendingRequests = new Map();

      // Promises to track when the worker script is loaded and ready.
      this.loaded = new Promise((resolve) => {
        this.loadedResolver = resolve;
      });

      // Create a promise that resolves when the worker signals that it is fully initialized.
      this.ready = new Promise((resolve) => {
        this.readyResolver = resolve;
      });

      // Listen for messages from the worker.
      this.worker.addEventListener("message", (event) => {
        const data = event.data;

        // Worker script loaded.
        if (data.loaded) {
          this.loadedResolver();
          return;
        }

        // Worker ready.
        if (data.ready) {
          this.readyResolver();
          return;
        }

        // Handle responses for method calls.
        const { requestId, error, result, methodName } = data;
        if (requestId && this.pendingRequests.has(requestId)) {
          const { resolve, reject } = this.pendingRequests.get(requestId);
          this.pendingRequests.delete(requestId);
          if (error) {
            console.error(
              `WebClient: Error from worker in ${methodName}:`,
              error
            );
            reject(new Error(error));
          } else {
            resolve(result);
          }
        }
      });

      // Once the worker script has loaded, initialize the worker.
      this.loaded.then(() => {
        this.worker.postMessage({
          action: WorkerAction.INIT,
          args: [this.rpcUrl, this.seed],
        });
      });
    } else {
      console.log("WebClient: Web Workers are not available.");
      // Worker not available; set up fallback values.
      this.worker = null;
      this.pendingRequests = null;
      this.loaded = Promise.resolve();
      this.ready = Promise.resolve();
    }

    // Create the underlying WASM WebClient.
    this.wasmWebClient = new WasmWebClient();
  }

  /**
   * Factory method to create and initialize a WebClient instance.
   * This method is async so you can await the asynchronous call to create_client().
   *
   * @param {string} rpcUrl - The RPC URL.
   * @param {string} seed - The seed for the account.
   * @returns {Promise<WebClient>} The fully initialized WebClient.
   */
  static async create_client(rpcUrl, seed) {
    // Construct the instance (synchronously).
    const instance = new WebClient(rpcUrl, seed);

    // Wait for the underlying wasmWebClient to be initialized.
    await instance.wasmWebClient.create_client(rpcUrl, seed);

    // Wait for the worker to be ready
    await instance.ready;

    // Return a proxy that forwards missing properties to wasmWebClient.
    return new Proxy(instance, {
      get(target, prop, receiver) {
        // If the property exists on the wrapper, return it.
        if (prop in target) {
          return Reflect.get(target, prop, receiver);
        }
        // Otherwise, if the wasmWebClient has it, return that.
        if (target.wasmWebClient && prop in target.wasmWebClient) {
          const value = target.wasmWebClient[prop];
          if (typeof value === "function") {
            return value.bind(target.wasmWebClient);
          }
          return value;
        }
        return undefined;
      },
    });
  }

  /**
   * Call a method via the worker.
   * @param {string} methodName - Name of the method to call.
   * @param  {...any} args - Arguments for the method.
   * @returns {Promise<any>}
   */
  async callMethodWithWorker(methodName, ...args) {
    await this.ready;
    // Create a unique request ID.
    const requestId = `${methodName}-${Date.now()}-${Math.random()}`;
    return new Promise((resolve, reject) => {
      // Save the resolve and reject callbacks in the pendingRequests map.
      this.pendingRequests.set(requestId, { resolve, reject });
      // Send the method call request to the worker.
      this.worker.postMessage({
        action: WorkerAction.CALL_METHOD,
        methodName,
        args,
        requestId,
      });
    });
  }

  // ----- Explicitly Wrapped Methods (Worker-Forwarded) -----

  async new_wallet(storageMode, mutable) {
    try {
      if (!this.worker) {
        return await this.wasmWebClient.new_wallet(storageMode, mutable);
      }
      const serializedStorageMode = storageMode.as_str();
      const serializedAccountBytes = await this.callMethodWithWorker(
        MethodName.NEW_WALLET,
        serializedStorageMode,
        mutable
      );
      return wasm.Account.deserialize(new Uint8Array(serializedAccountBytes));
    } catch (error) {
      console.error("INDEX.JS: Error in new_wallet:", error);
      throw error;
    }
  }

  async new_faucet(storageMode, nonFungible, tokenSymbol, decimals, maxSupply) {
    try {
      if (!this.worker) {
        return await this.wasmWebClient.new_faucet(
          storageMode,
          nonFungible,
          tokenSymbol,
          decimals,
          maxSupply
        );
      }
      const serializedStorageMode = storageMode.as_str();
      const serializedMaxSupply = maxSupply.toString();
      const serializedAccountBytes = await this.callMethodWithWorker(
        MethodName.NEW_FAUCET,
        serializedStorageMode,
        nonFungible,
        tokenSymbol,
        decimals,
        serializedMaxSupply
      );

      return wasm.Account.deserialize(new Uint8Array(serializedAccountBytes));
    } catch (error) {
      console.error("INDEX.JS: Error in new_faucet:", error);
      throw error;
    }
  }

  async new_transaction(accountId, transactionRequest) {
    try {
      if (!this.worker) {
        return await this.wasmWebClient.new_transaction(
          accountId,
          transactionRequest
        );
      }
      const serializedAccountId = accountId.to_string();
      const serializedTransactionRequest = transactionRequest.serialize();
      const serializedTransactionResultBytes = await this.callMethodWithWorker(
        MethodName.NEW_TRANSACTION,
        serializedAccountId,
        serializedTransactionRequest
      );
      return wasm.TransactionResult.deserialize(
        new Uint8Array(serializedTransactionResultBytes)
      );
    } catch (error) {
      console.error("INDEX.JS: Error in new_transaction:", error);
      throw error;
    }
  }

  async new_mint_transaction(targetAccountId, faucetId, noteType, amount) {
    try {
      if (!this.worker) {
        return await this.wasmWebClient.new_mint_transaction(
          targetAccountId,
          faucetId,
          noteType,
          amount
        );
      }
      const serializedTargetAccountId = targetAccountId.to_string();
      const serializedFaucetId = faucetId.to_string();
      const serializedNoteType = noteType.serialize();
      const serializedAmount = amount.toString();
      const serializedTransactionResultBytes = await this.callMethodWithWorker(
        MethodName.NEW_MINT_TRANSACTION,
        serializedTargetAccountId,
        serializedFaucetId,
        serializedNoteType,
        serializedAmount
      );
      return wasm.TransactionResult.deserialize(
        new Uint8Array(serializedTransactionResultBytes)
      );
    } catch (error) {
      console.error("INDEX.JS: Error in new_mint_transaction:", error);
      throw error; // Ensure the test catches and asserts
    }
  }

  async new_consume_transaction(targetAccountId, noteId) {
    try {
      if (!this.worker) {
        return await this.wasmWebClient.new_consume_transaction(
          targetAccountId,
          noteId
        );
      }
      const serializedTargetAccountId = targetAccountId.to_string();
      const serializedTransactionResultBytes = await this.callMethodWithWorker(
        MethodName.NEW_CONSUME_TRANSACTION,
        serializedTargetAccountId,
        noteId
      );
      return wasm.TransactionResult.deserialize(
        new Uint8Array(serializedTransactionResultBytes)
      );
    } catch (error) {
      console.error(
        "INDEX.JS: Error in consume_transaction:",
        JSON.stringify(error)
      );
      throw error;
    }
  }

  async new_send_transaction(
    senderAccountId,
    receiverAccountId,
    faucetId,
    noteType,
    amount,
    recallHeight = null
  ) {
    try {
      if (!this.worker) {
        return await this.wasmWebClient.new_send_transaction(
          senderAccountId,
          receiverAccountId,
          faucetId,
          noteType,
          amount,
          recallHeight
        );
      }
      const serializedSenderAccountId = senderAccountId.to_string();
      const serializedReceiverAccountId = receiverAccountId.to_string();
      const serializedFaucetId = faucetId.to_string();
      const serializedNoteType = noteType.serialize();
      const serializedAmount = amount.toString();
      const serializedTransactionResultBytes = await this.callMethodWithWorker(
        MethodName.NEW_SEND_TRANSACTION,
        serializedSenderAccountId,
        serializedReceiverAccountId,
        serializedFaucetId,
        serializedNoteType,
        serializedAmount,
        recallHeight
      );
      return wasm.TransactionResult.deserialize(
        new Uint8Array(serializedTransactionResultBytes)
      );
    } catch (error) {
      console.error("INDEX.JS: Error in send_transaction:", error);
      throw error;
    }
  }

  async submit_transaction(transactionResult, prover = undefined) {
    try {
      if (!this.worker) {
        return await this.wasmWebClient.submit_transaction(
          transactionResult,
          prover
        );
      }
      const serializedTransactionResult = transactionResult.serialize();
      const args = [serializedTransactionResult];

      // If a prover is provided, serialize it and add it to the args.
      if (prover) {
        args.push(prover.serialize());
      }

      // Always call the same worker method.
      await this.callMethodWithWorker(MethodName.SUBMIT_TRANSACTION, ...args);
    } catch (error) {
      console.error("INDEX.JS: Error in submit_transaction:", error);
      throw error;
    }
  }

  async sync_state() {
    try {
      if (!this.worker) {
        return await this.wasmWebClient.sync_state();
      }
      const serializedSyncSummaryBytes = await this.callMethodWithWorker(
        MethodName.SYNC_STATE
      );
      return wasm.SyncSummary.deserialize(
        new Uint8Array(serializedSyncSummaryBytes)
      );
    } catch (error) {
      console.error("INDEX.JS: Error in sync_state:", error);
      throw error;
    }
  }

  terminate() {
    this.worker.terminate();
  }
}