/*
 This file is part of GNU Taler
 (C) 2022 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 
 */
/**
 * Messaging for the WebExtensions wallet.  Should contain
 * parts that are specific for WebExtensions, but as little business
 * logic as possible.
 */
/**
 * Imports.
 */
import {
  LogLevel,
  Logger,
  TalerErrorCode,
  WalletDiagnostics,
  WalletNotification,
  getErrorDetailFromException,
  makeErrorDetail,
  setGlobalLogLevelFromString,
  setLogLevelFromString,
} from "@gnu-taler/taler-util";
import {
  ServiceWorkerHttpLib,
  BrowserHttpLib,
} from "@gnu-taler/web-util/browser";
import {
  DbAccess,
  OpenedPromise,
  SetTimeoutTimerAPI,
  SynchronousCryptoWorkerFactoryPlain,
  Wallet,
  WalletOperations,
  WalletStoresV1,
  deleteTalerDatabase,
  exportDb,
  importDb,
  openPromise,
  openTalerDatabase,
} from "@gnu-taler/taler-wallet-core";
import {
  MessageFromBackend,
  MessageFromFrontend,
  MessageResponse,
} from "./platform/api.js";
import { platform } from "./platform/background.js";
import { ExtensionOperations } from "./taler-wallet-interaction-loader.js";
import { BackgroundOperations } from "./wxApi.js";
/**
 * Currently active wallet instance.  Might be unloaded and
 * re-instantiated when the database is reset.
 *
 * FIXME:  Maybe move the wallet resetting into the Wallet class?
 */
let currentWallet: Wallet | undefined;
let currentDatabase: DbAccess | undefined;
/**
 * Last version of an outdated DB, if applicable.
 */
let outdatedDbVersion: number | undefined;
const walletInit: OpenedPromise = openPromise();
const logger = new Logger("wxBackend.ts");
async function getDiagnostics(): Promise {
  const manifestData = platform.getWalletWebExVersion();
  const errors: string[] = [];
  let firefoxIdbProblem = false;
  let dbOutdated = false;
  try {
    await walletInit.promise;
  } catch (e) {
    errors.push("Error during wallet initialization: " + e);
    if (
      currentDatabase === undefined &&
      outdatedDbVersion === undefined &&
      platform.isFirefox()
    ) {
      firefoxIdbProblem = true;
    }
  }
  if (!currentWallet) {
    errors.push("Could not create wallet backend.");
  }
  if (!currentDatabase) {
    errors.push("Could not open database");
  }
  if (outdatedDbVersion !== undefined) {
    errors.push(`Outdated DB version: ${outdatedDbVersion}`);
    dbOutdated = true;
  }
  const diagnostics: WalletDiagnostics = {
    walletManifestDisplayVersion: manifestData.version_name || "(undefined)",
    walletManifestVersion: manifestData.version,
    errors,
    firefoxIdbProblem,
    dbOutdated,
  };
  return diagnostics;
}
type BackendHandlerType = {
  [Op in keyof BackgroundOperations]: (
    req: BackgroundOperations[Op]["request"],
  ) => Promise;
};
type ExtensionHandlerType = {
  [Op in keyof ExtensionOperations]: (
    req: ExtensionOperations[Op]["request"],
  ) => Promise;
};
async function resetDb(): Promise {
  await deleteTalerDatabase(indexedDB as any);
  await reinitWallet();
}
async function runGarbageCollector(): Promise {
  const dbBeforeGc = currentDatabase;
  if (!dbBeforeGc) {
    throw Error("no current db before running gc");
  }
  const dump = await exportDb(dbBeforeGc.idbHandle());
  await deleteTalerDatabase(indexedDB as any);
  logger.info("cleaned");
  await reinitWallet();
  logger.info("init");
  const dbAfterGc = currentDatabase;
  if (!dbAfterGc) {
    throw Error("no current db before running gc");
  }
  await importDb(dbAfterGc.idbHandle(), dump);
  logger.info("imported");
}
function freeze(time: number): Promise {
  return new Promise((res, rej) => {
    setTimeout(res, time);
  });
}
async function sum(ns: Array): Promise {
  return ns.reduce((prev, cur) => prev + cur, 0);
}
const extensionHandlers: ExtensionHandlerType = {
  isInjectionEnabled,
};
async function isInjectionEnabled(): Promise {
  const settings = await platform.getSettingsFromStorage();
  return settings.injectTalerSupport === true;
}
const backendHandlers: BackendHandlerType = {
  freeze,
  sum,
  getDiagnostics,
  resetDb,
  runGarbageCollector,
  setLoggingLevel,
};
async function setLoggingLevel({
  tag,
  level,
}: {
  tag?: string;
  level: LogLevel;
}): Promise {
  logger.info(`setting ${tag} to ${level}`);
  if (!tag) {
    setGlobalLogLevelFromString(level);
  } else {
    setLogLevelFromString(tag, level);
  }
}
async function dispatch<
  Op extends WalletOperations | BackgroundOperations | ExtensionOperations,
