/*
 This file is part of GNU Taler
 (C) 2019 GNUnet e.V.
 (C) 2024 Taler Systems SA

 GNU Taler is free software; you can redistribute it and/or modify it under the
 terms of the GNU General Public License as published by the Free Software
 Foundation; either version 3, or (at your option) any later version.

 GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along with
 GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

/**
 * Entry-point for the wallet under qtart, the QuickJS-based GNU Taler runtime.
 */

/**
 * Imports.
 */
import {
  discoverPolicies,
  getBackupStartState,
  getRecoveryStartState,
  mergeDiscoveryAggregate,
  reduceAction,
} from "@gnu-taler/anastasis-core";
import {
  CoreApiMessageEnvelope,
  CoreApiResponse,
  CoreApiResponseSuccess,
  Logger,
  WalletNotification,
  enableNativeLogging,
  getErrorDetailFromException,
  openPromise,
  performanceNow,
  setGlobalLogLevelFromString,
} from "@gnu-taler/taler-util";
import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
import { qjsOs } from "@gnu-taler/taler-util/qtart";
import { Wallet, createNativeWalletHost2 } from "@gnu-taler/taler-wallet-core";
import {
  testArgon2id,
  testWithFdold,
  testWithGv,
  testWithLocal,
} from "./wallet-qjs-tests.js";

import * as talerUtil from "@gnu-taler/taler-util";
import * as talerUtilHttp from "@gnu-taler/taler-util/http";
import * as talerWalletCore from "@gnu-taler/taler-wallet-core";

setGlobalLogLevelFromString("trace");

const logger = new Logger("taler-wallet-embedded/index.ts");

/**
 * Sends JSON to the host application, i.e. the process that
 * runs the JavaScript interpreter (quickjs / qtart) to run
 * the embedded wallet.
 */
function sendNativeMessage(ev: CoreApiMessageEnvelope): void {
  const m = JSON.stringify(ev);
  qjsOs.postMessageToHost(m);
}

class NativeWalletMessageHandler {
  wp = openPromise<Wallet>();
  httpLib = createPlatformHttpLib();

  /**
   * Handle a request from the native wallet.
   */
  async handleMessage(
    operation: string,
    id: string,
    args: any,
  ): Promise<CoreApiResponse> {
    const wrapSuccessResponse = (result: unknown): CoreApiResponseSuccess => {
      return {
        type: "response",
        id,
        operation,
        result,
      };
    };

    switch (operation) {
      case "init": {
        const wR = await createNativeWalletHost2({
          notifyHandler: async (notification: WalletNotification) => {
            sendNativeMessage({ type: "notification", payload: notification });
          },
          httpLib: this.httpLib,
          cryptoWorkerType: args.cryptoWorkerType,
          ...args,
        });

        if (args.logLevel) {
          setGlobalLogLevelFromString(args.logLevel);
        }

        if (args.useNativeLogging === true) {
          enableNativeLogging();
        }

        const resp = await wR.wallet.handleCoreApiRequest(
          "initWallet",
          "native-init",
          {
            config: args.config ?? {},
          },
        );

        let initResponse: any =
          resp.type == "response" ? resp.result : resp.error;

        this.wp.resolve(wR.wallet);

        return wrapSuccessResponse({
          ...initResponse,
        });
      }

      default: {
        const wallet = await this.wp.promise;
        return await wallet.handleCoreApiRequest(operation, id, args);
      }
    }
  }
}

/**
 * Handle an Anastasis request from the native app.
 */
async function handleAnastasisRequest(
  operation: string,
  id: string,
  args: any,
): Promise<CoreApiResponse> {
  const wrapSuccessResponse = (result: unknown): CoreApiResponseSuccess => {
    return {
      type: "response",
      id,
      operation,
      result,
    };
  };

  let req = args ?? {};

  switch (operation) {
    case "anastasisReduce": {
      let reduceRes = await reduceAction(req.state, req.action, req.args ?? {});
      // For now, this will return "success" even if the wrapped Anastasis
      // response is a ReducerStateError.
      return wrapSuccessResponse(reduceRes);
    }

    case "anastasisStartBackup": {
      return wrapSuccessResponse(await getBackupStartState());
    }

    case "anastasisStartRecovery": {
      return wrapSuccessResponse(await getRecoveryStartState());
    }

    case "anastasisDiscoverPolicies": {
      let discoverRes = await discoverPolicies(req.state, req.cursor);
      let aggregatedPolicies = mergeDiscoveryAggregate(
        discoverRes.policies ?? [],
        req.state.discoveryState?.aggregatedPolicies ?? [],
      );
      return wrapSuccessResponse({
        ...req.state,
        discoveryState: {
          state: "finished",
          aggregatedPolicies,
          cursor: discoverRes.cursor,
        },
      });
    }

    default: {
      throw Error("unsupported anastasis operation");
    }
  }
}

export function installNativeWalletListener(): void {
  setGlobalLogLevelFromString("trace");

  const handler = new NativeWalletMessageHandler();

  const onMessage = async (msgStr: any): Promise<void> => {
    if (typeof msgStr !== "string") {
      logger.error("expected string as message");
      return;
    }

    const msg = JSON.parse(msgStr);
    const operation = msg.operation;
    if (typeof operation !== "string") {
      logger.error(
        "message to native wallet helper must contain operation of type string",
      );
      return;
    }

    const id = msg.id;
    logger.trace(`native listener: got request for ${operation} (${id})`);

    const startTimeMs = performanceNow();
    let respMsg: CoreApiResponse;

    try {
      if (msg.operation.startsWith("anastasis")) {
        // Entry point for Anastasis
        respMsg = await handleAnastasisRequest(operation, id, msg.args ?? {});
      } else {
        // Entry point for wallet-core
        respMsg = await handler.handleMessage(operation, id, msg.args ?? {});
      }
    } catch (e) {
      respMsg = {
        type: "error",
        id,
        operation,
        error: getErrorDetailFromException(e),
      };
    }

    const endTimeMs = performanceNow();
    const requestDurationMs = Math.round(
      Number((endTimeMs - startTimeMs) / 1000n / 1000n),
    );

    logger.trace(
      `native listener: sending ${respMsg.type} for ${operation} (${id}) after ${requestDurationMs} ms`,
    );
    sendNativeMessage(respMsg);
  };

  qjsOs.setMessageFromHostHandler((m) => onMessage(m));

  logger.info("native wallet listener installed");
}

// @ts-ignore
globalThis.installNativeWalletListener = installNativeWalletListener;
// @ts-ignore
globalThis.testWithGv = testWithGv;
// @ts-ignore
globalThis.testWithLocal = testWithLocal;
// @ts-ignore
globalThis.testArgon2id = testArgon2id;
// @ts-ignore
globalThis.testReduceAction = reduceAction;
// @ts-ignore
globalThis.testDiscoverPolicies = discoverPolicies;
// @ts-ignore
globalThis.testWithFdold = testWithFdold;

// @ts-ignore
globalThis.talerModules = {
  talerUtil,
  talerWalletCore,
  talerUtilHttp,
};
