/*
 This file is part of GNU Taler
 (C) 2019-2020 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 
 */
import { canonicalizeBaseUrl } from "./helpers.js";
import { AmountString } from "./taler-types.js";
import { URLSearchParams, URL } from "./url.js";
export type TalerUri =
  | PayUriResult
  | PayTemplateUriResult
  | DevExperimentUri
  | PayPullUriResult
  | PayPushUriResult
  | BackupRestoreUri
  | RefundUriResult
  | RewardUriResult
  | WithdrawUriResult
  | ExchangeUri
  | WithdrawExchangeUri
  | AuditorUri;
export interface PayUriResult {
  type: TalerUriAction.Pay;
  merchantBaseUrl: string;
  orderId: string;
  sessionId: string;
  claimToken?: string;
  noncePriv?: string;
}
export interface PayTemplateUriResult {
  type: TalerUriAction.PayTemplate;
  merchantBaseUrl: string;
  templateId: string;
  templateParams: Record;
}
export interface WithdrawUriResult {
  type: TalerUriAction.Withdraw;
  bankIntegrationApiBaseUrl: string;
  withdrawalOperationId: string;
}
export interface RefundUriResult {
  type: TalerUriAction.Refund;
  merchantBaseUrl: string;
  orderId: string;
}
export interface RewardUriResult {
  type: TalerUriAction.Reward;
  merchantBaseUrl: string;
  merchantRewardId: string;
}
export interface ExchangeUri {
  type: TalerUriAction.Exchange;
  exchangeBaseUrl: string;
  exchangePub: string;
}
export interface AuditorUri {
  type: TalerUriAction.Auditor;
  auditorBaseUrl: string;
  auditorPub: string;
}
export interface PayPushUriResult {
  type: TalerUriAction.PayPush;
  exchangeBaseUrl: string;
  contractPriv: string;
}
export interface PayPullUriResult {
  type: TalerUriAction.PayPull;
  exchangeBaseUrl: string;
  contractPriv: string;
}
export interface DevExperimentUri {
  type: TalerUriAction.DevExperiment;
  devExperimentId: string;
}
export interface BackupRestoreUri {
  type: TalerUriAction.Restore;
  walletRootPriv: string;
  providers: Array;
}
export interface WithdrawExchangeUri {
  type: TalerUriAction.WithdrawExchange;
  exchangeBaseUrl: string;
  exchangePub: string;
  amount?: AmountString;
}
/**
 * Parse a taler[+http]://withdraw URI.
 * Return undefined if not passed a valid URI.
 */
export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
  const pi = parseProtoInfo(s, "withdraw");
  if (!pi) {
    return undefined;
  }
  const parts = pi.rest.split("/");
  if (parts.length < 2) {
    return undefined;
  }
  const host = parts[0].toLowerCase();
  const pathSegments = parts.slice(1, parts.length - 1);
  /**
   * The statement below does not tolerate a slash-ended URI.
   * This results in (1) the withdrawalId being passed as the
   * empty string, and (2) the bankIntegrationApi ending with the
   * actual withdrawal operation ID.  That can be fixed by
   * trimming the parts-list.  FIXME
   */
  const withdrawId = parts[parts.length - 1];
  const p = [host, ...pathSegments].join("/");
  return {
    type: TalerUriAction.Withdraw,
    bankIntegrationApiBaseUrl: canonicalizeBaseUrl(`${pi.innerProto}://${p}/`),
    withdrawalOperationId: withdrawId,
  };
}
/**
 * @deprecated use TalerUriAction
 */