>(req: MessageFromFrontend & { id: string }): Promise {
  switch (req.channel) {
    case "background": {
      const handler = backendHandlers[req.operation] as (req: any) => any;
      if (!handler) {
        return {
          type: "error",
          id: req.id,
          operation: String(req.operation),
          error: getErrorDetailFromException(
            Error(`unknown background operation`),
          ),
        };
      }
      try {
        const result = await handler(req.payload);
        return {
          type: "response",
          id: req.id,
          operation: String(req.operation),
          result,
        };
      } catch (er) {
        return {
          type: "error",
          id: req.id,
          error: getErrorDetailFromException(er),
          operation: String(req.operation),
        };
      }
    }
    case "extension": {
      const handler = extensionHandlers[req.operation] as (req: any) => any;
      if (!handler) {
        return {
          type: "error",
          id: req.id,
          operation: String(req.operation),
          error: getErrorDetailFromException(
            Error(`unknown extension operation`),
          ),
        };
      }
      try {
        const result = await handler(req.payload);
        return {
          type: "response",
          id: req.id,
          operation: String(req.operation),
          result,
        };
      } catch (er) {
        return {
          type: "error",
          id: req.id,
          error: getErrorDetailFromException(er),
          operation: String(req.operation),
        };
      }
    }
    case "wallet": {
      const w = currentWallet;
      if (!w) {
        return {
          type: "error",
          id: req.id,
          operation: req.operation,
          error: makeErrorDetail(
            TalerErrorCode.WALLET_CORE_NOT_AVAILABLE,
            {},
            "wallet core not available",
          ),
        };
      }
      return await w.handleCoreApiRequest(req.operation, req.id, req.payload);
    }
  }
  const anyReq = req as any;
  return {
    type: "error",
    id: anyReq.id,
    operation: String(anyReq.operation),
    error: getErrorDetailFromException(
      Error(
        `unknown channel ${anyReq.channel}, should be "background", "extension" or "wallet"`,
      ),
    ),
  };
}
async function reinitWallet(): Promise {
  if (currentWallet) {
    currentWallet.stop();
    currentWallet = undefined;
  }
  currentDatabase = undefined;
  // setBadgeText({ text: "" });
  try {
    currentDatabase = await openTalerDatabase(indexedDB as any, reinitWallet);
  } catch (e) {
    logger.error("could not open database", e);
    walletInit.reject(e);
    return;
  }
  let httpLib;
  let cryptoWorker;
  let timer;
  if (platform.useServiceWorkerAsBackgroundProcess()) {
    httpLib = new ServiceWorkerHttpLib();
    cryptoWorker = new SynchronousCryptoWorkerFactoryPlain();
    timer = new SetTimeoutTimerAPI();
  } else {
    httpLib = new BrowserHttpLib();
    // We could (should?) use the BrowserCryptoWorkerFactory here,
    // but right now we don't, to have less platform differences.
    // cryptoWorker = new BrowserCryptoWorkerFactory();
    cryptoWorker = new SynchronousCryptoWorkerFactoryPlain();
    timer = new SetTimeoutTimerAPI();
  }
  const settings = await platform.getSettingsFromStorage();
  logger.info("Setting up wallet");
  const wallet = await Wallet.create(
    currentDatabase,
    httpLib,
    timer,
    cryptoWorker,
    {
      features: {
        allowHttp: settings.walletAllowHttp,
        batchWithdrawal: settings.walletBatchWithdrawal,
      },
    },
  );
  try {
    await wallet.handleCoreApiRequest("initWallet", "native-init", {});
  } catch (e) {
    logger.error("could not initialize wallet", e);
    walletInit.reject(e);
    return;
  }
  wallet.addNotificationListener((message) => {
    logger.info("wallet -> ui", message);
    platform.sendMessageToAllChannels(message);
  });
  platform.keepAlive(() => {
    return wallet.runTaskLoop().catch((e) => {
      logger.error("error during wallet task loop", e);
    });
  });
  // Useful for debugging in the background page.
  if (typeof window !== "undefined") {
    (window as any).talerWallet = wallet;
  }
  currentWallet = wallet;
  return walletInit.resolve();
}
/**
 * Main function to run for the WebExtension backend.
 *
 * Sets up all event handlers and other machinery.
 */
export async function wxMain(): Promise {
  logger.trace("starting");
  const afterWalletIsInitialized = reinitWallet();
  platform.registerReloadOnNewVersion();
  // Handlers for messages coming directly from the content
  // script on the page
  platform.listenToAllChannels(async (message) => {
    //wait until wallet is initialized
    await afterWalletIsInitialized;
    const result = await dispatch(message);
    return result;
  });
  platform.registerAllIncomingConnections();
  try {
    await platform.registerOnInstalled(() => {
      platform.openWalletPage("/welcome");
    });
  } catch (e) {
    console.error(e);
  }
}