/*
 This file is part of GNU Taler
 (C) 2019 GNUnet e.V.
 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 
 */
/**
 * Imports.
 */
import {
  Wallet,
  getDefaultNodeWallet,
  DefaultNodeWalletArgs,
  NodeHttpLib,
  TalerErrorCode,
  makeErrorDetails,
  handleWorkerError,
  handleWorkerMessage,
  HttpRequestLibrary,
  OpenedPromise,
  HttpResponse,
  HttpRequestOptions,
  openPromise,
  Headers,
  CoreApiEnvelope,
  CoreApiResponse,
  CoreApiResponseSuccess,
  WalletNotification,
  WALLET_EXCHANGE_PROTOCOL_VERSION,
  WALLET_MERCHANT_PROTOCOL_VERSION,
  bytesToString,
  stringToBytes,
} from "taler-wallet-core";
import fs from "fs";
export { handleWorkerError, handleWorkerMessage };
export class AndroidHttpLib implements HttpRequestLibrary {
  useNfcTunnel = false;
  private nodeHttpLib: HttpRequestLibrary = new NodeHttpLib();
  private requestId = 1;
  private requestMap: {
    [id: number]: OpenedPromise;
  } = {};
  constructor(private sendMessage: (m: string) => void) {}
  fetch(url: string, opt?: HttpRequestOptions): Promise {
    return this.nodeHttpLib.fetch(url, opt);
  }
  get(url: string, opt?: HttpRequestOptions): Promise {
    if (this.useNfcTunnel) {
      const myId = this.requestId++;
      const p = openPromise();
      this.requestMap[myId] = p;
      const request = {
        method: "get",
        url,
      };
      this.sendMessage(
        JSON.stringify({
          type: "tunnelHttp",
          request,
          id: myId,
        }),
      );
      return p.promise;
    } else {
      return this.nodeHttpLib.get(url, opt);
    }
  }
  postJson(
    url: string,
    body: any,
    opt?: HttpRequestOptions,
  ): Promise {
    if (this.useNfcTunnel) {
      const myId = this.requestId++;
      const p = openPromise();
      this.requestMap[myId] = p;
      const request = {
        method: "postJson",
        url,
        body,
      };
      this.sendMessage(
        JSON.stringify({ type: "tunnelHttp", request, id: myId }),
      );
      return p.promise;
    } else {
      return this.nodeHttpLib.postJson(url, body, opt);
    }
  }
  handleTunnelResponse(msg: any): void {
    const myId = msg.id;
    const p = this.requestMap[myId];
    if (!p) {
      console.error(
        `no matching request for tunneled HTTP response, id=${myId}`,
      );
    }
    const headers = new Headers();
    if (msg.status != 0) {
      const resp: HttpResponse = {
        // FIXME: pass through this URL
        requestUrl: "",
        headers,
        status: msg.status,
        requestMethod: "FIXME",
        json: async () => JSON.parse(msg.responseText),
        text: async () => msg.responseText,
        bytes: async () => {
          throw Error("bytes() not supported for tunnel response");
        },
      };
      p.resolve(resp);
    } else {
      p.reject(new Error(`unexpected HTTP status code ${msg.status}`));
    }
    delete this.requestMap[myId];
  }
}
function sendAkonoMessage(ev: CoreApiEnvelope): void {
  // @ts-ignore
  const sendMessage = globalThis.__akono_sendMessage;
  if (typeof sendMessage !== "function") {
    const errMsg =
      "FATAL: cannot install android wallet listener: akono functions missing";
    console.error(errMsg);
    throw new Error(errMsg);
  }
  const m = JSON.stringify(ev);
  // @ts-ignore
  sendMessage(m);
}
class AndroidWalletMessageHandler {
  walletArgs: DefaultNodeWalletArgs | undefined;
  maybeWallet: Wallet | undefined;
  wp = openPromise();
  httpLib = new NodeHttpLib();
  /**
   * Handle a request from the Android wallet.
   */
  async handleMessage(
    operation: string,
    id: string,
    args: any,
  ): Promise {
    const wrapResponse = (result: unknown): CoreApiResponseSuccess => {
      return {
        type: "response",
        id,
        operation,
        result,
      };
    };
    switch (operation) {
      case "init": {
        this.walletArgs = {
          notifyHandler: async (notification: WalletNotification) => {
            sendAkonoMessage({ type: "notification", payload: notification });
          },
          persistentStoragePath: args.persistentStoragePath,
          httpLib: this.httpLib,
        };
        const w = await getDefaultNodeWallet(this.walletArgs);
        this.maybeWallet = w;
        w.runRetryLoop().catch((e) => {
          console.error("Error during wallet retry loop", e);
        });
        this.wp.resolve(w);
        return wrapResponse({
          supported_protocol_versions: {
            exchange: WALLET_EXCHANGE_PROTOCOL_VERSION,
            merchant: WALLET_MERCHANT_PROTOCOL_VERSION,
          },
        });
      }
      case "getHistory": {
        return wrapResponse({ history: [] });
      }
      case "startTunnel": {
        // this.httpLib.useNfcTunnel = true;
        throw Error("not implemented");
      }
      case "stopTunnel": {
        // this.httpLib.useNfcTunnel = false;
        throw Error("not implemented");
      }
      case "tunnelResponse": {
        // httpLib.handleTunnelResponse(msg.args);
        throw Error("not implemented");
      }
      case "reset": {
        const oldArgs = this.walletArgs;
        this.walletArgs = { ...oldArgs };
        if (oldArgs && oldArgs.persistentStoragePath) {
          try {
            fs.unlinkSync(oldArgs.persistentStoragePath);
          } catch (e) {
            console.error("Error while deleting the wallet db:", e);
          }
          // Prevent further storage!
          this.walletArgs.persistentStoragePath = undefined;
        }
        const wallet = await this.wp.promise;
        wallet.stop();
        this.wp = openPromise();
        this.maybeWallet = undefined;
        const w = await getDefaultNodeWallet(this.walletArgs);
        this.maybeWallet = w;
        w.runRetryLoop().catch((e) => {
          console.error("Error during wallet retry loop", e);
        });
        this.wp.resolve(w);
        return wrapResponse({});
      }
      default: {
        const wallet = await this.wp.promise;
        return await wallet.handleCoreApiRequest(operation, id, args);
      }
    }
  }
}
export function installAndroidWalletListener(): void {
  const handler = new AndroidWalletMessageHandler();
  const onMessage = async (msgStr: any): Promise => {
    if (typeof msgStr !== "string") {
      console.error("expected string as message");
      return;
    }
    const msg = JSON.parse(msgStr);
    const operation = msg.operation;
    if (typeof operation !== "string") {
      console.error(
        "message to android wallet helper must contain operation of type string",
      );
      return;
    }
    const id = msg.id;
    console.log(`android listener: got request for ${operation} (${id})`);
    try {
      const respMsg = await handler.handleMessage(operation, id, msg.args);
      console.log(
        `android listener: sending success response for ${operation} (${id})`,
      );
      sendAkonoMessage(respMsg);
    } catch (e) {
      const respMsg: CoreApiResponse = {
        type: "error",
        id,
        operation,
        error: makeErrorDetails(
          TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
          "unexpected exception",
          {},
        ),
      };
      sendAkonoMessage(respMsg);
      return;
    }
  };
  // @ts-ignore
  globalThis.__akono_onMessage = onMessage;
  console.log("android wallet listener installed");
}