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

/**
 * Messaging for the WebExtensions wallet.  Should contain
 * parts that are specific for WebExtensions, but as little business
 * logic as possible.
 */

/**
 * Imports.
 */
import {
  AbsoluteTime,
  CoreApiResponse,
  LogLevel,
  Logger,
  NotificationType,
  OpenedPromise,
  SetTimeoutTimerAPI,
  TalerError,
  TalerErrorCode,
  TalerErrorDetail,
  TransactionMinorState,
  WalletNotification,
  getErrorDetailFromException,
  makeErrorDetail,
  openPromise,
  setGlobalLogLevelFromString,
  setLogLevelFromString,
} from "@gnu-taler/taler-util";
import { HttpRequestLibrary } from "@gnu-taler/taler-util/http";
import {
  DbAccess,
  HttpFactory,
  SynchronousCryptoWorkerFactoryPlain,
  Wallet,
  WalletApiOperation,
  WalletOperations,
  WalletStoresV1,
  deleteTalerDatabase,
  exportDb,
  importDb,
} from "@gnu-taler/taler-wallet-core";
import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser";
import { BridgeIDBFactory } from "../../idb-bridge/src/bridge-idb.js";
import { MessageFromFrontend } from "./platform/api.js";
import { platform } from "./platform/background.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<typeof WalletStoresV1> | undefined;

const walletInit: OpenedPromise<void> = openPromise<void>();

const logger = new Logger("wxBackend.ts");

export interface ExtensionOperations {
  getInjectionSettings: {
    request: void;
    response: {
      autoOpen: boolean;
      injectTalerSupport: boolean;
    };
  };
}

export type MessageFromExtension<Op extends keyof ExtensionOperations> = {
  channel: "extension";
  operation: Op;
  payload: ExtensionOperations[Op]["request"];
};

export type MessageResponse = CoreApiResponse;

type BackendHandlerType = {
  [Op in keyof BackgroundOperations]: (
    req: BackgroundOperations[Op]["request"],
  ) => Promise<BackgroundOperations[Op]["response"]>;
};

type ExtensionHandlerType = {
  [Op in keyof ExtensionOperations]: (
    req: ExtensionOperations[Op]["request"],
  ) => Promise<ExtensionOperations[Op]["response"]>;
};

async function resetDb(): Promise<void> {
  await deleteTalerDatabase(indexedDB as any);
  await reinitWallet();
}

export type WalletActivityTrack = {
  // id: number;
  events: (WalletNotification & { when: AbsoluteTime })[];
  start: AbsoluteTime;
  type: NotificationType;
  end: AbsoluteTime;
  groupId: string;
  requestId?: string; //only for request event
};

//FIXME: maybe circular buffer
const activity: WalletActivityTrack[] = [];

function convertWalletActivityNotification(
  knownEvents: WalletActivityTrack[],
  event: WalletNotification & {
    when: AbsoluteTime;
  },
): WalletActivityTrack | undefined {
  switch (event.type) {
    case NotificationType.BalanceChange: {
      const groupId = `${event.type}:${event.hintTransactionId}`;
      const found = knownEvents.find((a) => a.groupId === groupId);
      if (found) {
        found.end = event.when;
        found.events.unshift(event);
        return undefined;
      }
      return {
        type: event.type,
        start: event.when,
        end: AbsoluteTime.never(),
        events: [event],
        groupId,
      };
    }
    case NotificationType.BackupOperationError: {
      const groupId = "";
      return {
        type: event.type,
        start: event.when,
        end: AbsoluteTime.never(),
        events: [event],
        groupId,
      };
    }
    case NotificationType.TransactionStateTransition: {
      const groupId = `${event.type}:${event.transactionId}`;
      const found = knownEvents.find((a) => a.groupId === groupId);
      if (found) {
        found.end = event.when;
        found.events.unshift(event);
        return undefined;
      }
      return {
        type: event.type,
        start: event.when,
        end: AbsoluteTime.never(),
        events: [event],
        groupId,
      };
    }
    case NotificationType.ExchangeStateTransition: {
      const groupId = `${event.type}:${event.exchangeBaseUrl}`;
      const found = knownEvents.find((a) => a.groupId === groupId);
      if (found) {
        found.end = event.when;
        found.events.unshift(event);
        return undefined;
      }
      return {
        type: event.type,
        start: event.when,
        end: AbsoluteTime.never(),
        events: [event],
        groupId,
      };
    }
    case NotificationType.Idle: {
      const groupId = "";
      return {
        type: event.type,
        start: event.when,
        end: AbsoluteTime.never(),
        events: [event],
        groupId,
      };
    }
    case NotificationType.TaskObservabilityEvent: {
      const groupId = `${event.type}:${event.taskId}`;
      const found = knownEvents.find((a) => a.groupId === groupId);
      if (found) {
        found.end = event.when;
        found.events.unshift(event);
        return undefined;
      }
      return {
        type: event.type,
        start: event.when,
        end: AbsoluteTime.never(),
        events: [event],
        groupId,
      };
    }
    case NotificationType.RequestObservabilityEvent: {
      const groupId = `${event.type}:${event.operation}:${event.requestId}`;
      const found = knownEvents.find((a) => a.groupId === groupId);
      if (found) {
        found.end = event.when;
        found.events.unshift(event);
        return undefined;
      }
      return {
        type: event.type,
        start: event.when,
        end: AbsoluteTime.never(),
        events: [event],
        groupId,
        requestId: event.requestId,
      };
    }
  }
  return undefined;
}

