/*
 This file is part of GNU Taler
 (C) 2023 Taler Systems S.A.
 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 
 */
import {
  CoreApiRequestEnvelope,
  CoreApiResponse,
  j2s,
  Logger,
  TalerError,
  WalletNotification,
} from "@gnu-taler/taler-util";
import { connectRpc, JsonMessage } from "@gnu-taler/taler-util/twrpc";
import { OpenedPromise, openPromise } from "./index.js";
import { WalletCoreApiClient } from "./wallet-api-types.js";
const logger = new Logger("remote.ts");
export interface RemoteWallet {
  /**
   * Low-level interface for making API requests to wallet-core.
   */
  makeCoreApiRequest(
    operation: string,
    payload: unknown,
  ): Promise;
  /**
   * Close the connection to the remote wallet.
   */
  close(): void;
}
export interface RemoteWalletConnectArgs {
  socketFilename: string;
  notificationHandler?: (n: WalletNotification) => void;
}
export async function createRemoteWallet(
  args: RemoteWalletConnectArgs,
): Promise {
  let nextRequestId = 1;
  let requestMap: Map<
    string,
    {
      promiseCapability: OpenedPromise;
    }
  > = new Map();
  const ctx = await connectRpc({
    socketFilename: args.socketFilename,
    onEstablished(connection) {
      const ctx: RemoteWallet = {
        makeCoreApiRequest(operation, payload) {
          const id = `req-${nextRequestId}`;
          const req: CoreApiRequestEnvelope = {
            operation,
            id,
            args: payload,
          };
          const promiseCap = openPromise();
          requestMap.set(id, {
            promiseCapability: promiseCap,
          });
          connection.sendMessage(req as unknown as JsonMessage);
          return promiseCap.promise;
        },
        close() {
          connection.close();
        },
      };
      return {
        result: ctx,
        onDisconnect() {
          logger.info("remote wallet disconnected");
        },
        onMessage(m) {
          // FIXME: use a codec for parsing the response envelope!
          logger.info(`got message from remote wallet: ${j2s(m)}`);
          if (typeof m !== "object" || m == null) {
            logger.warn("message from wallet not understood (wrong type)");
            return;
          }
          const type = (m as any).type;
          if (type === "response" || type === "error") {
            const id = (m as any).id;
            if (typeof id !== "string") {
              logger.warn(
                "message from wallet not understood (no id in response)",
              );
              return;
            }
            const h = requestMap.get(id);
            if (!h) {
              logger.warn(`no handler registered for response id ${id}`);
              return;
            }
            h.promiseCapability.resolve(m as any);
          } else if (type === "notification") {
            if (args.notificationHandler) {
              args.notificationHandler((m as any).payload);
            }
          } else {
            logger.warn("message from wallet not understood");
          }
        },
      };
    },
  });
  return ctx;
}
/**
 * Get a high-level API client from a remove wallet.
 */
export function getClientFromRemoteWallet(
  w: RemoteWallet,
): WalletCoreApiClient {
  const client: WalletCoreApiClient = {
    async call(op, payload): Promise {
      const res = await w.makeCoreApiRequest(op, payload);
      switch (res.type) {
        case "error":
          throw TalerError.fromUncheckedDetail(res.error);
        case "response":
          return res.result;
      }
    },
  };
  return client;
}
export interface WalletNotificationWaiter {
  notify(wn: WalletNotification): void;
  waitForNotificationCond(
    cond: (n: WalletNotification) => T | false | undefined,
  ): Promise;
}
interface NotificationCondEntry {
  condition: (n: WalletNotification) => T | false | undefined;
  promiseCapability: OpenedPromise;
}
/**
 * Helper that allows creating a promise that resolves when the
 * wallet
 */
export function makeNotificationWaiter(): WalletNotificationWaiter {
  // Bookkeeping for waiting on notification conditions
  let nextCondIndex = 1;
  const condMap: Map> = new Map();
  function onNotification(n: WalletNotification) {
    condMap.forEach((cond, condKey) => {
      const res = cond.condition(n);
      if (res) {
        cond.promiseCapability.resolve(res);
      }
    });
  }
  function waitForNotificationCond(
    cond: (n: WalletNotification) => T | false | undefined,
  ) {
    const promCap = openPromise();
    condMap.set(nextCondIndex++, {
      condition: cond,
      promiseCapability: promCap,
    });
    return promCap.promise;
  }
  return {
    waitForNotificationCond,
    notify: onNotification,
  };
}