export enum TalerUriType {
  TalerPay = "taler-pay",
  TalerTemplate = "taler-template",
  TalerPayTemplate = "taler-pay-template",
  TalerWithdraw = "taler-withdraw",
  TalerTip = "taler-tip",
  TalerRefund = "taler-refund",
  TalerPayPush = "taler-pay-push",
  TalerPayPull = "taler-pay-pull",
  TalerRecovery = "taler-recovery",
  TalerDevExperiment = "taler-dev-experiment",
  Unknown = "unknown",
}
const talerActionPayPull = "pay-pull";
const talerActionPayPush = "pay-push";
const talerActionPayTemplate = "pay-template";
export enum TalerUriAction {
  Pay = "pay",
  Withdraw = "withdraw",
  Refund = "refund",
  Reward = "reward",
  PayPull = "pay-pull",
  PayPush = "pay-push",
  PayTemplate = "pay-template",
  Exchange = "exchange",
  Auditor = "auditor",
  Restore = "restore",
  DevExperiment = "dev-experiment",
  WithdrawExchange = "withdraw-exchange",
}
interface TalerUriProtoInfo {
  innerProto: "http" | "https";
  rest: string;
}
function parseProtoInfo(
  s: string,
  action: string,
): TalerUriProtoInfo | undefined {
  const pfxPlain = `taler://${action}/`;
  const pfxHttp = `taler+http://${action}/`;
  if (s.toLowerCase().startsWith(pfxPlain)) {
    return {
      innerProto: "https",
      rest: s.substring(pfxPlain.length),
    };
  } else if (s.toLowerCase().startsWith(pfxHttp)) {
    return {
      innerProto: "http",
      rest: s.substring(pfxHttp.length),
    };
  } else {
    return undefined;
  }
}
type Parser = (s: string) => TalerUri | undefined;
const parsers: { [A in TalerUriAction]: Parser } = {
  [TalerUriAction.Pay]: parsePayUri,
  [TalerUriAction.PayPull]: parsePayPullUri,
  [TalerUriAction.PayPush]: parsePayPushUri,
  [TalerUriAction.PayTemplate]: parsePayTemplateUri,
  [TalerUriAction.Restore]: parseRestoreUri,
  [TalerUriAction.Refund]: parseRefundUri,
  [TalerUriAction.Reward]: parseRewardUri,
  [TalerUriAction.Withdraw]: parseWithdrawUri,
  [TalerUriAction.DevExperiment]: parseDevExperimentUri,
  [TalerUriAction.Exchange]: parseExchangeUri,
  [TalerUriAction.Auditor]: parseAuditorUri,
  [TalerUriAction.WithdrawExchange]: parseWithdrawExchangeUri,
};
export function parseTalerUri(string: string): TalerUri | undefined {
  const https = string.startsWith("taler://");
  const http = string.startsWith("taler+http://");
  if (!https && !http) return undefined;
  const actionStart = https ? 8 : 13;
  const actionEnd = string.indexOf("/", actionStart + 1);
  const action = string.substring(actionStart, actionEnd);
  const found = Object.values(TalerUriAction).find((x) => x === action);
  if (!found) return undefined;
  return parsers[found](string);
}
export function stringifyTalerUri(uri: TalerUri): string {
  switch (uri.type) {
    case TalerUriAction.DevExperiment: {
      return stringifyDevExperimentUri(uri);
    }
    case TalerUriAction.Pay: {
      return stringifyPayUri(uri);
    }
    case TalerUriAction.PayPull: {
      return stringifyPayPullUri(uri);
    }
    case TalerUriAction.PayPush: {
      return stringifyPayPushUri(uri);
    }
    case TalerUriAction.PayTemplate: {
      return stringifyPayTemplateUri(uri);
    }
    case TalerUriAction.Restore: {
      return stringifyRestoreUri(uri);
    }
    case TalerUriAction.Refund: {
      return stringifyRefundUri(uri);
    }
    case TalerUriAction.Reward: {
      return stringifyRewardUri(uri);
    }
    case TalerUriAction.Withdraw: {
      return stringifyWithdrawUri(uri);
    }
    case TalerUriAction.Exchange: {
      return stringifyExchangeUri(uri);
    }
    case TalerUriAction.WithdrawExchange: {
      return stringifyWithdrawExchange(uri);
    }
    case TalerUriAction.Auditor: {
      return stringifyAuditorUri(uri);
    }
  }
}
/**
 * Parse a taler[+http]://pay URI.
 * Return undefined if not passed a valid URI.
 */
