/*
 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/>
 */

/**
 * Interface to the wallet through WebExtension messaging.
 */

/**
 * Imports.
 */
import {
  AbsoluteTime,
  CoreApiResponse,
  DetailsMap,
  Logger,
  LogLevel,
  NotificationType,
  TalerError,
  TalerErrorCode,
  TalerErrorDetail,
  WalletNotification,
} from "@gnu-taler/taler-util";
import {
  WalletCoreApiClient,
  WalletCoreOpKeys,
  WalletCoreRequestType,
  WalletCoreResponseType,
} from "@gnu-taler/taler-wallet-core";
import {
  ExtensionNotification,
  MessageFromBackend,
  MessageFromFrontendBackground,
  MessageFromFrontendWallet,
} from "./platform/api.js";
import { platform } from "./platform/foreground.js";
import { WalletActivityTrack } from "./wxBackend.js";

/**
 *
 * @author sebasjm
 */

const logger = new Logger("wxApi");

export const WALLET_CORE_SUPPORTED_VERSION = "7:0:0";

export interface ExtendedPermissionsResponse {
  newValue: boolean;
}

export interface BackgroundOperations {
  resetDb: {
    request: void;
    response: void;
  };
  runGarbageCollector: {
    request: void;
    response: void;
  };
  reinitWallet: {
    request: void;
    response: void;
  };
  getNotifications: {
    request: {
      filter: string;
      operationsFrom?: string;
    };
    response: WalletActivityTrack[];
  };
  clearNotifications: {
    request: void;
    response: void;
  };
  setLoggingLevel: {
    request: {
      tag?: string;
      level: LogLevel;
    };
    response: void;
  };
}

export type WalletEvent = {
  notification: WalletNotification;
  when: AbsoluteTime;
};

export interface BackgroundApiClient {
  call<Op extends keyof BackgroundOperations>(
    operation: Op,
    payload: BackgroundOperations[Op]["request"],
  ): Promise<BackgroundOperations[Op]["response"]>;
}

export class BackgroundError<T = any> extends Error {
  public readonly errorDetail: TalerErrorDetail & T;
  public readonly cause: Error;

  constructor(title: string, e: TalerErrorDetail & T, cause: Error) {
    super(title);
    this.errorDetail = e;
    this.cause = cause;
  }

  hasErrorCode<C extends keyof DetailsMap>(
    code: C,
  ): this is BackgroundError<DetailsMap[C]> {
    return this.errorDetail.code === code;
  }
}

/**
 * BackgroundApiClient integration with browser platform
 */
class BackgroundApiClientImpl implements BackgroundApiClient {
  async call<Op extends keyof BackgroundOperations>(
    operation: Op,
    payload: BackgroundOperations[Op]["request"],
  ): Promise<BackgroundOperations[Op]["response"]> {
    let response: CoreApiResponse;

    const message: MessageFromFrontendBackground<Op> = {
      channel: "background",
      operation,
      payload,
    };

    try {
      response = await platform.sendMessageToBackground(message);
    } catch (error) {
      if (error instanceof Error) {
        throw new BackgroundError(
          operation,
          {
            code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
            when: AbsoluteTime.now(),
          },
          error,
        );
      }
      throw error;
    }
    if (response.type === "error") {
      throw new BackgroundError(
        `Background operation "${operation}" failed`,
        response.error,
        TalerError.fromUncheckedDetail(response.error),
      );
    }
    logger.trace("response", response);
    return response.result as any;
  }
}

/**
 * WalletCoreApiClient integration with browser platform
 */
class WalletApiClientImpl implements WalletCoreApiClient {
  async call<Op extends WalletCoreOpKeys>(
    operation: Op,
    payload: WalletCoreRequestType<Op>,
  ): Promise<WalletCoreResponseType<Op>> {
    let response: CoreApiResponse;
    try {
      // FIXME: This type must be fixed and needs documentation!
      // @ts-ignore
      const message: MessageFromFrontendWallet<Op> = {
        channel: "wallet",
        operation,
        payload,
      };
      response = await platform.sendMessageToBackground(message);
    } catch (error) {
      if (error instanceof Error) {
        throw new BackgroundError(
          operation,
          {
            code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
            when: AbsoluteTime.now(),
          },
          error,
        );
      }
      throw error;
    }
    if (response.type === "error") {
      throw new BackgroundError(
        `Wallet operation "${operation}" failed`,
        response.error,
        TalerError.fromUncheckedDetail(response.error),
      );
    }
    logger.trace("got response", response);
    return response.result as any;
  }
}

function onUpdateNotification(
  messageTypes: Array<NotificationType>,
  doCallback: undefined | ((n: WalletNotification) => void),
): () => void {
  //if no callback, then ignore
  if (!doCallback)
    return () => {
      return;
    };
  const onNewMessage = (message: MessageFromBackend): void => {
    const shouldNotify =
      message.type === "wallet" &&
      messageTypes.includes(message.notification.type);
    if (shouldNotify) {
      doCallback(message.notification);
    }
  };
  return platform.listenToWalletBackground(onNewMessage);
}

export type WxApiType = {
  wallet: WalletCoreApiClient;
  background: BackgroundApiClient;
  listener: {
    trigger: (d: ExtensionNotification) => void;
    onUpdateNotification: typeof onUpdateNotification;
  };
};

function trigger(w: ExtensionNotification) {
  platform.triggerWalletEvent({
    type: "web-extension",
    notification: w,
  });
}

export const wxApi = {
  wallet: new WalletApiClientImpl(),
  background: new BackgroundApiClientImpl(),
  listener: {
    trigger,
    onUpdateNotification,
  },
};