function addNewWalletActivityNotification(
  list: WalletActivityTrack[],
  n: WalletNotification,
) {
  const start = AbsoluteTime.now();
  const ev = { ...n, when: start };
  const activity = convertWalletActivityNotification(list, ev);
  if (activity) {
    list.unshift(activity); // insert at start
  }
}

async function getNotifications({
  filter,
  operationFrom,
}: {
  filter: string;
  operationFrom?: string;
}): Promise<WalletActivityTrack[]> {
  const rg = new RegExp(`.*${filter}.*`);
  return activity.filter((event) => {
    if (operationFrom) {
      if (event.type !== NotificationType.RequestObservabilityEvent) {
        return false;
      }
      if (event.requestId) {
        return event.requestId.startsWith(operationFrom);
      }
    }
    if (!filter) return true;
    const testFilter = rg.test(event.groupId.toLowerCase());
    return testFilter;
  });
}

async function clearNotifications(): Promise<void> {
  activity.splice(0, activity.length);
}

async function runGarbageCollector(): Promise<void> {
  const dbBeforeGc = currentDatabase;
  if (!dbBeforeGc) {
    throw Error("no current db before running gc");
  }
  const dump = await exportDb(indexedDB as any);

  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");
}

const extensionHandlers: ExtensionHandlerType = {
  getInjectionSettings,
};

async function getInjectionSettings(): Promise<{
  autoOpen: boolean;
  injectTalerSupport: boolean;
}> {
  const settings = await platform.getSettingsFromStorage();
  return {
    autoOpen: settings.autoOpen,
    injectTalerSupport: settings.injectTalerSupport,
  };
}

const backendHandlers: BackendHandlerType = {
  resetDb,
  runGarbageCollector,
  getNotifications,
  clearNotifications,
  reinitWallet,
  setLoggingLevel,
};

async function setLoggingLevel({
  tag,
  level,
}: {
  tag?: string;
  level: LogLevel;
}): Promise<void> {
  logger.info(`setting ${tag} to ${level}`);
  if (!tag) {
    setGlobalLogLevelFromString(level);
  } else {
    setLogLevelFromString(tag, level);
  }
}
let nextMessageIndex = 0;

async function dispatch<
  Op extends WalletOperations | BackgroundOperations | ExtensionOperations,
