/*
 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 <http://www.gnu.org/licenses/>
 */

import {
  CoreApiRequestEnvelope,
  CoreApiResponse,
  Logger,
  OpenedPromise,
  openPromise,
  TalerError,
  WalletNotification,
} from "@gnu-taler/taler-util";
import { connectRpc, JsonMessage } from "@gnu-taler/taler-util/twrpc";
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<CoreApiResponse>;

  /**
   * Close the connection to the remote wallet.
   */
  close(): void;
}

export interface RemoteWalletConnectArgs {
  name?: string;
  socketFilename: string;
  notificationHandler?: (n: WalletNotification) => void;
}

export async function createRemoteWallet(
  args: RemoteWalletConnectArgs,
): Promise<RemoteWallet> {
  let nextRequestId = 1;
  let requestMap: Map<
    string,
    {
      promiseCapability: OpenedPromise<CoreApiResponse>;
    }
  > = new Map();

  const ctx = await connectRpc<RemoteWallet>({
    socketFilename: args.socketFilename,
    onEstablished(connection) {
      const ctx: RemoteWallet = {
        makeCoreApiRequest(operation, payload) {
          const id = `req-${nextRequestId}`;
          nextRequestId += 1;
          const req: CoreApiRequestEnvelope = {
            operation,
            id,
            args: payload,
          };
          const promiseCap = openPromise<CoreApiResponse>();
          requestMap.set(id, {
            promiseCapability: promiseCap,
          });
          connection.sendMessage(req as unknown as JsonMessage);
          return promiseCap.promise;
        },
        close() {
          connection.close();
        },
      };
      return {
        result: ctx,
        onDisconnect() {
          logger.info(`${args.name}: remote wallet disconnected`);
        },
        onMessage(m) {
          // FIXME: use a codec for parsing the response envelope!

          if (typeof m !== "object" || m == null) {
            logger.warn(`${args.name}: message 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(
                `${args.name}: message not understood (no id in response)`,
              );
              return;
            }
            const h = requestMap.get(id);
            if (!h) {
              logger.warn(
                `${args.name}: 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(`${args.name}: message 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<any> {
      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<T>(
    cond: (n: WalletNotification) => T | false | undefined,
  ): Promise<T>;
}

interface NotificationCondEntry<T> {
  condition: (n: WalletNotification) => T | false | undefined;
  promiseCapability: OpenedPromise<T>;
}

/**
 * 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<number, NotificationCondEntry<any>> = new Map();
  function onNotification(n: WalletNotification) {
    condMap.forEach((cond, condKey) => {
      const res = cond.condition(n);
      if (res) {
        cond.promiseCapability.resolve(res);
      }
    });
  }
  function waitForNotificationCond<T>(
    cond: (n: WalletNotification) => T | false | undefined,
  ) {
    const promCap = openPromise<T>();
    condMap.set(nextCondIndex++, {
      condition: cond,
      promiseCapability: promCap,
    });
    return promCap.promise;
  }
  return {
    waitForNotificationCond,
    notify: onNotification,
  };
}