export function parsePayUri(s: string): PayUriResult | undefined {
  const pi = parseProtoInfo(s, "pay");
  if (!pi) {
    return undefined;
  }
  const c = pi?.rest.split("?");
  const q = new URLSearchParams(c[1] ?? "");
  const claimToken = q.get("c") ?? undefined;
  const noncePriv = q.get("n") ?? undefined;
  const parts = c[0].split("/");
  if (parts.length < 3) {
    return undefined;
  }
  const host = parts[0].toLowerCase();
  const sessionId = parts[parts.length - 1];
  const orderId = parts[parts.length - 2];
  const pathSegments = parts.slice(1, parts.length - 2);
  const p = [host, ...pathSegments].join("/");
  const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
  return {
    type: TalerUriAction.Pay,
    merchantBaseUrl,
    orderId,
    sessionId,
    claimToken,
    noncePriv,
  };
}
export function parsePayTemplateUri(
  uriString: string,
): PayTemplateUriResult | undefined {
  const pi = parseProtoInfo(uriString, talerActionPayTemplate);
  if (!pi) {
    return undefined;
  }
  const c = pi.rest.split("?");
  const parts = c[0].split("/");
  if (parts.length < 2) {
    return undefined;
  }
  const q = new URLSearchParams(c[1] ?? "");
  const params: Record = {};
  q.forEach((v, k) => {
    params[k] = v;
  });
  const host = parts[0].toLowerCase();
  const templateId = parts[parts.length - 1];
  const pathSegments = parts.slice(1, parts.length - 1);
  const hostAndSegments = [host, ...pathSegments].join("/");
  const merchantBaseUrl = canonicalizeBaseUrl(
    `${pi.innerProto}://${hostAndSegments}/`,
  );
  return {
    type: TalerUriAction.PayTemplate,
    merchantBaseUrl,
    templateId,
    templateParams: params,
  };
}
export function parsePayPushUri(s: string): PayPushUriResult | undefined {
  const pi = parseProtoInfo(s, talerActionPayPush);
  if (!pi) {
    return undefined;
  }
  const c = pi?.rest.split("?");
  const parts = c[0].split("/");
  if (parts.length < 2) {
    return undefined;
  }
  const host = parts[0].toLowerCase();
  const contractPriv = parts[parts.length - 1];
  const pathSegments = parts.slice(1, parts.length - 1);
  const hostAndSegments = [host, ...pathSegments].join("/");
  const exchangeBaseUrl = canonicalizeBaseUrl(
    `${pi.innerProto}://${hostAndSegments}/`,
  );
  return {
    type: TalerUriAction.PayPush,
    exchangeBaseUrl,
    contractPriv,
  };
}
export function parsePayPullUri(s: string): PayPullUriResult | undefined {
  const pi = parseProtoInfo(s, talerActionPayPull);
  if (!pi) {
    return undefined;
  }
  const c = pi?.rest.split("?");
  const parts = c[0].split("/");
  if (parts.length < 2) {
    return undefined;
  }
  const host = parts[0].toLowerCase();
  const contractPriv = parts[parts.length - 1];
  const pathSegments = parts.slice(1, parts.length - 1);
  const hostAndSegments = [host, ...pathSegments].join("/");
  const exchangeBaseUrl = canonicalizeBaseUrl(
    `${pi.innerProto}://${hostAndSegments}/`,
  );
  return {
    type: TalerUriAction.PayPull,
    exchangeBaseUrl,
    contractPriv,
  };
}
/**
 * Parse a taler[+http]://reward URI.
 * Return undefined if not passed a valid URI.
 */
export function parseRewardUri(s: string): RewardUriResult | undefined {
  const pi = parseProtoInfo(s, "reward");
  if (!pi) {
    return undefined;
  }
  const c = pi?.rest.split("?");
  const parts = c[0].split("/");
  if (parts.length < 2) {
    return undefined;
  }
  const host = parts[0].toLowerCase();
  const rewardId = parts[parts.length - 1];
  const pathSegments = parts.slice(1, parts.length - 1);
  const hostAndSegments = [host, ...pathSegments].join("/");
  const merchantBaseUrl = canonicalizeBaseUrl(
    `${pi.innerProto}://${hostAndSegments}/`,
  );
  return {
    type: TalerUriAction.Reward,
    merchantBaseUrl,
    merchantRewardId: rewardId,
  };
}
export function parseExchangeUri(s: string): ExchangeUri | undefined {
  const pi = parseProtoInfo(s, "exchange");
  if (!pi) {
    return undefined;
  }
  const c = pi?.rest.split("?");
  const parts = c[0].split("/");
  if (parts.length < 2) {
    return undefined;
  }
  const host = parts[0].toLowerCase();
  const exchangePub = parts[parts.length - 1];
  const pathSegments = parts.slice(1, parts.length - 1);
  const hostAndSegments = [host, ...pathSegments].join("/");
  const exchangeBaseUrl = canonicalizeBaseUrl(
    `${pi.innerProto}://${hostAndSegments}/`,
  );
  return {
    type: TalerUriAction.Exchange,
    exchangeBaseUrl,
    exchangePub,
  };
}
export function parseWithdrawExchangeUri(
  s: string,
): WithdrawExchangeUri | undefined {
  const pi = parseProtoInfo(s, "withdraw-exchange");
  if (!pi) {
    return undefined;
  }
  const c = pi?.rest.split("?");
  const parts = c[0].split("/");
  if (parts.length < 2) {
    return undefined;
  }
  const host = parts[0].toLowerCase();
  const exchangePub = parts[parts.length - 1];
  const pathSegments = parts.slice(1, parts.length - 1);
  const hostAndSegments = [host, ...pathSegments].join("/");
  const exchangeBaseUrl = canonicalizeBaseUrl(
    `${pi.innerProto}://${hostAndSegments}/`,
  );
  const q = new URLSearchParams(c[1] ?? "");
  const amount = q.get("a") ?? undefined;
  return {
    type: TalerUriAction.WithdrawExchange,
    exchangeBaseUrl,
    exchangePub,
    amount,
  };
}
export function parseAuditorUri(s: string): AuditorUri | undefined {
  const pi = parseProtoInfo(s, "auditor");
  if (!pi) {
    return undefined;
  }
  const c = pi?.rest.split("?");
  const parts = c[0].split("/");
  if (parts.length < 2) {
    return undefined;
  }
  const host = parts[0].toLowerCase();
  const auditorPub = parts[parts.length - 1];
  const pathSegments = parts.slice(1, parts.length - 1);
  const hostAndSegments = [host, ...pathSegments].join("/");
  const auditorBaseUrl = canonicalizeBaseUrl(
    `${pi.innerProto}://${hostAndSegments}/`,
  );
  return {
    type: TalerUriAction.Auditor,
    auditorBaseUrl,
    auditorPub,
  };
}
/**
 * Parse a taler[+http]://refund URI.
 * Return undefined if not passed a valid URI.
 */