>(
  req: MessageFromFrontend<Op> & { id: string },
  from: string,
): Promise<MessageResponse> {
  nextMessageIndex = (nextMessageIndex + 1) % (Number.MAX_SAFE_INTEGER - 100);

  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) {
        const lastError: TalerErrorDetail =
          walletInit.lastError instanceof TalerError
            ? walletInit.lastError.errorDetail
            : undefined;

        return {
          type: "error",
          id: req.id,
          operation: req.operation,
          error: makeErrorDetail(
            TalerErrorCode.WALLET_CORE_NOT_AVAILABLE,
            { lastError },
            `wallet core not available${
              !lastError ? "" : `,last error: ${lastError.hint}`
            }`,
          ),
        };
      }
      //multiple client can create the same id, send the wallet an unique key
      const newId = `${from}:${req.id}`;
      const resp = await w.handleCoreApiRequest(
        req.operation,
        newId,
        req.payload,
      );
      //return to the client the original id
      resp.id = req.id;
      return resp;
    }
  }

  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<void> {
  if (currentWallet) {
    await currentWallet.client.call(WalletApiOperation.Shutdown, {});
    currentWallet = undefined;
  }
  currentDatabase = undefined;
  // setBadgeText({ text: "" });
  let cryptoWorker;
  let timer;

  const httpFactory = (): HttpRequestLibrary => {
    return new BrowserFetchHttpLib({
      // enableThrottling: false,
    });
  };

  if (platform.useServiceWorkerAsBackgroundProcess()) {
    cryptoWorker = new SynchronousCryptoWorkerFactoryPlain();
    timer = new SetTimeoutTimerAPI();
  } else {
    // 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(
    {
      idbFactory: indexedDB as any as BridgeIDBFactory,
      readBackupJson(path) {
        throw Error("DB file export not supported for platform webext");
      },
      exportToFile() {
        throw Error("DB file export not supported for platform webext");
      },
      getStats() {
        throw Error("DB statistics not supported for platform webext");
      },
    },
    httpFactory as any as HttpFactory,
    timer,
    cryptoWorker,
  );
  try {
    await wallet.handleCoreApiRequest("initWallet", "native-init", {
      config: {
        testing: {
          emitObservabilityEvents: settings.showWalletActivity,
          devModeActive: settings.advancedMode,
        },
        features: {
          allowHttp: settings.walletAllowHttp,
          enableV1Contracts: settings.walletEnableV1Contracts,
        },
      },
    });
  } catch (e) {
    logger.error("could not initialize wallet", e);
    walletInit.reject(e);
    return;
  }
  wallet.addNotificationListener((message) => {
    logger.trace("Wallet -> Webex", message);
    if (settings.showWalletActivity) {
      addNewWalletActivityNotification(activity, message);
    }

    processWalletNotification(message);

    platform.sendMessageToAllChannels({
      type: "wallet",
      notification: message,
    });
  });

  // Useful for debugging in the background page.
  if (typeof window !== "undefined") {
    (window as any).talerWallet = wallet;
  }
  currentWallet = wallet;
  updateIconBasedOnBalance();
  return walletInit.resolve();
}

/**
 * Main function to run for the WebExtension backend.
 *
 * Sets up all event handlers and other machinery.
 */
export async function wxMain(): Promise<void> {
  logger.trace("starting");
  const afterWalletIsInitialized = reinitWallet();

  logger.trace("reload on new version");
  platform.registerReloadOnNewVersion();

  // Handlers for messages coming directly from the content
  // script on the page
  logger.trace("listen all channels");
  platform.listenToAllChannels(async (message, from) => {
    //wait until wallet is initialized
    logger.trace("Webex -> Wallet", message);
    await afterWalletIsInitialized;
    const result = await dispatch(message, from);
    return result;
  });

  logger.trace("register all incoming connections");
  platform.registerAllIncomingConnections();

  logger.trace("redirect if first start");
  try {
    platform.registerOnInstalled(() => {
      platform.openWalletPage("/welcome");
    });
  } catch (e) {
    console.error(e);
  }
}

async function updateIconBasedOnBalance() {
  const balance = await currentWallet?.client.call(
    WalletApiOperation.GetBalances,
    {},
  );
  if (balance) {
    let showAlert = false;
    for (const b of balance.balances) {
      if (b.flags.length > 0) {
        console.log("b.flags", JSON.stringify(b.flags));
        showAlert = true;
        break;
      }
    }

    if (showAlert) {
      platform.setAlertedIcon();
    } else {
      platform.setNormalIcon();
    }
  }
}

/**
 * All the actions triggered by notification that need to be
 * run in the background.
 *
 * @param message
 */
async function processWalletNotification(message: WalletNotification) {
  if (
    message.type === NotificationType.TransactionStateTransition &&
    (message.newTxState.minor === TransactionMinorState.KycRequired ||
      message.oldTxState.minor === TransactionMinorState.KycRequired ||
      message.newTxState.minor === TransactionMinorState.BankConfirmTransfer ||
      message.oldTxState.minor === TransactionMinorState.BankConfirmTransfer)
  ) {
    await updateIconBasedOnBalance();
  }
}