export function parseRefundUri(s: string): RefundUriResult | undefined {
  const pi = parseProtoInfo(s, "refund");
  if (!pi) {
    return undefined;
  }
  const c = pi?.rest.split("?");
  const parts = c[0].split("/");
  if (parts.length < 3) {
    return undefined;
  }
  const host = parts[0].toLowerCase();
  const sessionId = parts[parts.length - 1];
  const orderId = parts[parts.length - 2];
  const pathSegments = parts.slice(1, parts.length - 2);
  const hostAndSegments = [host, ...pathSegments].join("/");
  const merchantBaseUrl = canonicalizeBaseUrl(
    `${pi.innerProto}://${hostAndSegments}/`,
  );
  return {
    type: TalerUriAction.Refund,
    merchantBaseUrl,
    orderId,
  };
}
export function parseDevExperimentUri(s: string): DevExperimentUri | undefined {
  const pi = parseProtoInfo(s, "dev-experiment");
  const c = pi?.rest.split("?");
  if (!c) {
    return undefined;
  }
  const parts = c[0].split("/");
  return {
    type: TalerUriAction.DevExperiment,
    devExperimentId: parts[0],
  };
}
export function parseRestoreUri(uri: string): BackupRestoreUri | undefined {
  const pi = parseProtoInfo(uri, "restore");
  if (!pi) {
    return undefined;
  }
  const c = pi.rest.split("?");
  const parts = c[0].split("/");
  if (parts.length < 2) {
    return undefined;
  }
  const walletRootPriv = parts[0];
  if (!walletRootPriv) return undefined;
  const providers = new Array();
  parts[1].split(",").map((name) => {
    const url = canonicalizeBaseUrl(
      `${pi.innerProto}://${decodeURIComponent(name)}/`,
    );
    providers.push(url);
  });
  return {
    type: TalerUriAction.Restore,
    walletRootPriv,
    providers,
  };
}
// ================================================
//  To string functions
// ================================================
/**
 * @deprecated use stringifyRecoveryUri
 */
export function constructRecoveryUri(args: {
  walletRootPriv: string;
  providers: string[];
}): string {
  return stringifyRestoreUri(args);
}
/**
 * @deprecated stringifyPayPullUri
 */
export function constructPayPullUri(args: {
  exchangeBaseUrl: string;
  contractPriv: string;
}): string {
  return stringifyPayPullUri(args);
}
/**
 * @deprecated use stringifyPayPushUri
 */
export function constructPayPushUri(args: {
  exchangeBaseUrl: string;
  contractPriv: string;
}): string {
  return stringifyPayPushUri(args);
}
/**
 *
 * @deprecated use stringifyPayUri
 */
export function constructPayUri(
  merchantBaseUrl: string,
  orderId: string,
  sessionId: string,
  claimToken?: string,
  noncePriv?: string,
): string {
  return stringifyPayUri({
    merchantBaseUrl,
    orderId,
    sessionId,
    claimToken,
    noncePriv,
  });
}
export function stringifyPayUri({
  merchantBaseUrl,
  orderId,
  sessionId,
  claimToken,
  noncePriv,
}: Omit): string {
  const { proto, path, query } = getUrlInfo(merchantBaseUrl, {
    c: claimToken,
    n: noncePriv,
  });
  return `${proto}://pay/${path}${orderId}/${sessionId}${query}`;
}
export function stringifyPayPullUri({
  contractPriv,
  exchangeBaseUrl,
}: Omit): string {
  const { proto, path } = getUrlInfo(exchangeBaseUrl);
  return `${proto}://pay-pull/${path}${contractPriv}`;
}
export function stringifyPayPushUri({
  contractPriv,
  exchangeBaseUrl,
}: Omit): string {
  const { proto, path } = getUrlInfo(exchangeBaseUrl);
  return `${proto}://pay-push/${path}${contractPriv}`;
}
export function stringifyRestoreUri({
  providers,
  walletRootPriv,
}: Omit): string {
  const list = providers
    .map((url) => `${encodeURIComponent(new URL(url).href)}`)
    .join(",");
  return `taler://restore/${walletRootPriv}/${list}`;
}
export function stringifyWithdrawExchange({
  exchangeBaseUrl,
  exchangePub,
  amount,
}: Omit): string {
  const { proto, path, query } = getUrlInfo(exchangeBaseUrl, {
    a: amount,
  });
  return `${proto}://withdraw-exchange/${path}${exchangePub}${query}`;
}
export function stringifyDevExperimentUri({
  devExperimentId,
}: Omit): string {
  return `taler://dev-experiment/${devExperimentId}`;
}
export function stringifyPayTemplateUri({
  merchantBaseUrl,
  templateId,
  templateParams,
}: Omit): string {
  const { proto, path, query } = getUrlInfo(merchantBaseUrl, templateParams);
  return `${proto}://pay-template/${path}${templateId}${query}`;
}
export function stringifyRefundUri({
  merchantBaseUrl,
  orderId,
}: Omit): string {
  const { proto, path } = getUrlInfo(merchantBaseUrl);
  return `${proto}://refund/${path}${orderId}`;
}
export function stringifyRewardUri({
  merchantBaseUrl,
  merchantRewardId,
}: Omit): string {
  const { proto, path } = getUrlInfo(merchantBaseUrl);
  return `${proto}://reward/${path}${merchantRewardId}`;
}
export function stringifyExchangeUri({
  exchangeBaseUrl,
  exchangePub,
}: Omit): string {
  const { proto, path } = getUrlInfo(exchangeBaseUrl);
  return `${proto}://exchange/${path}${exchangePub}`;
}
export function stringifyAuditorUri({
  auditorBaseUrl,
  auditorPub,
}: Omit): string {
  const { proto, path } = getUrlInfo(auditorBaseUrl);
  return `${proto}://auditor/${path}${auditorPub}`;
}
export function stringifyWithdrawUri({
  bankIntegrationApiBaseUrl,
  withdrawalOperationId,
}: Omit): string {
  const { proto, path } = getUrlInfo(bankIntegrationApiBaseUrl);
  return `${proto}://withdraw/${path}${withdrawalOperationId}`;
}
/**
 * Use baseUrl to defined http or https
 * create path using host+port+pathname
 * use params to create a query parameter string or empty
 *
 * @param baseUrl
 * @param params
 * @returns
 */
function getUrlInfo(
  baseUrl: string,
  params: Record = {},
): { proto: string; path: string; query: string } {
  const url = new URL(baseUrl);
  let proto: string;
  if (url.protocol === "https:") {
    proto = "taler";
  } else if (url.protocol === "http:") {
    proto = "taler+http";
  } else {
    throw Error(`Unsupported URL protocol in ${baseUrl}`);
  }
  let path = url.hostname;
  if (url.port) {
    path = path + ":" + url.port;
  }
  if (url.pathname) {
    path = path + url.pathname;
  }
  if (!path.endsWith("/")) {
    path = path + "/";
  }
  const qp = new URLSearchParams();
  let withParams = false;
  Object.entries(params).forEach(([name, value]) => {
    if (value !== undefined) {
      withParams = true;
      qp.append(name, value);
    }
  });
  const query = withParams ? "?" + qp.toString() : "";
  return { proto, path, query };
}